viewvc-1.1.22/0000755000175000017500000000000012265242273013307 5ustar cmpilatocmpilatoviewvc-1.1.22/conf/0000755000175000017500000000000012265242267014237 5ustar cmpilatocmpilatoviewvc-1.1.22/conf/cvsgraph.conf.dist0000644000175000017500000003006411160527277017670 0ustar cmpilatocmpilato# CvsGraph configuration # # - Empty lines and whitespace are ignored. # # - Comments start with '#' and everything until # end of line is ignored. # # - Strings are C-style strings in which characters # may be escaped with '\' and written in octal # and hex escapes. Note that '\' must be escaped # if it is to be entered as a character. # # - Some strings are expanded with printf like # conversions which start with '%'. Not all # are applicable at all times, in which case they # will expand to nothing. # %c = cvsroot (with trailing '/') # %C = cvsroot (*without* trailing '/') # %m = module (with trailing '/') # %M = module (*without* trailing '/') # %f = filename without path # %F = filename without path and with ",v" stripped # %p = path part of filename (with trailing '/') # %r = number of revisions # %b = number of branches # %% = '%' # %R = the revision number (e.g. '1.2.4.4') # %P = previous revision number # %B = the branch number (e.g. '1.2.4') # %d = date of revision # %a = author of revision # %s = state of revision # %t = current tag of branch or revision # %0..%9 = command-line argument -0 .. -9 # %l = HTMLized log entry of the revision # NOTE: %l is obsolete. See %(%) and cvsgraph.conf(5) for # more details. # %L = log entry of revision # The log entry expansion takes an optional argument to # specify maximum length of the expansion like %L[25]. # %(...%) = HTMLize the string within the parenthesis. # ViewVC currently uses the following four command-line arguments to # pass URL information to cvsgraph: # -3 link to current file's log page # -4 link to current file's checkout page minus "rev" parameter # -5 link to current file's diff page minus "r1" and "r2" parameters # -6 link to current directory page minus "pathrev" parameter # # - Numbers may be entered as octal, decimal or # hex as in 0117, 79 and 0x4f respectively. # # - Fonts are numbered 0..4 (defined as in libgd) # 0 = tiny # 1 = small # 2 = medium (bold) # 3 = large # 4 = giant # # - Colors are a string like HTML type colors in # the form "#rrggbb" with parts written in hex # rr = red (00..ff) # gg = green (00-ff) # bb = blue (00-ff) # # - There are several reserved words besides of the # feature-keywords. These additional reserved words # expand to numerical values: # * false = 0 # * true = 1 # * not = -1 # * left = 0 # * center = 1 # * right = 2 # * gif = 0 # * png = 1 # * jpeg = 2 # * tiny = 0 # * small = 1 # * medium = 2 # * large = 3 # * giant = 4 # # - Booleans have three possible arguments: true, false # and not. `Not' means inverse of what it was (logical # negation) and is represented by the value -1. # For the configuration file that means that the default # value is negated. # # cvsroot # The *absolute* base directory where the # CVS/RCS repository can be found # cvsmodule # cvsroot = "--unused--"; # unused with ViewVC, will be overridden cvsmodule = ""; # unused with ViewVC -- please leave it blank # color_bg # The background color of the image # transparent_bg # Make color_bg the transparent color (only useful with PNG) color_bg = "#ffffff"; transparent_bg = false; # date_format # The strftime(3) format string for date and time date_format = "%d-%b-%Y %H:%M:%S"; # box_shadow # Add a shadow around the boxes # upside_down # Reverse the order of the revisions # left_right # Draw the image left to right instead of top down, # or right to left is upside_down is set simultaneously. # strip_untagged # Remove all untagged revisions except the first, last and tagged ones # strip_first_rev # Also remove the first revision if untagged # auto_stretch # Try to reformat the tree to minimize image size # use_ttf # Use TrueType fonts for text # anti_alias # Enable pretty TrueType anti-alias drawing # thick_lines # Draw all connector lines thicker (range: 1..11) box_shadow = true; upside_down = false; left_right = false; strip_untagged = false; strip_first_rev = false; #auto_stretch = true; # not yet stable. use_ttf = false; anti_alias = true; thick_lines = 1; # msg_color # Sets the error/warning message color # msg_font # msg_ttfont # msg_ttsize # Sets the error/warning message font msg_color = "#800000"; msg_font = medium; msg_ttfont = "/dos/windows/fonts/ariali.ttf"; msg_ttsize = 11.0; # parse_logs # Enable the parsing of the *entire* ,v file to read the # log-entries between revisions. This is necessary for # the %L expansion to work, but slows down parsing by # a very large factor. You're warned. parse_logs = false; # tag_font # The font of the tag text # tag_color # The color of the tag text # tag_ignore # A extended regular expression to exclude certain tags from view. # See regex(7) for details on the format. # Note 1: tags matched in merge_from/merge_to are always displayed unless # tag_ignore_merge is set to true. # Note 2: normal string rules apply and special characters must be # escaped. # tag_ignore_merge # If set to true, allows tag_ignore to also hide merge_from and merge_to # tags. # tag_nocase # Ignore the case is tag_ignore expressions # tag_negate # Negate the matching criteria of tag_ignore. When true, only matching # tags will be shown. # Note: tags matched with merge_from/merge_to will still be displayed. tag_font = medium; #tag_ttfont = "/dos/windows/fonts/ariali.ttf"; #tag_ttsize = 11.0; tag_color = "#007000"; #tag_ignore = "(test|alpha)_release"; #tag_ignore_merge = false; #tag_nocase = false; #tag_negate = false; # rev_hidenumber # If set to true no revision numbers will be printed in the graph. #rev_hidenumber = false; rev_font = giant; #rev_ttfont = "/dos/windows/fonts/arial.ttf"; #rev_ttsize = 12.0; rev_color = "#000000"; rev_bgcolor = "#f0f0f0"; rev_separator = 1; rev_minline = 15; rev_maxline = 75; rev_lspace = 5; rev_rspace = 5; rev_tspace = 3; rev_bspace = 3; rev_text = "%d"; # or "%d\n%a, %s" for author and state too rev_text_font = tiny; #rev_text_ttfont = "/dos/windows/fonts/times.ttf"; #rev_text_ttsize = 9.0; rev_text_color = "#500020"; rev_maxtags = 25; # merge_color # The color of the line connecting merges # merge_front # If true, draw the merge-lines on top if the image # merge_nocase # Ignore case in regular expressions # merge_from # A regex describing a tag that is used as the merge source # merge_to # A regex describing a tag that is the target of the merge # merge_findall # Try to match all merge_to targets possible. This can result in # multiple lines originating from one tag. # merge_arrows # Use arrows to point to the merge destination. Default is true. # merge_cvsnt # Use CVSNT's mergepoint registration for merges # merge_cvsnt_color # The color of the line connecting merges from/to registered # mergepoints. # arrow_width # arrow_length # Specify the size of the arrows. Default is 3 wide and 12 long. # # NOTE: # - The merge_from is an extended regular expression as described in # regex(7) and POSIX 1003.2 (see also Single Unix Specification at # http://www.opengroup.com). # - The merge_to is an extended regular expression with a twist. All # subexpressions from the merge_from are expanded into merge_to # using %[1-9] (in contrast to \[1-9] for backreferences). Care is # taken to escape the constructed expression. # - A '$' at the end of the merge_to expression can be important to # prevent 'near match' references. Normally, you want the destination # to be a good representation of the source. However, this depends # on how well you defined the tags in the first place. # # Example: # merge_from = "^f_(.*)"; # merge_to = "^t_%1$"; # tags: f_foo, f_bar, f_foobar, t_foo, t_bar # result: # f_foo -> "^t_foo$" -> t_foo # f_bar -> "^t_bar$" -> t_bar # f_foobar-> "^t_foobar$" -> # merge_color = "#a000a0"; merge_front = false; merge_nocase = false; merge_from = "^f_(.*)"; merge_to = "^t_%1$"; merge_findall = false; #merge_arrows = true; #arrow_width = 3; #arrow_length = 12; merge_cvsnt = true; merge_cvsnt_color = "#606000"; # branch_font # The font of the number and tags # branch_color # All branch element's color # branch_[lrtb]space # Interior spacing (margin) # branch_margin # Exterior spacing # branch_connect # Length of the vertical connector # branch_dupbox # Add the branch-tag also at the bottom/top of the trunk # branch_fold # Fold empty branches in one box to save space # branch_foldall # Put all empty branches in one box, even if they # were interspaced with branches with revisions. # branch_resort # Resort the branches by the number of revisions to save space # branch_subtree # Only show the branch denoted or all branches that sprout # from the denoted revision. The argument may be a symbolic # tag. This option you would normally want to set from the # command line with the -O option. branch_font = medium; #branch_ttfont = "/dos/windows/fonts/arialbd.ttf"; #branch_ttsize = 18.0; branch_tag_color= "#000080"; branch_tag_font = medium; #branch_tag_ttfont = "/dos/windows/fonts/arialbi.ttf"; #branch_tag_ttsize = 14.0; branch_color = "#0000c0"; branch_bgcolor = "#ffffc0"; branch_lspace = 5; branch_rspace = 5; branch_tspace = 3; branch_bspace = 3; branch_margin = 15; branch_connect = 8; branch_dupbox = false; branch_fold = true; branch_foldall = false; branch_resort = false; #branch_subtree = "1.2.4"; # title # The title string is expanded (see above for details) # title_[xy] # Position of title # title_font # The font # title_align # 0 = left # 1 = center # 2 = right # title_color title = "%c%p%f\nRevisions: %r, Branches: %b"; title_x = 10; title_y = 5; title_font = small; #title_ttfont = "/dos/windows/fonts/times.ttf"; #title_ttsize = 10.0; title_align = left; title_color = "#800000"; # Margins of the image # Note: the title is outside the margin margin_top = 35; margin_bottom = 10; margin_left = 10; margin_right = 10; # Image format(s) # image_type # gif (0) = Create gif image # png (1) = Create png image # jpeg (2) = Create jpeg image # Image types are available if they can be found in # the gd library. Newer versions of gd do not have # gif anymore. CvsGraph will automatically generate # png images instead. # image_quality # The quality of a jpeg image (1..100) # image_compress # Set the compression of a PNG image (gd version >= 2.0.12). # Values range from -1 to 9 where: # - -1 default compression (usually 3) # - 0 no compression # - 1 lowest level compression # - ... ... # - 9 highest level of compression # image_interlace # Write interlaces PNG/JPEG images for progressive loading. image_type = png; image_quality = 75; image_compress = 3; image_interlace = true; # HTML image map generation # map_name # The name= attribute in ... # map_branch_href # map_branch_alt # map_rev_href # map_rev_alt # map_diff_href # map_diff_alt # map_merge_href # map_merge_alt # These are the href= and alt= attributes in the # tags of HTML. The strings are expanded (see above). map_name = "MyMapName\" name=\"MyMapName"; map_branch_href = "href=\"%6pathrev=%(%t%)\""; map_branch_alt = "alt=\"%0 %(%t%) (%B)\""; # You might want to experiment with the following setting: # 1. The default setting will take you to a ViewVC generated page displaying # that revision of the file, if you click into a revision box: map_rev_href = "href=\"%4rev=%R\""; # 2. This alternative setting will take you to the anchor representing this # revision on a ViewVC generated Log page for that file: # map_rev_href = "href=\"%3#rev%R\""; # map_rev_alt = "alt=\"%1 %(%t%) (%R)\""; map_diff_href = "href=\"%5r1=%P&r2=%R\""; map_diff_alt = "alt=\"%2 %P <-> %R\""; map_merge_href = "href=\"%5r1=%P&r2=%R\""; map_merge_alt = "alt=\"%2 %P <-> %R\""; viewvc-1.1.22/conf/mimetypes.conf.dist0000644000175000017500000000176211160527277020072 0ustar cmpilatocmpilato#--------------------------------------------------------------------------- # # MIME type mapping file for ViewVC # # Information on ViewVC is located at the following web site: # http://viewvc.org/ # #--------------------------------------------------------------------------- # THE FORMAT OF THIS FILE # # This file contains records -- one per line -- of the following format: # # MIME_TYPE [EXTENSION [EXTENSION ...]] # # where whitespace separates the MIME_TYPE from the EXTENSION(s), # and the EXTENSIONs from each other. # # For example: # # text/x-csh csh # text/x-csrc c # text/x-diff diff patch # image/png png # image/jpeg jpeg jpg jpe # # By default, this file is left empty, allowing ViewVC to continue # consulting it first without overriding the MIME type mappings # found in more standard mapping files (such as those provided as # part of the operating system or web server software). # # viewvc-1.1.22/conf/viewvc.conf.dist0000644000175000017500000013010412134325756017352 0ustar cmpilatocmpilato##--------------------------------------------------------------------------- ## ## Configuration file for ViewVC ## ## Information on ViewVC is located at the following web site: ## http://viewvc.org/ ## ##--------------------------------------------------------------------------- ## THE FORMAT OF THIS CONFIGURATION FILE ## ## This file is delineated by sections, specified in [brackets]. Within ## each section, are a number of configuration settings. These settings ## take the form of: name = value. Values may be continued on the ## following line by indenting the continued line. ## ## WARNING: Indentation *always* means continuation. Name=value lines ## should always start in column zero. ## ## Comments should always start in column zero, and are identified ## with "#". By default each of the configuration items is ## commented out, with the default value of the option shown. ## You'll need to remove the '#' that precedes configuration ## options whose values you wish to modify. ## ## Certain configuration settings may have multiple values. These should ## be separated by a comma. The settings where this is allowed are noted ## below. Any other setting that requires special syntax is noted at that ## setting. ## ## ## SOME TERMINOLOGY USED HEREIN ## ## "root" - This is a CVS or Subversion repository. For Subversion, the ## meaning is pretty clear, as the virtual, versioned directory tree ## stored inside a Subversion repository looks nothing like the actual ## tree visible with shell utilities that holds the repository. For ## CVS, this is more confusing, because CVS's repository layout mimics ## (actually, defines) the layout of the stuff housed in the repository. ## But a CVS repository can be identified by the presence of a CVSROOT ## subdirectory in its root directory. ## ## "module" - A module is a top-level subdirectory of a root, usually ## associated with the concept of a single "project" among many housed ## within a single repository. ## ## ## BASIC VIEWVC CONFIGURATION HINTS ## ## While ViewVC has quite a few configuration options, you generally ## only need to change a small subset of them to get your ViewVC ## installation working properly. Here are some options that we ## recommend you pay attention to. Of course, don't try to change the ## options here -- do so in the relevant section of the configuration ## file below. ## ## For correct operation, you will probably need to change the following ## configuration variables: ## ## cvs_roots (for CVS) ## svn_roots (for Subversion) ## root_parents (for CVS or Subversion) ## default_root ## root_as_url_component ## rcs_dir ## mime_types_files ## the many options in the [utilities] section ## ## It is usually desirable to change the following variables: ## ## address ## forbidden ## ## To optimize delivery of ViewVC static files: ## ## docroot ## ## To customize the display of ViewVC for your site: ## ## template_dir ## the [templates] override section ## ##--------------------------------------------------------------------------- [general] ## cvs_roots: Specifies each of the CVS roots on your system and ## assigns names to them. Each root should be given by a "name: path" ## value (where the path is an absolute filesystem path). Multiple roots ## should be separated by commas and can be placed on separate lines. ## ## Example: ## cvs_roots = cvsroot: /opt/cvs/repos1, ## anotherroot: /usr/local/cvs/repos2 ## #cvs_roots = ## svn_roots: Specifies each of the Subversion roots (repositories) on ## your system and assigns names to them. Each root should be given by ## a "name: path" value (where the path is an absolute filesystem path). ## Multiple roots should be separated by commas and can be placed on ## separate lines. ## ## NOTE: ViewVC offers *experimental* support for displaying remote ## Subversion repositories. Simply use the repository's URL instead ## of a local filesystem path when defining the root. ## ## Example: ## svn_roots = svnrepos: /opt/svn/, ## anotherrepos: /usr/local/svn/repos2 ## #svn_roots = ## root_parents: Specifies a list of directories under which any ## number of repositories may reside. You can specify multiple root ## parents separated by commas or new lines, each of which is of the ## form "path: type" (where the type is either "cvs" or "svn", and ## the path is an absolute filesystem path). ## ## Rather than force you to add a new entry to 'cvs_roots' or ## 'svn_roots' each time you create a new repository, ViewVC rewards ## you for organizing all your repositories under a few parent ## directories by allowing you to simply tell it about those parent ## directories. ViewVC will then notice each repository of the ## specified type in that directory as a root whose name is the ## subdirectory in which that repository lives. ## ## For example, if you have three Subversion repositories located at ## /opt/svn/projects, /opt/svn/websites, and /opt/svn/devstuff, you ## could list them individually in svn_roots like so: ## ## svn_roots = projects: /opt/svn/projects, ## websites: /opt/svn/websites, ## devstuff: /opt/svn/devstuff ## ## or you could instead use the root_parents configuration option: ## ## root_parents = /opt/svn: svn ## ## The benefit of this latter approach is that, as you add new ## repositories to your /opt/svn directory, they automatically become ## available for display in ViewVC without additional configuration. ## ## WARNING: the root names derived for repositories configured via the ## root_parents option can, of course, clash with names you have ## defined in your cvs_roots or svn_roots configuration items. If ## this occurs, you can either rename the offending repository on ## disk, or grant new names to the clashing item in cvs_roots or ## svn_roots. Each parent path is processed sequentially, so the ## names of repositories under later parent paths may override earlier ## ones. ## ## Example: ## root_parents = /opt/svn: svn, ## /opt/cvs: cvs ## #root_parents = ## default_root: This is the name of the default root. Valid names ## include those explicitly listed in the cvs_roots and svn_roots ## configuration options, as well as those implicitly indicated by ## virtue of being the basenames of repositories found in the ## root_parents option locations. ## ## NOTE: This setting is ignored when root_as_url_component is enabled. ## ## Example: ## default_root = cvsroot ## #default_root = ## mime_types_files: This is a list of pathnames to a set of MIME type ## mapping files to help ViewVC guess the correct MIME type of a ## versioned file. The pathnames listed here are specified in order of ## authoritativeness either as absolute paths or relative to this ## configuration file. ## ## As a convenience, ViewVC provides a MIME type mapping file ## (mimetypes.conf) which is, by default, the preferred provider of ## MIME type mapping answers, but which is also empty. If you find ## that ViewVC is unable to accurately guess MIME types based on the ## extensions of some of your versioned files, you can add records of ## your preferred mappings to the provided mimetypes.conf file (or to ## your system's mapping files, if you wish). ## ## You might, for example, wish to have ViewVC also consult the mapping ## files provided by your operating system and Apache. ## ## Example: ## mime_types_files = mimetypes.conf, ## /etc/mime.types, ## /usr/local/apache2/conf/mime.types ## #mime_types_files = mimetypes.conf ## address: The address of the local repository maintainer. (This ## option is provided only as a convenience for ViewVC installations ## which are using the default template set, where the value of this ## option will be displayed in the footer of every ViewVC page.) ## ## Example: ## address = admin@server.com ## #address = ## kv_files: Provides a mechanism for custom key/value pairs to be ## available to templates. These are stored in key/value (KV) files. ## ## The paths of the KV files are listed here, specified either as ## absolute paths or relative to this configuration file. The files ## use the same format as this configuration file, containing one or ## more user-defined sections, and user-defined options in those ## sections. ViewVC makes these options available to template authors ## as: ## ## kv.SECTION.OPTION ## ## Note that an option name can be dotted. For example: ## ## [my_images] ## logos.small = /images/small-logo.png ## logos.big = /images/big-logo.png ## ## Templates can use these with a directive like: [kv.my_images.logos.small] ## ## Note that section names which are common to multiple KV files will ## be merged. If two files have a [my_images] section, then the ## options in those two like-named sections will be merged together. ## If two files have the same option name in a section, then one will ## overwrite the other (and which one "wins" is unspecified). ## ## To further categorize the KV files, and how the values are provided to ## the templates, a KV file name may be annotated with an additional level ## of dotted naming. For example: ## ## kv_files = [asf]kv/images.conf ## ## Assuming the same section as above, the template would refer to an image ## using [kv.asf.my_images.logos.small] ## ## Lastly, it is possible to use %lang% in the filenames to specify a ## substitution of the selected language-tag. ## ## Example: ## kv_files = kv/file1.conf, kv/file2.conf, [i18n]kv/%lang%_data.conf ## #kv_files = ## This option is a comma-separated list of language-tag values ## available to ViewVC. The first language-tag listed is the default ## language, and will be used if an Accept-Language header is not ## present in the request, or none of the user's requested languages ## are available. If there are ties on the selection of a language, ## then the first to appear in the list is chosen. ## ## Example: ## languages = en-us, en-gb, de ## #languages = en-us ##--------------------------------------------------------------------------- [utilities] ## ViewVC uses (sometimes optionally) various third-party programs to do some ## of the heavy lifting. Generally, it will attempt to execute those utility ## programs in such a way that if they are found in ViewVC's executable ## search path ($PATH, %PATH%, etc.) all is well. But sometimes these tools ## aren't installed in the executable search path, so here's where you can ## tell ViewVC where to find them. ## ## NOTE: Options with a "_dir" suffix are for configuring the ## directories in which certain programs live. Note that this might ## not be the same directory into which the program's installer dumped ## the whole program package -- we want the deepest directory in which ## the executable program itself resides ("C:\rcstools\bin\win32" ## rather than just "C:\rcstools", for example). The values of options ## whose names lack the "_dir" suffix should point to the actual ## program itself (such as "C:\Program Files\cvsnt\cvs.exe"). ## rcs_dir: Directory in which the RCS utilities are installed, used ## for viewing CVS repositories. ## ## Example: ## rcs_dir = /usr/bin/ ## #rcs_dir = ## cvsnt: Location of cvsnt program. ViewVC can use CVSNT (www.cvsnt.org) ## instead of the RCS utilities to retrieve information from CVS ## repositories. To enable use of CVSNT, set the "cvsnt" value to the ## path of the CVSNT executable. (If CVSNT is on the standard path, you ## can also set it to the name of the CVSNT executable). By default ## "cvsnt" is set to "cvs" on Windows and is not set on other platforms. ## ## Examples: ## cvsnt = K:\Program Files\cvsnt\cvs.exe ## cvsnt = /usr/bin/cvs ## cvsnt = cvs ## #cvsnt = ## svn: Location of the Subversion command-line client, used for ## viewing Subversion repositories. ## ## Example: ## svn = /usr/bin/svn ## #svn = ## diff: Location of the GNU diff program, used for showing file ## version differences. ## ## Example: ## diff = /usr/bin/diff ## #diff = ## cvsgraph: Location of the CvsGraph program, a graphical CVS version ## graph generator (see options.use_cvsgraph). ## ## Example: ## cvsgraph = /usr/local/bin/cvsgraph ## #cvsgraph = ##--------------------------------------------------------------------------- [options] ## root_as_url_component: Interpret the first path component in the URL ## after the script location as the root to use. This is an ## alternative to using the "root=" query key. If ViewVC is configured ## with multiple repositories, this results in more natural looking ## ViewVC URLs. ## ## NOTE: Enabling this option will break backwards compatibility with ## any old ViewCVS URL which doesn't have an explicit "root" parameter. ## #root_as_url_component = 1 ## checkout_magic: Use checkout links with magic /*checkout*/ prefixes so ## checked out HTML pages can have working links to other repository files ## ## NOTE: This option is DEPRECATED and should not be used in new ViewVC ## installations. Setting "default_file_view = co" achieves the same effect ## #checkout_magic = 0 ## allowed_views: List the ViewVC views which are enabled. Views not ## in this comma-delited list will not be served (or, will return an ## error on attempted access). ## ## Valid items for this list include: "annotate", "co", "diff", "markup", ## "roots", "tar". ## ## ----------+--------------------------------------------------------- ## VIEW | DESCRIPTION ## ----------+--------------------------------------------------------- ## annotate | The 'annotate' view shows the contents of a single ## | revision of a versioned file in exactly the same way as ## | the markup view, but with additional line-by-line ## | change attribution (the revision number, author, etc. ## | the most recent edit to that line of text as of the ## | displayed version). ## ----------+--------------------------------------------------------- ## co | The 'co' (aka "checkout" or "download") view isn't ## | really a branded view at all, but allows for direct ## | downloading of the contents of a single revision of a ## | versioned file. ## ----------+--------------------------------------------------------- ## diff | The 'diff' view displays line-based differences between ## | two revisions of a versioned file in a variety of ## | different user-selectable formats. ## ----------+--------------------------------------------------------- ## markup | The 'markup' view shows the contents of a single ## | revision of a versioned file, with syntax highlighting ## | where possible and enabled. It can also optionally ## | show change log information for that revision of the ## | file. ## ----------+--------------------------------------------------------- ## roots | The 'roots' view is a simple listing of the various ## | repositories which ViewVC has been configured to serve ## | to users. ## ----------+--------------------------------------------------------- ## tar | The 'tar' view isn't a branded view, but generates ## | a GNU Tar archive file containing a single versioned ## | directory and its contents (recursively). ## ----------+--------------------------------------------------------- ## #allowed_views = annotate, diff, markup, roots ## Comma-delimited list of MIME content types (with support for fnmatch- ## style glob characters) which are considered not-human-readable and for ## which ViewVC will neither generate links to, nor support the direct ## display of, non-checkout views which carry the file's content (the ## 'markup', 'annotate', 'diff', and 'patch' views). ## ## NOTE: Handling of this option is given priority over ViewVC's ## longstanding support for showing web-friendly file formats -- even ## binary ones such as "image/jpeg" and "image/gif" -- in the 'markup' ## view. Thus, if you add "image/*" to this list, 'markup'-view ## display of JPEG, GIF, and PNG images will be disabled. ## ## Example: ## binary_mime_types = application/octet-stream, image/*, application/pdf, ## application/vnd*, application/msword, audio/* # #binary_mime_types = ## authorizer: The name of the ViewVC authorizer plugin to use when ## authorizing access to repository contents. This value must be the ## name of a Python module addressable as vcauth.MODULENAME (most ## easily accomplished by placing it in ViewVC's lib/vcauth/ directory) ## and which implements a ViewVCAuthorizer class (as a subclass of ## vcauth.GenericViewVCAuthorizer). You can provide custom parameters ## to the authorizer module by defining configuration sections named ## authz-MODULENAME and adding the parameter keys and values there. ## ## ViewVC provides the following modules: ## svnauthz - based on Subversion authz files ## forbidden - simple path glob matches against top-level root directories ## forbiddenre - root and path matches against regular expressions ## ## NOTE: Only one authorizer may be in use for a given ViewVC request. ## It doesn't matter if you configure the parameters of multiple ## authorizer plugins -- only the authorizer whose name is configured ## here (or effectively configured here via per-vhost or per-root ## configuration) will be activated. ## #authorizer = ## hide_cvsroot: Don't show the CVSROOT directory ## 1 Hide CVSROOT directory ## 0 Show CVSROOT directory ## ## NOTE: Someday this option may be removed in favor of letting ## individual authorizer plugin hide the CVSROOT. ## #hide_cvsroot = 1 ## mangle_email_addresses: Mangle email addresses in marked-up output. ## There are various levels of mangling available: ## 0 - No mangling; markup un-mangled email addresses as hyperlinks ## 1 - Obfuscation (using entity encoding); no hyperlinking ## 2 - Data-dropping address truncation; no hyperlinking ## ## NOTE: this will not effect the display of versioned file contents, only ## addresses that appear in version control metadata (e.g. log messages). ## #mangle_email_addresses = 0 ## custom_log_formatting: Specifies mappings of regular expressions to ## substitution format strings used to URL-ize strings found in ## revision log messages. Multiple mappings (specified as ## REGEXP:FORMATSTRING) may be defined, separated by commas. ## ## NOTE: Due to a limitation of the configuration format, commas may ## not be used in the regular expression portion of each mapping. ## Commas "in the raw" may not be used in the format string portion, ## either, but you can probably use the URI-encoded form of the comma ## ("%2C") instead with no ill side-effects. If you must specify a ## colon character in either the regular expression or the format ## string, escape it with a preceding backslash ("\:"). ## ## Example: ## custom_log_formatting = ## artf[0-9]+ : http://example.com/tracker?id=\0, ## issue ([0-9]+) : http://example.com/bug?id=\1&opts=full%2csortby=id ## #custom_log_formatting = ## default_file_view: "log", "co", or "markup" ## Controls whether the default view for file URLs is a checkout view or ## a log view. "log" is the default for backwards compatibility with old ## ViewCVS URLs, but "co" has the advantage that it allows ViewVC to serve ## static HTML pages directly from a repository with working links ## to other repository files ## ## NOTE: Changing this option may break compatibility with existing ## bookmarked URLs. ## ## ALSO NOTE: If you choose one of the "co" or "markup" views, be sure ## to enable it (via the allowed_views option) ## #default_file_view = log ## http_expiration_time: Expiration time (in seconds) for cacheable ## pages served by ViewVC. Note that in most cases, a cache aware ## client will only revalidate the page after it expires (using the ## If-Modified-Since and/or If-None-Match headers) and that browsers ## will also revalidate the page when the reload button is pressed. ## Set to 0 to disable the transmission of these caching headers. ## #http_expiration_time = 600 ## generate_etags: Generate Etag headers for relevant pages to assist ## in browser caching. ## 1 Generate Etags ## 0 Don't generate Etags ## #generate_etags = 1 ## svn_ignore_mimetype: Don't consult the svn:mime-type property to ## determine how to display a file in the markup view. This is ## especially helpful when versioned images carry the default ## Subversion-calculated MIME type of "application/octet-stream" (which ## isn't recognized as viewable type by browsers). ## #svn_ignore_mimetype = 0 ## max_filesize_kbytes: Limit ViewVC's processing of file contents in ## "markup" and "annotate" views to only those files which are smaller ## than this setting, expressed in kilobytes. Set to 0 to disable ## this safeguard. ## ## NOTE: The "co" and "tar" views are unaffected by this setting. ## #max_filesize_kbytes = 512 ## svn_config_dir: Path of the Subversion runtime configuration ## directory ViewVC should consult for various things, including cached ## remote authentication credentials. If unset, Subversion will use ## the default location(s) ($HOME/.subversion, etc.) ## #svn_config_dir = ## use_rcsparse: Use the rcsparse Python module to retrieve CVS ## repository information instead of invoking rcs utilities [EXPERIMENTAL] ## #use_rcsparse = 0 ## sort_by: File sort order ## file Sort by filename ## rev Sort by revision number ## date Sort by commit date ## author Sort by author ## log Sort by log message ## #sort_by = file ## sort_group_dirs: Group directories when sorting ## 1 Group directories together ## 0 No grouping -- sort directories as any other item would be sorted ## #sort_group_dirs = 1 ## hide_attic: Hide or show the contents of the Attic subdirectory ## 1 Hide dead files inside Attic subdir ## 0 Show the files which are inside the Attic subdir ## #hide_attic = 1 ## hide_errorful_entries: Hide or show errorful directory entries ## (perhaps due to not being readable, or some other rlog parsing ## error, etc.) ## 1 Hide errorful entries from the directory display ## 0 Show errorful entries (with their errors) in the directory display ## #hide_errorful_entries = 0 ## log_sort: Sort order for log messages ## date Sort revisions by date ## rev Sort revision by revision number ## none Use the version control system's ordering ## #log_sort = date ## diff_format: Default diff format ## h Human readable ## u Unified diff ## c Context diff ## s Side by side ## l Long human readable (more context) ## f Full human readable (entire file) ## #diff_format = h ## hr_breakable: Diff view line breaks ## 1 lines break at spaces ## 0 no line breaking ## Or, use a positive integer > 1 to cut lines after that many characters ## #hr_breakable = 1 ## hr_funout: Give out function names in human readable diffs. ## (Only works well for C source files, otherwise diff's heuristic falls short.) ## ('-p' option to diff) ## #hr_funout = 1 ## hr_ignore_white: Ignore whitespace (indendation and stuff) for human ## readable diffs. ## ('-w' option to diff) ## #hr_ignore_white = 0 ## hr_ignore_keyword_subst: Ignore diffs which are caused by keyword ## substitution (such as "$Id - Stuff"). ## ('-kk' option to rcsdiff) ## #hr_ignore_keyword_subst = 1 ## hr_intraline: Enable highlighting of intraline changes in human ## readable diffs. [Requires Python 2.4] ## #hr_intraline = 0 ## allow_compress: Allow compression via gzip of output if the Browser ## accepts it (HTTP_ACCEPT_ENCODING contains "gzip"). ## ## NOTE: this relies on Python's gzip module, which has proven to be ## not-so-performant. Enabling this feature should reduce the overall ## transfer size of ViewVC's responses to the client's request, but ## will do so with a speed penalty. ## #allow_compress = 0 ## template_dir: The directory which contains the EZT templates used by ## ViewVC to customize the display of the various output views. ViewVC ## looks in this directory for files with names that match the name of ## the view ("log", "directory", etc.) plus the ".ezt" extension. If ## specified as a relative path, it is relative to the directory where ## this config file resides; absolute paths may be used as well. If ## %lang% occurs in the pathname, then the selected language will be ## substituted. ## ## SEE ALSO: the [templates] configuration section, where you can ## override templates on a per-view basis. ## #template_dir = templates ## docroot: Web path to a directory that contains ViewVC static files ## (stylesheets, images, etc.) If set, static files will get ## downloaded directory from this location. If unset, static files ## will be served by the ViewVC script (at a likely performance ## penalty, and from the "docroot" subdirectory of the directory ## specified by the "template_dir" option). ## ## NOTE: This option is evaluated outside the context of a particular ## root. Be careful when using per-root configuration to select an ## alternate template set as the default value for this option will ## still be based on the global default template set per 'template_dir' ## above, not on 'template_dir' as overridden for a given root. ## #docroot = ## show_subdir_lastmod: Show last changelog message for CVS subdirectories ## ## NOTE: The current implementation makes many assumptions and may show ## the incorrect file at some times. The main assumption is that the ## last modified file has the newest filedate. But some CVS operations ## touches the file without even when a new version is not checked in, ## and TAG based browsing essentially puts this out of order, unless ## the last checkin was on the same tag as you are viewing. Enable ## this if you like the feature, but don't rely on correct results. ## ## SECURITY WARNING: Enabling this will currently leak unauthorized ## path names. ## #show_subdir_lastmod = 0 ## show_roots_lastmod: In the root listing view, show the most recent ## modifications made to the root. (Subversion roots only.) ## ## NOTE: Enabling this feature will significantly reduce the ## performance of the root listing view. ## #show_roots_lastmod = 0 ## show_logs: Show the most recent log entry in directory listings. ## #show_logs = 1 ## show_log_in_markup: Show log when viewing file contents. ## #show_log_in_markup = 1 ## cross_copies: Cross filesystem copies when traversing Subversion ## file revision histories. ## #cross_copies = 1 ## use_localtime: Display dates as UTC or in local time zone. ## #use_localtime = 0 ## iso8601_dates: Display timestamps using a standard ISO-8601 format. ## #iso8601_timestamps = 0 ## short_log_len: The length (in characters) to which the most recent ## log entry should be truncated when shown in the directory view. ## #short_log_len = 80 ## enable_syntax_coloration: Should we colorize known file content ## syntaxes? ## ## NOTE: This feature requires the Pygments Python module ## (http://pygments.org) and works only when ViewVC can determine the ## MIME content type of the file whose contents it wishes to colorize. ## Use the 'mime_types_files' configuration option to specify MIME ## type mapping files useful for making that determination. ## #enable_syntax_coloration = 1 ## tabsize: The number of spaces into which horizontal tab characters ## are converted when viewing file contents. Set to 0 to preserve ## tab characters. ## #tabsize = 8 ## detect_encoding: Should we attempt to detect versioned file ## character encodings? [Requires 'chardet' module, and is currently ## used only for the 'markup' and 'annotate' views.] ## #detect_encoding = 0 ## use_cvsgraph: Use CvsGraph to offer visual graphs of CVS revision history. ## #use_cvsgraph = 0 ## cvsgraph_conf: Location of the customized cvsgraph configuration file. ## May be specified as an absolute path or as a path relative to this ## configuration file. ## #cvsgraph_conf = cvsgraph.conf ## use_re_search: Enable regular expression search of files in a directory. ## ## WARNING: Enabling this option can consume HUGE amounts of server ## time. A "checkout" must be performed on *each* file in a directory, ## and the result needs to be searched for a match against the regular ## expression. ## ## SECURITY WARNING: Since a user can enter the regular expression, it ## is possible for them to enter an expression with many alternatives ## and a lot of backtracking. Executing that search over thousands of ## lines over dozens of files can easily tie up a server for a long ## period of time. This option should only be used on sites with ## trusted users. It is highly inadvisable to use this on a public site. ## #use_re_search = 0 ## dir_pagesize: Maximum number of directory entries on a given page. ## This allows ViewVC to present discrete pages to the users instead of ## the entire directory. Set to 0 to disable pagination. ## #dir_pagesize = 0 ## log_pagesize: Maximum number of revision log entries on a given page. ## This allows ViewVC to present discrete pages to the users instead of ## the entire revision log. Set to 0 to disable pagination. ## #log_pagesize = 0 ## log_pagesextra: Maximum number of extra pages (based on ## log_pagesize) of revision log data to fetch and present to the user ## as additional options for display. Revision log information ## "beyond" this window is still accessible, but must be navigated to ## in multiple steps. ## ## Example: ## log_pagesize = 100 ## log_pagesextra = 3 ## ## For a versioned file with 1000 revisions, the above settings would ## present to the user the first 100 of those 1000 revisions, with ## links to three additional pages (the 200-299th revisions, 300-399th ## revisions, and 400-499th revisions) plus a link to the 500th ## revision. Following these links slides the display "window", ## showing the requested set of revisions plus links to three ## additional pages beyond those, and so on. ## #log_pagesextra = 3 ## limit_changes: Maximum number of changed paths shown per commit in ## the Subversion revision view and in query results. This is not a ## hard limit (the UI provides options to show all changed paths), but ## it prevents ViewVC from generating enormous and hard to read pages ## by default when they happen to contain import or merge commits ## affecting hundreds or thousands of files. Set to 0 to disable the ## limit. ## #limit_changes = 100 ##--------------------------------------------------------------------------- [templates] ## You can override the templates used by various ViewVC views in this ## section. By default, ViewVC will look for templates in the ## directory specified by the "template_dir" configuration option (see ## the documentation for that option for details). But if you want to ## use a different template for a particular view, simply uncomment the ## appropriate option below and specify the currect location of the EZT ## template file you wish to use for that view. ## ## Templates are specified relative to the configured template ## directory (see the "template_dir" option), but absolute paths may ## also be used as well. ## ## If %lang% occurs in the pathname, then the selected language will be ## substituted. ## ## NOTE: the selected language is defined by the "languages" item in the ## [general] section, and based on the request's Accept-Language ## header. ## ## diff: Template used for the file differences view. ## #diff = ## directory: Template used for the directory listing view. ## #directory = ## error: Template used for the ViewVC error display view. ## #error = ## file: Template used for the file contents/annotation view. ## #file = ## graph: Template used for the revision graph view. ## #graph = ## log: Template used for the revision log view. ## #log = ## query: Template used for the non-integrated query interface. ## #query = ## query_form: Template used for the query form view. ## #query_form = ## query_results: Template used for the query results view. ## #query_results = ## revision: Template used for the revision/changeset view. ## #revision = ## roots: Template used for the root listing view. ## #roots = ##--------------------------------------------------------------------------- [cvsdb] ## enabled: Enable database integration feature. ## #enabled = 0 ## host: Database hostname. Leave unset to use a local Unix socket ## connection. ## #host = ## post: Database listening port. ## #port = 3306 ## database_name: ViewVC database name. ## #database_name = ViewVC ## user: Username of user with read/write privileges to the database ## specified by the 'database_name' configuration option. ## #user = ## passwd: Password of user with read/write privileges to the database ## specified by the 'database_name' configuration option. ## #passwd = ## readonly_user: Username of user with read privileges to the database ## specified by the 'database_name' configuration option. ## #readonly_user = ## readonly_passwd: Password of user with read privileges to the database ## specified by the 'database_name' configuration option. ## #readonly_passwd = ## row_limit: Maximum number of rows returned by a given normal query ## to the database. ## ## NOTE: This limits the amount of data provided to ViewVC by the ## database. It is from this already-reduced data set that ViewVC ## builds the query response it presents to the user, which may or may ## not include still more limiting via the query form's 'limit' ## parameter. In other words, there is no value which the user can use ## in the query form's 'limit' parameter which will cause more data to ## be returned by the database for ViewVC to process. ## #row_limit = 1000 ## rss_row_limit: Maximum number of rows returned by a given query to ## the database made as part of an RSS feed request. (Keeping in mind ## that RSS readers tend to poll regularly for new data, you might want ## to keep this set to a conservative number.) ## ## See also the `NOTE' for the 'row_limit' option, which applies here ## as well. ## #rss_row_limit = 100 ## check_database_for_root: Check if the repository is found in the ## database before showing the query link and RSS feeds. ## ## WARNING: Enabling this check adds the cost of a database connection ## and query to most ViewVC requests. If all your roots are represented ## in the commits database, or if you don't care about the creation of ## RSS and query links that might lead ultimately to error pages for ## certain of your roots, or if you simply don't want to add this extra ## cost to your ViewVC requests, leave this disabled. ## #check_database_for_root = 0 ##--------------------------------------------------------------------------- [vhosts] ## Virtual hosts are individual logical servers accessible via ## different hostnames, but which are all really the same physical ## computer. For example, you might have your web server configured to ## accept incoming traffic for both http://www.yourdomain.com/ and ## http://viewvc.yourdomain.com/. Users pointing their web browsers at ## each of those two URLs might see entirely different content via one ## URL versus the other, but all that content actually lives on the ## same computer, is served up via the same web server, and so ## on. It just *looks* like its coming from multiple servers. ## ## ViewVC allows you to customize its configuration options for ## individual virtual hosts. You might, for example, wish to expose ## all of your Subversion repositories at http://svn.yourdomain.com/viewvc/ ## and all your CVS ones at http://cvs.yourdomain.com/viewvc/, with no ## cross-exposure. Using ViewVC's virtual host (vhost) configuration ## support, you can do this. Simply create two vhost configurations ## (one for each of your hostnames), then configure the cvs_roots ## option only for the vhost associated with cvs.yourdomain.com, and ## configure the svn_roots option only for the vhost associated with ## svn.yourdomain.com. ## ## This section is a freeform configuration section, where you create ## both the option names and their values. The names of the options ## are then treated as canonical names of virtual hosts, and their ## values are defined to be comma-delimited lists of hostname globs ## against which incoming ViewVC requests will be matched to figure out ## which vhost they apply to. ## ## After you've named and defined your vhosts, you may then create new ## configuration sections whose names are of the form ## vhost-VHOSTNAME/CONFIGSECTION. VHOSTNAME here is the canonical name ## of one of the virtual hosts you defined under the [vhosts] section. ## Inside those configuration sections, you override the standard ## ViewVC options typically found in the base configuration section ## named CONFIGSECTION ("general", "option", etc.) ## ## NOTE: Per-vhost overrides may only be applied to the following ## sections: ## ## general ## options ## utilities ## templates ## cvsdb ## authz-* ## ## Here is an example: ## ## [vhosts] ## libs = libs.yourdomain.*, *.yourlibs.* ## gui = guiproject.yourdomain.* ## ## [vhost-libs/general] ## cvs_roots = ## svn_roots = svnroot: /var/svn/libs-repos ## default_root = svnroot ## ## [vhost-libs/options] ## show_logs = 1 ## ## [vhost-gui/general] ## cvs_roots = cvsroot: /var/cvs/guiproject ## svn_roots = ## default_root = cvsroot ## ##--------------------------------------------------------------------------- ## ViewVC recognizes per-root configuration overrides, too. To ## override the value of a configuration parameter only for a single ## root, create a configuration section whose names is of the form ## root-ROOTNAME/CONFIGSECTION. ROOTNAME here is the name of the root ## as defined explicitly in cvs_roots or svn_roots or implicitly as the ## basename of a root path in root_parents. Options found in this new ## configuration section override for this one root the corresponding ## options found in the base configuration section CONFIGSECTION ## ("options", "authz-*", etc.) as interpreted after per-vhost ## overrides (if any) have been applied. ## ## NOTE: Per-root overrides may only be applied to the following ## sections: ## ## options ## utilities ## authz-* ## ## WARNING: Do not use per-root overrides if your ViewVC instance is ## served via the standalone.py server option! Doing so could cause ## ViewVC to be unable to function properly (or at all). ## ## Here is an example showing how to enable Subversion authz-based ## authorization for only the single root named "svnroot": ## ## [root-svnroot/options] ## authorizer = svnauthz ## ## [root-svnroot/authz-svnauthz] ## authzfile = /path/to/authzfile ## ##--------------------------------------------------------------------------- [authz-forbidden] ## The "forbidden" authorizer forbids access to repository modules, ## defined to be top-level subdirectories in a repository. ## ## NOTE: The options in this section apply only when the 'authorizer' ## option (in the [options] section) is set to 'forbidden'. ## forbidden: A comma-delimited list of patterns which match modules ## that ViewVC should hide from users. ## ## You can use a simple list of modules, or something more complex: ## ## *) The "!" can be used before a module to explicitly state that it ## is NOT forbidden. Whenever this form is seen, then all modules will ## be forbidden unless one of the "!" modules match. ## ## *) Shell-style "glob" expressions may be used. "*" will match any ## sequence of zero or more characters, "?" will match any single ## character, "[seq]" will match any character in seq, and "[!seq]" ## will match any character not in seq. ## ## *) Tests are performed in sequence. The first match will terminate the ## testing. This allows for more complex allow/deny patterns. ## ## Tests are case-sensitive. ## ## NOTE: Again, this is for the hiding of modules within repositories, *not* ## for the hiding of repositories (roots) themselves. ## ## Some examples: ## ## Disallow "example" but allow all others: ## forbidden = example ## ## Disallow "example1" and "example2" but allow all others: ## forbidden = example1, example2 ## ## Allow *only* "example1" and "example2": ## forbidden = !example1, !example2 ## ## Forbid modules starting with "x": ## forbidden = x* ## ## Allow modules starting with "x" but no others: ## forbidden = !x* ## ## Allow "xml", forbid other modules starting with "x", and allow the rest: ## forbidden = !xml, x*, !* ## #forbidden = ##--------------------------------------------------------------------------- [authz-forbiddenre] ## The "forbiddenre" authorizer forbids access to repositories and ## repository paths by comparing a list of regular expressions ## (separated by commas) against paths consisting of the repository (or ## root) name plus the path of the versioned file or directory to be ## tested. For example, to see if the user is authorized to see the ## path "/trunk/www/index.html" in the repository whose root name is ## "svnrepos", this authorizer will check the path ## "svnrepos/trunk/www/index.html" against the list of forbidden ## regular expressions. Directory paths will be terminated by a forward ## slash. ## ## NOTE: The options in this section apply only when the 'authorizer' ## option (in the [options] section) is set to 'forbiddenre'. ## forbiddenre: A comma-delimited list of regular expressions which ## match paths that ViewVC should hide from users. ## ## Like the "forbidden" authorizer... ## ## *) The "!" can be used before a module to explicitly state that it ## is NOT forbidden. Whenever this form is seen, then all modules will ## be forbidden unless one of the "!" modules match. ## ## *) Tests are performed in sequence. The first match will terminate the ## testing. This allows for more complex allow/deny patterns. ## ## Unlike the "forbidden" authorizer, you can can use this to hide roots, too. ## ## Some examples: ## ## Disallow files named "PRIVATE", but allow all others: ## forbiddenre = /PRIVATE$ ## ## Disallow the "hidden" repository, allowing all others: ## forbiddenre = ^hidden(/|$) ## ## Allow only the "example1" and "example2" roots and the paths inside them, ## disallowing all others (which can be done in multiple ways): ## forbiddenre = !^example1(/|$), !^example2(/|$)/ ## forbiddenre = !^example[12](/|$) ## ## Only allow visibility of HTML files and the directories that hold them: ## forbiddenre = !^([^/]+|.*(/|\.html))$ ## #forbiddenre = ##--------------------------------------------------------------------------- [authz-svnauthz] ## The "svnauthz" authorizer uses a Subversion authz configuration file ## to determine access to repository paths. ## ## NOTE: The options in this section apply only when the 'authorizer' ## option (in the [options] section) is set to 'svnauthz'. ## authzfile: Specifies the location of the authorization rules file ## (using an absolute path). ## #authzfile = ## force_username_case: Like the AuthzForceUsernameCase httpd.conf ## directive, set this to "upper" or "lower" to force the normalization ## to upper- or lower-case, respectively, of incoming usernames prior ## to comparison against the authorization rules files. Leave the ## option unset to preserve the username case. ## #force_username_case = ##--------------------------------------------------------------------------- [query] ## The configuration items in this section are used exclusively by the ## 'query' script, a separate script from ViewVC itself that ships ## with ViewVC and allows for queries into the ViewVC commits ## database. If you aren't using this separate script (which was made ## largely irrelevant by the introduction of an integrated "query" ## view in ViewVC itself, or aren't using the ViewVC commits database ## functionality at all, you can ignore these configurations items ## altogether. ## viewvc_base_url: Base URL at which ViewVC may be accessed on this ## server. The default value for this option is determined at ## run-time by the various front-ends to the query script. ## ## Examples: ## viewvc_base_url = /viewvc.py ## viewvc_base_url = /viewvc.wsgi ## viewvc_base_url = /cgi-bin/viewvc ## viewvc_base_url = viewvc ## ## To disable cross-linking between the query script and ViewVC, ## uncomment this option and leave its value empty. ## #viewvc_base_url = ##--------------------------------------------------------------------------- viewvc-1.1.22/README0000644000175000017500000000032210351044240014150 0ustar cmpilatocmpilatoViewVC -- Viewing the content of CVS/SVN repositories with a Webbrowser. Please read the file INSTALL for more information. And see windows/README for more information on running ViewVC on Microsoft Windows. viewvc-1.1.22/templates-contrib/0000755000175000017500000000000012265242273016743 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/README0000644000175000017500000000042611204260775017624 0ustar cmpilatocmpilatoThis directory contains ViewVC template sets contributed by their respective authors and expected to work against ViewVC 1.1.x. They are not necessarily supported by the ViewVC development community, and do not necessarily carry the same license or copyright as ViewVC itself. viewvc-1.1.22/templates-contrib/newvc/0000755000175000017500000000000012265242273020065 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/newvc/README0000644000175000017500000000037611015337675020756 0ustar cmpilatocmpilatoTemplate Set: newvc Author(s): C. Michael Pilato Compatibility: ViewVC 1.1 The "newvc" template set uses top navigation tabs to flip between various views of a file or directory, and aims for a clean-yet-modern look and feel. viewvc-1.1.22/templates-contrib/newvc/templates/0000755000175000017500000000000012265242273022063 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/newvc/templates/diff.ezt0000644000175000017500000001156711015337675023534 0ustar cmpilatocmpilato[# Setup page definitions] [define page_title]Diff of:[end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "diff"]
[for diff_format_hidden_values][end] (Generate patch)
[if-any raw_diff]
[raw_diff]
[else] [define change_right][end] [define last_change_type][end] [# these should live in stylesheets] [for changes] [is changes.type "change"][else] [if-any change_right][change_right][define change_right][end][end] [end] [is changes.type "header"] [else] [is changes.type "add"] [else] [is changes.type "remove"] [else] [is changes.type "change"] [if-any changes.have_left] [end] [define change_right][change_right] [if-any changes.have_right] [end] [end] [else] [is changes.type "no-changes"] [else] [is changes.type "binary-diff"] [else] [is changes.type "error"] [else][# a line of context] [end][end][end][end][end][end][end] [define last_change_type][changes.type][end] [end] [if-any change_right][change_right][end]
# Line [changes.line_info_left] | Line [changes.line_info_right]
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end] + [changes.right]
[changes.line_number] [changes.left]
[changes.line_number] < [changes.left]
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end] > [changes.right]
- No changes -
- Binary file revisions differ -
- ViewVC depends on rcsdiff and GNU diff to create this page. ViewVC cannot find GNU diff. Even if you have GNU diff installed, the rcsdiff program must be configured and compiled with the GNU diff location. -
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end]   [changes.right]

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines
[end]
[include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/newvc/templates/directory.ezt0000644000175000017500000001122111305230101024565 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Index of:[end] [define help_href][docroot]/help_[if-any where]dir[else]root[end]view.html[end] [# end] [include "include/header.ezt" "directory"] [if-any where][else] [end] [is picklist_len "0"][else][is picklist_len "1"][else] [end][end]
Jump to page:
[for dir_paging_hidden_values][end]
[is roottype "svn"] [if-any rev]r[rev][end] [else] [is num_dead "0"] [else] [if-any attic_showing] Hide [else] Show [end] dead files [end] [end]
[if-any up_href] [end] [for entries] [define click_href][is entries.pathtype "dir"][entries.view_href][else][if-any entries.prefer_markup][entries.view_href][else][entries.download_href][end][end][end] [end]
File [is sortby "file"] [is sortdir [end] Last Change [is sortby "rev"] [is sortdir [end]
 ../
[if-any click_href][end] [entries.name][is entries.pathtype "dir"]/[end][if-any click_href][end] [is entries.state "dead"](dead)[end] [if-any entries.rev] [if-any entries.log_href][entries.rev][else][entries.rev][end] ([entries.ago] ago) by [entries.author]: [entries.log] [is entries.pathtype "dir"][is roottype "cvs"] (from [entries.log_file]/[entries.log_rev]) [end][end] [end]
[if-any search_re_action]
[for search_re_hidden_values][end]
[if-any search_re]
[for search_re_hidden_values][end]
[end]          [end] [include "include/pathrev_form.ezt"]          [files_shown] file[is files_shown "1"][else]s[end] shown
[include "include/props.ezt"]
[include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/newvc/templates/query.ezt0000644000175000017500000001703711015337675023767 0ustar cmpilatocmpilato Checkin Database Query [# setup page definitions] [define help_href][docroot]/help_query.html[end] [# end]

Select your parameters for querying the CVS commit database. You can search for multiple matches by typing a comma-seperated list into the text fields. Regular expressions, and wildcards are also supported. Blank text input fields are treated as wildcards.

Any of the text entry fields can take a comma-seperated list of search arguments. For example, to search for all commits from authors jpaint and gstein, just type: jpaint, gstein in the Author input box. If you are searching for items containing spaces or quotes, you will need to quote your request. For example, the same search above with quotes is: "jpaint", "gstein".

Wildcard and regular expression searches are entered in a similar way to the quoted requests. You must quote any wildcard or regular expression request, and a command charactor preceeds the first quote. The command charactor l is for wildcard searches, and the wildcard charactor is a percent (%). The command charactor for regular expressions is r, and is passed directly to MySQL, so you'll need to refer to the MySQL manual for the exact regex syntax. It is very similar to Perl. A wildard search for all files with a .py extention is: l"%.py" in the File input box. The same search done with a regular expression is: r".*\.py".

All search types can be mixed, as long as they are seperated by commas.

CVS Repository:
CVS Branch:
Directory:
File:
Author:
Sort By:
Date:
In the last hours
In the last day
In the last week
In the last month
Since the beginning of time
[is query "skipped"] [else]

[num_commits] matches found.

[if-any commits] [# uncommment, if you want a separate Description column: (also see below) ] [for commits] [for commits.files] [# uncommment, if you want a separate Description column: {if-index commits.files first{ {end} (substitute brackets for the braces) ] [# and also take the following out in the "Description column"-case:] [if-index commits.files last] [end] [# ---] [end] [end] [# uncommment, if you want a separate Description column: ]
Revision File Branch +/- Date AuthorDescription
[if-any commits.files.rev][commits.files.rev][else] [end] [commits.files.link] [if-any commits.files.branch][commits.files.branch][else] [end] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [commits.files.plus]/[commits.files.minus] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [if-any commits.files.date][commits.files.date][else] [end] [if-any commits.files.author][commits.files.author][else] [end] {commits.log}
  Log:
[commits.log]
            
[end] [end] [include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/newvc/templates/markup.ezt0000644000175000017500000000074411015337675024116 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]View of:[end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "markup"] [include "include/fileview.ezt"]
[markup]
[include "include/props.ezt"]
[include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/newvc/templates/revision.ezt0000644000175000017500000000576211441730461024453 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Revision [rev] of:[end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "revision"]
Jump to revision: [for jump_rev_hidden_values][end] [if-any prev_href] Previous[end] [if-any next_href] Next[end]
Author: [if-any author][author][else](unknown author)[end]
Date: [if-any date][date][else](unknown date)[end] [if-any ago]([ago] ago)[end]
Log Message:
[log]
[include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/newvc/templates/docroot/0000755000175000017500000000000012265242273023534 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/newvc/templates/docroot/help_rootview.html0000644000175000017500000001673611015337675027330 0ustar cmpilatocmpilato ViewVC Help: General

ViewVC Help: General

Help

General
Directory View
Log View

Internet

Home
Upgrading
Contributing
License

ViewVC is a WWW interface for CVS and Subversion repositories. It allows you to browse the files and directories in a repository while showing you metadata from the repository history: log messages, modification dates, author names, revision numbers, copy history, and so on. It provides several different views of repository data to help you find the information you are looking for:

Multiple Repositories

A single installation of ViewVC is often used to provide access to more than one repository. In these installations, ViewVC shows a Project Root drop down box in the top right corner of every generated page to allow for quick access to any repository.

Sticky Revision and Tag

By default, ViewVC will show the files and directories and revisions that currently exist in the repository. But it's also possible to browse the contents of a repository at a point in its past history by choosing a "sticky tag" (in CVS) or a "sticky revision" (in Subversion) from the forms at the top of directory and log pages. They're called sticky because once they're chosen, they stick around when you navigate to other pages, until you reset them. When they're set, directory and log pages only show revisions preceding the specified point in history. In CVS, when a tag refers to a branch or a revision on a branch, only revisions from the branch history are shown, including branch points and their preceding revisions.

Dead Files

In CVS directory listings, ViewVC can optionally display dead files. Dead files are files which used to be in a directory but are currently deleted, or files which just don't exist in the currently selected sticky tag. Dead files cannot be shown in Subversion repositories. The only way to see a deleted file in a Subversion directory is to navigate to a sticky revision where the file previously existed.

Artificial Tags

In CVS Repositories, ViewVC adds artificial tags HEAD and MAIN to tag listings and accepts them in place of revision numbers and real tag names in all URLs. MAIN acts like a branch tag pointing at the default branch, while HEAD acts like a revision tag pointing to the latest revision on the default branch. The default branch is usually just the trunk, but may be set to other branches inside individual repository files. CVS will always check out revisions from a file's default branch when no other branch is specified on the command line.

More Information

More information about ViewVC is available from viewvc.org. See the links below for guides to CVS and Subversion

Documentation about CVS

Open Source Development with CVS
CVS User's Guide
Another CVS tutorial
Yet another CVS tutorial (a little old, but nice)
An old but very useful FAQ about CVS

Documentation about Subversion

Version Control with Subversion


ViewVC Users Mailinglist
viewvc-1.1.22/templates-contrib/newvc/templates/docroot/help_dirview.html0000644000175000017500000001245111015337675027111 0ustar cmpilatocmpilato ViewVC Help: Directory View

ViewVC Help: Directory View

Help

General
Directory View
Log View

Internet

Home
Upgrading
Contributing
License

The directory listing view should be a familiar sight to any computer user. It shows the path of the current directory being viewed at the top of the page. Below that is a table summarizing the directory contents, and then comes actual contents, a sortable list of all files and subdirectories inside the current directory.

The summary table is made up of some or all of the following rows:

  • Files Shown - Number of files shown in the directory listing. This might be less than the actual number of files in the directory if a regular expression search is in place, hiding files which don't meet the search criteria. In CVS directory listings, this row will also have a link to toggle display of dead files, if any are present.
  • Directory Revision - For Subversion directories only. Shown as "# of #" where the first number is the most recent repository revision where the directory (or a path underneath it) was modified. The second number is just the latest repository revision. Both numbers are links to revision views
  • Sticky Revision/Tag - shows the current sticky revision or tag and contains form fields to set or clear it.
  • Current Search - If a regular expression search is in place, shows the search string.
  • Query - Provides a link to a query form for the directory

The actual directory list is a table with filenames and directory names in one column and information about the most recent revisions where each file or directory was modified in the other columns. Column headers can be clicked to sort the directory entries in order by a column, and clicked again to reverse the sort order.

File names are links to log views showing a list of revisions where a file was modified. Revision numbers are links to either view or download a file (depending on its file type). The links are reversed for directories. Directory revision numbers are links to log views, while directory names are links showing the contents of those directories. Also, in CVS repositories with the graph view enabled, there will be small icons next to file names which are links to revision graphs.

Depending on how ViewVC is configured, there may be more options at the bottom of directory pages:

  • Regular expression search - If enabled, will show a form field accepting a search string (a python regular expression). Once submitted, only files that have at least one occurance of the expression will show up in directory listings.
  • Tarball download - If enabled, will show a link to download a gzipped tar archive of the directory contents.

ViewVC Users Mailinglist
viewvc-1.1.22/templates-contrib/newvc/templates/docroot/help_query.html0000644000175000017500000000556511015337675026615 0ustar cmpilatocmpilato ViewVC Help: Query The Commit Database

ViewVC Help: Query The Commit Database

Other Help:

General
Directory View
Classic Log View
Alternative Log View
Query Database

Internet

Home
Upgrading
Contributing
License

Select your parameters for querying the CVS commit database in the form at the top of the page. You can search for multiple matches by typing a comma-seperated list into the text fields. Regular expressions, and wildcards are also supported. Blank text input fields are treated as wildcards.

Any of the text entry fields can take a comma-seperated list of search arguments. For example, to search for all commits from authors jpaint and gstein, just type: jpaint, gstein in the Author input box. If you are searching for items containing spaces or quotes, you will need to quote your request. For example, the same search above with quotes is: "jpaint", "gstein".

Wildcard and regular expression searches are entered in a similar way to the quoted requests. You must quote any wildcard or regular expression request, and a command character preceeds the first quote. The command character l(lowercase L) is for wildcard searches, and the wildcard character is a percent (%). The command character for regular expressions is r, and is passed directly to MySQL, so you'll need to refer to the MySQL manual for the exact regex syntax. It is very similar to Perl. A wildard search for all files with a .py extention is: l"%.py" in the File input box. The same search done with a regular expression is: r".*\.py".

All search types can be mixed, as long as they are seperated by commas.

viewvc-1.1.22/templates-contrib/newvc/templates/docroot/scripts.js0000644000175000017500000000006111015540775025556 0ustar cmpilatocmpilatofunction jumpTo(url) { window.location = url; }viewvc-1.1.22/templates-contrib/newvc/templates/docroot/images/0000755000175000017500000000000012265242273025001 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/newvc/templates/docroot/images/dir.png0000644000175000017500000000034411015337675026271 0ustar cmpilatocmpilato‰PNG  IHDRíÝâRPLTEÀÀÀÿÜ®ÿÞ­¸ }WK;+%kÅ9ûtRNS@æØfbKGDˆH pHYs  šœtIMEÔ 6Aw¼}HIDATxÚmƱ€0À_e7H&пˆ ½/û`”^u€#=?…~Æfòx"±ÌZ¥¿•¡¿Ñ«ÒÖvbC {T»s4|ÖžwÝ=é?IEND®B`‚viewvc-1.1.22/templates-contrib/newvc/templates/docroot/images/down.png0000644000175000017500000000024711015337675026464 0ustar cmpilatocmpilato‰PNG  IHDR H%v?PLTEâÇÃtRNS@æØfbKGDˆH pHYs  ÒÝ~ütIMEÔ #èe"IDATxÚc`Àöä0ð70°300AÅ+q—‡Ž™IEND®B`‚viewvc-1.1.22/templates-contrib/newvc/templates/docroot/images/viewvc-logo.png0000644000175000017500000002027011020011564027732 0ustar cmpilatocmpilato‰PNG  IHDRðF}¤Ž­sRGB®ÎébKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEØ:;÷ƒAx IDATxÚí}iPWÞ÷éåö]Y.—MÙ‘MÙ— A‘hPg¢ITR)5±R™™ššÑç}3©w¦f>d’ÉÄ!õš§ÔñIâ$&NF[EâAd‘Md¹÷r¹{÷ûáè±íÛÝ4j2Ô[ýÿ`á½§OŸåwþç÷_ιÃ0@Yþ\Yd@Ë"Ë ò'x‡«Á0LžYž®`?‡fÆê4𭣓#͸”¤ÎW¤Vø°qŒþ–Á-ËL4ÒÍ ÃŒYîš:zÇšîÛµJ}ˆÏÜßø`ïX’P`ÑŒþE–™ h¶Ð4í¦]½cÍ7ªû7”¤:Ò”ìƒã8Æybd™¹€Ð4 dÜ:Ðдg¬௠O [aH#‚lyzd™Y€æ(i6 Ýn7MÓÃ{÷™lƒ?uhnÔË!¾qA`†ã¸Ì­e™q€ÒÓîâr;›î¿9\ cÈÉZ£VêpGÚZž'Yf ÙÖ!‚õC@»\.—«Ïxírÿ?]´ SúÄl ö‰&jkÓ²Ì,@ójk¨§].—Óét¹\#–Þs=»ín ÇÈÜð5sƒ$‰ˆµ<[²Ì8@sˆ5ÓN§st¢¿¶w·Ým†mK Y–¶T¡PȘ–eFš k¨ª! ǰùvMÏN7ã„e’‚Š2ÃWP%cZ)òŸÌå€äe…BAQEQþºðôàÕÜîõÁÊëwÎ8è‘se™¹€F˜ÆqaZ©TÎÑÏò}•ù¡ï@×½§Ó)cZ–™è‡íÀqÇI’„°žTêEÞg&€¾Ðýå¨yÀétB@˘–eڀ憈*?-£Öÿ€ªbZI©æ,CåîÉڮϻËå’•´,"Bò¢ÍM»lv Ûy<5sÀ 5¥•’=Ç0ŒÝ9ér9=ë‡Èv¹\N·ÃG9Ë Œžlƒ_LÞ¾q÷lòìEÐ4„ à'3^Åû.Ãhæša§ËQqøÍÁñ®iUj˜úÌÂùk!yàiˆ×Þᛕ ŸÞì»4­úUJUõŸa¾)z< ÷@YhóaG|dXÏPÊ9€’ ¦[Ñ€±ëLÓÃcý.— Zo¼´„¦éʆÏ[û¯L·~»ÃÎWóÝ“È:üitó{ï½G$Byë­·`geZ?C54œ#¢feöŒ´†è£6¿PIºçÓªw;jZþµ4ý …BA’ä¤sÜê0HBé­ À1‚a˜;£­}1ÿ¯Ÿïõ÷ž-¥æs-ž¯ B£Öt_Nœ\L’AÐ-ý£ªÆ)­"IRÎÏh ÈŠ~¡îæþ;c=Ã7âCs¤TT”Z¾c ¡±ûTxPâˆýÖ]s›Ýey¸`d€.bŽ>¥¹ó˜ÔÈç%¢ÙM»Î4} ðññp3®Ö¡³™ê•$IBgߪžáÆ"Ø6·Ûn·CŒéíåPS^óç,T5~!±¢ØŒ°€x§ÛVucG÷ØU6š4ã4·ŸïØ×ÒW‡láüµ«­ï¨Ÿ$IÂËK?é6Ö³Ý?Ò^س8 ÙüGf3ÐÈÒÂq<'¦ŒÀÉ®»×º›$Öaj2›išjF#`˜ø°ÜY~ÑÒPEŸ¹¶àíýð$¢ƒž¸c¼ùøï¦\-0E¢/ˆáO} H^Ë m@Âìçš{OŸnübCñûRêJ_ä>8Þm6›|||<§Û<1X”"U=7w×÷(RÏPzMa~Én·&—Âf?žsMè)ôùâÅ‹µZ­Ãá€Ê~‡(>>^Ü;)ôjüDÄc#Ò)éå§[³DyŒjÅ‘8Pↇ†Ü”$ɜ貖¾ê›½‡b$ô/œÿê×ß¿g2™¼½½9o5™L ÃD§DÍ“8d§¿Ä8ˆ7ã@Ÿß›ì„YÔIc†6}^çZ¨ì&¡Ââ:`þüùqqqV«Õn·»\.4²A(•Jö™1^g____CCCGGÇðð°Éd²Ûí*•ÊÛÛ;(((666%%%00s˜þ+Ò22’››ËËàýýýkkkQwЫ.\ØÓÓÃ.¹|ùò¿üå/œŽ;ŽÔÔTÞŽ+ŠK—.i4š5kÖ444µùw¿û]yy9J~„Õ2 SVVvýúu¡§víÚUPPÀióµk×vïÞ}äÈ‘îînñš3gÎÒ¥KËËË333á«yÝ\¤ÐÖ³…ž‰YÕ6pñZ×™{¤ø%pœ(œ÷òÁóF£Ñ[ç…<~&³™¦éÙ†ØøÐìi©ç¿\­ÊGéöf0bé6x͆^¥R™ŸŸÿÉ'ŸUµÿþôôt’$!RáèWVVÞ¼ySè‘ÒÒRƒÁàv»F£ÐXŽŽr”a˜þþþ-[¶|óÍ7=å------|ðÁ믿þ§?ýI¯×ÃÙòõõõóó»|ù²ç#]]]hƒb¦©©éìÙ³ž+ùøƒZ­†%a /_¾,Ôñüü|ÇNgIIÉÁƒ…Ú¼wïÞ—_~™$I8¤°Ú¾¾¾cÇŽ íuQQQYYYpS…#ÖÙÙ¹eË–C‡IÄCOOÏŽ;vìØQTTôÁ$&&2 ã‰i\hC¬#ÄJ?0ΤHöÜtj½Ëéš°XOŒÉdœ–zîºÞ>pLj¤Y‹ …Wr ˜íÃ(ùŽa˜Í›7³—>GŽ=j·ÛÙ¹M4M‹L`Æ ‡šžRüzð着ªôôô}ûöM7îãr¹víÚ•™™Y__ƒ5.—kÉ’%BåëêêPóà«ÿõ¯y?yò~( uÜ÷HŠ‹‹¡µPVVf0„ŠÕÔÔ qª=xð s[¿~=<—gí›o¾IOO—Žf¶TVVæååíÝ»—Ý€)ÜvlLçÆ¬\¹uÂ8yOÊû„2?éçЧßc6›Ün:À'l~Äs Ý…‘~™>…BA‘*N ç=v 222RdúÛÛÛ¯_¿ŽF¦‹;vL„ddd ( ¸—µ¡ªªjùòå÷îÝ+}}}K—.½zõ*œªÅ‹ •¼páʇràÀÞ’@ЇRSS#Tm~~>|5EQ¯½öšP1§ÓyâÄ  >,T^£Ñ¬Zµ ÿ—_~¹nÝ:Š8¥X­Ö7~ûí·ž˜4dÒáɳõsÝ´ólÓ>‰ïËKX¡¢´‡Ã:9 c2š…ó_Å0IÙªc7zÎc›R ³I=µ¯ÕeBátøÉo¼!RçÑ£G‘Ϙ¦éÚÚÚááau‚j–èW_}Uˆ‘Cªž•••‘‘$Tl|||ݺuccc.—+))I¨$=‚éÍ›7[ZZxKž8qÂb± Åét:ëêêxKÆÄÄ„††"«÷õ×_W(Bíüî»ïØã966&²NV®\©Óéà677¿õÖ[Ð_ô$âv»7oÞ|ûöm¦q!o ;™3'fàbë‹Í(åejJ——°*i³yÂåvûj3bŠ$³ç/ÀÌѧøéB(ŠR(lÇýþ0„9ØÔüüü¤¤$¡:Ož<ÉN2Q'¾¾¾¥¥¥èñ¦"µ÷ûßÿ~dd„· EQo¿ývUUÕ¡C‡vïÞýÙgŸ:ujß¾}¼å;;;?úè#§Ó í<~‡fs³ÍfCoß¿¿P Fã™3g ôÝnw}}=´Î=¥   &Žã!!!eeeBÕVWW[­V´¢ ¾ZÉk×®E6ÜÖ­[­V«ÈR5oÞ¼ÂÂÂÔÔT•J%RÒd2½ÿþûœÜžû€6Ûî‰(é¸L]„Ýi­mÙ/”ùI/)¥ÍfÌ[Cà )Ž˜ï4v›?»¥9i» íhQ¢vnÚ´I¨ÚÆÆÆtw‘#G„J¾ôÒK*• ž6G~nñÀÈÈÈW_}%47n P©Tê’––öé§Ÿ¦¥¥ñ>µsçÎññqš¦-ZÄ[Àf³577£õ)NF> ½œâ|cáÂ…Ä£òæ›o 6›Í555p0ÅÄsÏ= ý›×¯_¯®®ñR¯_¿¾²²òÿøGEEÅž={Μ9³fÍ‘®íß¿ßd2±ÃÆ÷í¦]“v#‡_³G寮œk9hwZ¥àÒK­ÏŠ[ ã)Z•OÎÜeWBõµ¯iÚâèÑL„Õ5Îx$†½jÕ*½^/¤J‘’nhh¸}û6¿ P¼òÊ+ä14Ù˜>~ü¸Ù(..~æ™g(Š‚hÖ<µZíååõöÛoó>e±XŽ9BÓt~~>Eñg>Ö××Cù„—ÏÕÕÕ ÿFyy9z/ASž‹¹d¡·W¨Àùóç…0$.Q$¹hÑ"!@CéÉ7à³?9vìXIII[[d€ž²hÑ"¤›Q †$ÉÒÒÒððp^Oüàà`}}}NNŽ{ùå—ÙÚAhcäææªT*¥R©V«•J%tWCTBB‡~ˆŒ`6Ï„§ªÙáÃû[*‰+‡'zÐ ?Ï I’)…:•)Q)R8ÿ•K>TQ:I¾»ùüÍo)³Šªpœhãᘆsq:\x³gÏ~ñÅyëw8gΜéèèhnnæ-™™™œœŒ&@â5 ØL&𴥿¿‚²¸¸X(Ð0<<ÜÝÝÍÙp÷ä•••‹EˆoPõì³Ï"bÀ>ŠOQTyy¹ë°Ùl'Nœ2¯ËÊÊvÀ0LÄ P(JQ©T*•JÃ'úhµÜ׿Ȳ1ÛG.'ʼn­ü(J•ù ºé+šv?õ)¬m9`wZõêÙsüSØ€0ñZ«0pBD(º¹yóf_Çwß}'ôíÚµkÙêYJŠ3(!šû$b³Ù ! ç-ÓÐÐpôèQŽJMMõ´Ùl®®®>wîo=999^^^ì[רˆòòrF#Ä:êêê`Ä”×¼Öétl -b“¸\.t21öål“Z­VC óú¡÷›òékáMi‡MQ(é‘Å*…׈ :"ž¦ ʼ"JAAöŒã¸Ý59`â‰Ój?ÎéH©ˆ˜2ÕÕÕB€^´hPN¦„¸x¦>q:°GB¾ŽúúzOú´xñâ9sæ$&&r>?räÈÅ‹…vl¾ÁNç"IÒ`0¹nݺµsçN)æ5ÄœŸŸŸPg[[[‘«Í,üÃétÆÅÅEFFÆÄÄÄÅÅÍ} qqqÑÑÑ0©ž‡±xíœ[Cu¡¾Ižv!bÒj•.-¼ä|û7§¿L^$ñt–¹ÔvÔb3z)ý£²šÄ0¬käŠ'ø(CÿüsÞx¾Žc^CZ?wîÜk×®ñ–‡&Ð7ÏÎ#efïÞ½B#ìv».ôEˆÏÜîÑ«ûûž öHA¬dż@‘jÌ{ZhvÓÎê¦}€äY‹)ÅÚôAë =DbJo*ØóiÄŽJKK#""¤7@£Ñ¬^½ùU¦{‡ï‚ D¢ÙÝÝÝì}í•*•ÊétÚív»Ýîp8ì,±Ùl6› ŽI’ÙÙÙ¾¾¾ž•·µµq@–––LQÔ²eË<÷tÞÆÇLJ„„xÞòúH ".N$/ż†µ=ûì³"΢_þò—ýýýÈ· ÿ­­­Ýºu«È‹²³³ñC£øiÂHBÕ|§ÊóÆ-vß¼4¾óÃN_ûòiú‡ö“F˰Zá”ÇVÏÝ#ׯ¬ý<êYF‚0¥)ã)/¾ø¢^¯çeÏâ°†%333…ÖÃ0[¶l¹}û¶§+úûï¿ÏÉÉÉÊÊÊÎÎÎÎÎÎy ÙÙÙ™™™¿øÅ/P{”Jeaa¡”ŽÁÅ#7}Ä»úüóž|ƒ­È་Y<%55™×ˆ¿á8^VV&DÇ!)((xçw:ƒ©åååEEE"6wDDD||<ÛûA²'fŽoÊÍ¡³ñÞ6—â(éì˜å ·OÜlî¸Ûœú„hfútã^@bðB%¥†±n˜äÙÐÇï_ PÅy>ëX¿~ý‡~8999ep_¿~=[ œL)í‡Sõë_ÿúW¿ú•P({ÅŠyyy™™™³fÍ ÔÖÖŠ8e7n„5CïaQQÑ”þfÇKJJH’„Ù/Ë–-ÉN~è‰*,D|ƒCŠà Àñ,((HJJ’R¡§yªÕëõ›6m @—ùöíÛyù¯lÚ´‰£|é@”!ÛM;ëû#µÏ ^ÀÅê« H }PÕðÅ“«çÆ®ê{¦>%©M~ŽM¹ZkF&{<˘"HÏ4Ç×1¥)㛊Šb«éGòzõj‘t?·Û]SS³mÛ¶­[·nݺuÛ¶mâh.--…† Z¢EEESqOKK Bù"™H“““‘†ö$Zl!žþÅ&µK–,a›×lðüö·¿õ4XO²²²V®\É6"rhøŸ`ïX-å×5z¹g´‰mÌaÒ0Žcx[ÿå¾{­O¤ž©ËÜÀçÔJ-ZÓ“ã½üù þÔ\ŠT‹ÜìÏ6e¤ æªsFZ€†$gûöíYYYO>O™™™ï¿ÿ>;|€ã¸¿¿¿P2’ââb¨k! ãââ’““§äÈ Á;˜l,þìg?Ê,7¯Ù~@N·gÏžèèè'¥èèè¿þõ¯šþÐm‡.!°p©çk³uŒsU›¤¼C₟œn|"& ,’¸2iV!RÏ Cßþ)ç"„‡~mg<5´tS&***??™_ÃÝœõ˜¯¯ïW_}õꫯ>ö01íïÿ»F£Á¨¤ÅŸEz%c,_¾\: …Îê¢I×jµSZ&ȼæàŒ­ ÃÃÿýöÛ’’’džMaaágŸ}ãäìàÑÛ bóT¤×¤ÓXÓù?v‡Íóú äTÊ] ÖÔ]34ÞóØ-» 60O«òA:²±ÿø#ÿ!?E”*xÊØ‡tSfýúõð¢u¶^ºEˆü¦ÐóööþóŸÿ¼oß>Îá¹)E©T–””|ýõ×ï¼óŽV«å4 BÁÓqÁ–ôôtè&C%IrÅŠâà[°`jðì)Ì,I’”••ùùùÁÆ£;¥ØHñwìØ±{÷îéîi)))üq`` ô±Í5 qWʤ Å?ôšè¸pûëÑë %EÓƒÕ,¿¨èÀŒŽ¡+gšö®Éÿ_æ®Á¦®Á&#çÍ*B½5\×ÐÏûÀ¡[À&ñ‚†mÊ<ÿüó}ôÌp÷tÝà8¾jÕ*4úžN@ ÃŠŠŠt:ºÆ€}ê;>>ž7‘———‘‘ÑÓÓsêÔ©K—.µ¶¶Bo§ƒ!444!!!333//ÏÛÛûþUÂtТ….á„„„mÛ¶±³õÙíLNNFk韸¸ŠŠ ³Ùìyúð   ­V;¥ë}n:44tçÎ}}}¼ã ]Úh5r‚Slzÿ[TTTPPÐÑÑQYYyåÊ•[·n p‘$IÇÆÆ¦¥¥DFFÂ%bãœ<ŒíÞw»Ý‡Ãf³žhýë˜í Î??7r5E)ÙÏÐ4ír¹l6[Çk{ëÞ%pÅÿ~éK_]Ðt½ûäÝè½ëŸW0÷5š¿3Ñ|îö†á?¢LK0”ÀÄBµZ myÞi@?G„|ºžW<²ãÐ1Ì^èàÁ‡Ãk°Ûí@COT°ð.%‡Ã:Eçv»GGG-–û7«Õj…B˜\~pª ØóÄššGZÂæÙp% h0†a°1"À×±» „itÍ1ô—{VÈn{<9ׂ×ÂQBÊž¤A¾)­VëëëËæ]pÊИxj"Ò3¨PPé³~ºãÿ2€n»Wã¤íÏF­UR*O‹uN@B˜_Rïèõêæ}+r9-4ßi¿Ù{Ãðä"Øóα‹?ôïB3…ë"½òaO8á{¡-’a¸?â8Ϋ¢Ð¤òVÈÑ($I²žå8¼ÐWpÎà$Ñ4­ÑhQhÐs ¡â9Jž×`œ ôáv`·Û­Vë÷]»†&oÁ¯T¤W^ĺˆ€Ôy¸£Y­Ö^xoÂ:ÚDÇÀÈí[ýW&íf’ª©Cƒ ÀêšÓ'¸¦žÉ‹4ã)ïOÍMò]cÆ0oPºsr›ƒHöâ—ßqj˜òYÎ¥X¼È9Öç”-Þ²åÙÞÇÅá]vߌñ|¼ššy6 hVMÜ­ëûÔâ|äŒ>Žá~©Q†ìYÞñ ؤ^¤• ÃíwzMW,Ízê, ½"*Ñg¹F¥C9ÝòOÊÊ2m@#âÉ´Ýn7ZF.öntðä¤R„&Hã¯Ò«g{QJ’{ÚÊé¶™ìC&Ûà¥}ÈÒnu%¶,Jšë½D¥RÃ$5Oº,²H4¢êÐMh³Ù¬6KãÝÃ}–«âÕ‘8EWàá¢íN·Íá¶N»M€ˆPç‡é2ÑÁv„sZ8Yd@?‚ièFEîôîñú£Ç´íÇk÷‹Ó-Õ«g#Ÿ?äÍ‘9$‹ h~=Ͷü&¬c-#'&¯=õ¦à˜"T•¦ÉRR*v‹Í›e4Ëòø€|±JëÑÉÞöñïïÙÛ`ž ”ƒ¨äÙê -¥‡Fù ì°ŒfYžÐlLCJ ÁÁt…që@¯ù‡!ûM§—…ÁÐR‰Aêyj…ŠÑ#(sÎ;ȳ%ËS4xà}C®È@PZ‰Óåš¼5jësôLº§¾„ìM†ú+c¼Áìßiegœˆ„Oe‘å‰í©ª!¬ù¡í.‹Å92á±¹Ç\´ÝÍ8ià$EàJ£T„Ÿ†ÐkIIP((MØYឩɲÈò”ÍVÕÐ} ð_vlæ‚qÂýœü2„f”h††çù\YdùQÍVÕHa#³oT`'Ä ¤0”\ÂÉœb'OÉP–å§4ÖHa#6›9ÅÉ®d#›“ì&CY–ÿ  Ù$„s«•Ð/Úr~vE¹S,IDATSz–™,²üD€æ ÿÀ-Ó@ÚÏÜÊ"Ë ÐBøÁ´,²PÈ"ÅRĆ4Ô¢B¹}$mn“›{>‡Pµ6úÛø~çã€[°ïåÒÃÉñ©å"ÿÄ&FöÍø«‰Ý{GÎ]Ž8.¶¡Í‹~¥V ž˜¡) #C33ù:}5ýâIþóÿ”Œ¼•IÃÚ£doÔqˆ¦.ůºá×kQ7Ú|±aH#è[yµÔŽÇÚ! ‘Í‚\ÿ™ç<?&¼ù®ö8êfàËS”¦‰1qANÜ–=G†Ùû:–Ò d=C’[.¦ÆÞ?;}ÄysMVr‚?Zv£ì€¨ŒÝQ†eÿ”Íü"^vÙú¬CÓ†Â/kQB”!!aâ¼{1­‘üàìÚÒ±hÿ½H'·ž)4eH¨íOj÷ D\’Xü—µ»ãgë P‚ÛÚq"ìLik€ŠvŽÐn„ùY6©SC_ß^·¥I4jµÊ__a¼G¼OÎìð– Ð°ª€áÁn½ôNHK;z_É\F ÛBBÝV;5ÀþœµDç2¶’ýý­V÷QÅâ©ztw2vRõiË>@,(3ÿXüytÜü ¬EÙ\²ûIEND®B`‚viewvc-1.1.22/templates-contrib/newvc/templates/docroot/images/back_small.png0000644000175000017500000000031511015337675027601 0ustar cmpilatocmpilato‰PNG  IHDRíÝâRPLTEÿÿÿŒŒŒZZZ„„„111!!!8ÐtRNS@æØfgIFgNnzVIDATxÚc`@€4(Ífœa$›i& CÌ66dÀd¸08€FŒ.J ¬ÆÆL¡¡@õLÆÆ®¡¡ EÆÆAF²qRh T[(ØH "'ˆI‚‚@Í&] èP4Œ~IEND®B`‚viewvc-1.1.22/templates-contrib/newvc/templates/docroot/images/broken.png0000644000175000017500000000036711015337675027000 0ustar cmpilatocmpilato‰PNG  IHDRíÝâRPLTEkkkŒŒŒÎÎÎçççÿÿÿ999F‹…/tRNS@æØfbKGDˆH pHYs  šœtIMEÕ  ÷w [IDAT×c`€P P1LCCƒ`Œ(Ã8ÂMKƒ2Ba G #Ð54$Ä`Ht© `L`2TY@ŠXX@ŒPW„v#HÄ'K!©¹°„IEND®B`‚viewvc-1.1.22/templates-contrib/newvc/templates/docroot/help_log.html0000644000175000017500000000571511015337675026226 0ustar cmpilatocmpilato ViewVC Help: Log View

ViewVC Help: Log View

Help

General
Directory View
Log View

Internet

Home
Upgrading
Contributing
License

The log view displays the revision history of the selected source file or directory. For each revision the following information is displayed:

  • The revision number. In Subversion repositories, this is a link to the revision view
  • For files, links to view, download, and annotate the revision. For directories, a link to list directory contents
  • A link to select the revision for diffs (see below)
  • The date and age of the change
  • The author of the modification
  • The CVS branch (usually MAIN, if not on a branch)
  • Possibly a list of CVS tags bound to the revision (if any)
  • The size of the change measured in added and removed lines of code. (CVS only)
  • The size of the file in bytes at the time of the revision (Subversion only)
  • Links to view diffs to the previous revision or possibly to an arbitrary selected revision (if any, see above)
  • If the revision is the result of a copy, the path and revision copied from
  • If the revision precedes a copy or rename, the path at the time of the revision
  • And last but not least, the commit log message which should tell about the reason for the change.

At the bottom of the page you will find a form which allows to request diffs between arbitrary revisions.


ViewVC Users Mailinglist
viewvc-1.1.22/templates-contrib/newvc/templates/docroot/help.css0000644000175000017500000000035611015337675025205 0ustar cmpilatocmpilato/************************************/ /*** ViewVC Help CSS Stylesheet ***/ /************************************/ body { margin: 0.5em; } img { border: none; } table { width: 100%; } td { vertical-align: top; } col.menu { width:12em; } viewvc-1.1.22/templates-contrib/newvc/templates/docroot/styles.css0000644000175000017500000002142711661540647025604 0ustar cmpilatocmpilato/*******************************/ /*** ViewVC CSS Stylesheet ***/ /*******************************/ /*** Standard Tags ***/ html, body { background-color: white; color: black; font-family: sans-serif; font-size: 100%; margin: 5px; } h2 { border-top: 3px solid #d0d775; font-size: 115%; font-weight: bold; } a { text-decoration: none; color: rgb(30%,30%,60%); } img { border: none; } table { width: 100%; margin: 0; border: none; } td, th { vertical-align: top; } th { white-space: nowrap; } table.auto { width: auto; } table.fixed { width: 100%; table-layout: fixed; } table.fixed td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } form { margin: 0; } address { font-style: normal; display: inline; } .inline { display: inline; } /*** Icons ***/ .vc_icon { width: 16px; height: 16px; border: none; padding: 0 1px; } #vc_header { padding: 0 0 10px 0; border-bottom: 10px solid #94bd5e; } #vc_footer { text-align: right; font-size: 85%; padding: 10px 0 0 0; border-top: 10px solid #94bd5e; } #vc_topmatter { float: right; text-align: right; font-size: 85%; } #vc_current_path { color: rgb(50%,50%,50%); padding: 10px 0; font-size: 140%; font-weight: bold; } #vc_current_path a { color: rgb(60%,60%,60%); } #vc_current_path a:hover { background-color: rgb(90%,90%,90%); } #vc_current_path .thisitem { color: #94bd5e; } #vc_current_path .pathdiv { padding: 0 0.1em; } #vc_view_selection_group { background: black; color: white; margin: 0 0 5px 0; padding: 5px; text-align: right; } #vc_view_selection_group a { padding: 5px; font-size: 100%; color: white; text-decoration: none; } #vc_view_selection_group a.vc_view_link_this, #vc_view_selection_group a.vc_view_link:hover { color: #94bd5e; } #vc_view_selection_group a:hover { background-color: black; } #vc_view_main { border-top: 1px solid black; border-bottom: 1px solid black; } #vc_togglables { text-align: right; font-size: 85%; } #vc_main_body { background: white; padding: 5px 0 20px 0; } #vc_view_summary { font-size: 85%; text-align: right; margin-top: 5px; } /*** Table Headers ***/ .vc_header, .vc_header_sort { text-align: left; vertical-align: top; border-bottom: 1px solid black; background-color: rgb(80%,80%,80%); } .vc_header_sort { background-color: rgb(85%,85%,85%); } /*** Table Rows ***/ .vc_row_even { background-color: rgb(95%,95%,95%); } .vc_row_odd { background-color: rgb(90%,90%,90%); } /*** Directory View ***/ #dirlist td, #dirlist th { padding: 0.2em; vertical-align: middle; } #dirlist tr:hover { background-color: white; } /*** Log messages ***/ .vc_log { /* unfortunately, white-space: pre-wrap isn't widely supported ... */ white-space: -moz-pre-wrap; /* Mozilla based browsers */ white-space: -pre-wrap; /* Opera 4 - 6 */ white-space: -o-pre-wrap; /* Opera >= 7 */ white-space: pre-wrap; /* CSS3 */ word-wrap: break-word; /* IE 5.5+ */ } /*** Changed Paths Listing ***/ .vc_changedpaths { margin: 1em 0 1em 2em; } /*** Properties Listing ***/ .vc_properties { margin: 1em 0 1em 2em; } .vc_properties td, .vc_properties th { padding: 0.2em; } /*** File Content Markup Styles ***/ .vc_summary { background-color: #eeeeee; } #vc_file td { border-right-style: solid; border-right-color: #505050; text-decoration: none; font-weight: normal; font-style: normal; padding: 1px 5px; } .vc_file_line_number { border-right-width: 1px; background-color: #eeeeee; color: #505050; text-align: right; } .vc_file_line_author, .vc_file_line_rev { border-right-width: 1px; text-align: right; } .vc_file_line_text { border-right-width: 0px; background-color: white; font-family: monospace; text-align: left; white-space: pre; width: 100%; } .pygments-c { color: #408080; font-style: italic } /* Comment */ .pygments-err { border: 1px solid #FF0000 } /* Error */ .pygments-k { color: #008000; font-weight: bold } /* Keyword */ .pygments-o { color: #666666 } /* Operator */ .pygments-cm { color: #408080; font-style: italic } /* Comment.Multiline */ .pygments-cp { color: #BC7A00 } /* Comment.Preproc */ .pygments-c1 { color: #408080; font-style: italic } /* Comment.Single */ .pygments-cs { color: #408080; font-style: italic } /* Comment.Special */ .pygments-gd { color: #A00000 } /* Generic.Deleted */ .pygments-ge { font-style: italic } /* Generic.Emph */ .pygments-gr { color: #FF0000 } /* Generic.Error */ .pygments-gh { color: #000080; font-weight: bold } /* Generic.Heading */ .pygments-gi { color: #00A000 } /* Generic.Inserted */ .pygments-go { color: #808080 } /* Generic.Output */ .pygments-gp { color: #000080; font-weight: bold } /* Generic.Prompt */ .pygments-gs { font-weight: bold } /* Generic.Strong */ .pygments-gu { color: #800080; font-weight: bold } /* Generic.Subheading */ .pygments-gt { color: #0040D0 } /* Generic.Traceback */ .pygments-kc { color: #008000; font-weight: bold } /* Keyword.Constant */ .pygments-kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ .pygments-kp { color: #008000 } /* Keyword.Pseudo */ .pygments-kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ .pygments-kt { color: #B00040 } /* Keyword.Type */ .pygments-m { color: #666666 } /* Literal.Number */ .pygments-s { color: #BA2121 } /* Literal.String */ .pygments-na { color: #7D9029 } /* Name.Attribute */ .pygments-nb { color: #008000 } /* Name.Builtin */ .pygments-nc { color: #0000FF; font-weight: bold } /* Name.Class */ .pygments-no { color: #880000 } /* Name.Constant */ .pygments-nd { color: #AA22FF } /* Name.Decorator */ .pygments-ni { color: #999999; font-weight: bold } /* Name.Entity */ .pygments-ne { color: #D2413A; font-weight: bold } /* Name.Exception */ .pygments-nf { color: #0000FF } /* Name.Function */ .pygments-nl { color: #A0A000 } /* Name.Label */ .pygments-nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ .pygments-nt { color: #008000; font-weight: bold } /* Name.Tag */ .pygments-nv { color: #19177C } /* Name.Variable */ .pygments-ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ .pygments-w { color: #bbbbbb } /* Text.Whitespace */ .pygments-mf { color: #666666 } /* Literal.Number.Float */ .pygments-mh { color: #666666 } /* Literal.Number.Hex */ .pygments-mi { color: #666666 } /* Literal.Number.Integer */ .pygments-mo { color: #666666 } /* Literal.Number.Oct */ .pygments-sb { color: #BA2121 } /* Literal.String.Backtick */ .pygments-sc { color: #BA2121 } /* Literal.String.Char */ .pygments-sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ .pygments-s2 { color: #BA2121 } /* Literal.String.Double */ .pygments-se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ .pygments-sh { color: #BA2121 } /* Literal.String.Heredoc */ .pygments-si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ .pygments-sx { color: #008000 } /* Literal.String.Other */ .pygments-sr { color: #BB6688 } /* Literal.String.Regex */ .pygments-s1 { color: #BA2121 } /* Literal.String.Single */ .pygments-ss { color: #19177C } /* Literal.String.Symbol */ .pygments-bp { color: #008000 } /* Name.Builtin.Pseudo */ .pygments-vc { color: #19177C } /* Name.Variable.Class */ .pygments-vg { color: #19177C } /* Name.Variable.Global */ .pygments-vi { color: #19177C } /* Name.Variable.Instance */ .pygments-il { color: #666666 } /* Literal.Number.Integer.Long */ /*** Diff Styles ***/ .vc_diff_plusminus { width: 1em; } .vc_diff_remove, .vc_diff_add, .vc_diff_changes1, .vc_diff_changes2 { font-family: monospace; white-space: pre; } .vc_diff_remove { background: rgb(100%,60%,60%); } .vc_diff_add { background: rgb(60%,100%,60%); } .vc_diff_changes1 { background: rgb(100%,100%,70%); color: rgb(50%,50%,50%); text-decoration: line-through; } .vc_diff_changes2 { background: rgb(100%,100%,0%); } .vc_diff_nochange, .vc_diff_binary, .vc_diff_error { font-family: sans-serif; font-size: smaller; } /*** Intraline Diff Styles ***/ .vc_idiff_add { background-color: #aaffaa; } .vc_idiff_change { background-color:#ffff77; } .vc_idiff_remove { background-color:#ffaaaa; } .vc_idiff_empty { background-color:#e0e0e0; } table.vc_idiff col.content { width: 50%; } table.vc_idiff tbody { font-family: monospace; /* unfortunately, white-space: pre-wrap isn't widely supported ... */ white-space: -moz-pre-wrap; /* Mozilla based browsers */ white-space: -pre-wrap; /* Opera 4 - 6 */ white-space: -o-pre-wrap; /* Opera >= 7 */ white-space: pre-wrap; /* CSS3 */ word-wrap: break-word; /* IE 5.5+ */ } table.vc_idiff tbody th { background-color:#e0e0e0; text-align:right; } /*** Query Form ***/ .vc_query_form { } /*** Warning! ***/ .vc_warning { border-width: 1px 2px 2px 2px; border-color: black; border-style: solid; background-color: red; color: white; padding: 0.5em; } viewvc-1.1.22/templates-contrib/newvc/templates/file.ezt0000644000175000017500000000314612242412255023524 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Annotation of:[end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "annotate"] [include "include/fileview.ezt"]
[define last_rev]0[end] [define rowclass]vc_row_odd[end] [if-any lines]
[is annotation "annotated"] [end] [for lines] [is lines.rev last_rev] [else] [is rowclass "vc_row_even"] [define rowclass]vc_row_odd[end] [else] [define rowclass]vc_row_even[end] [end] [end] [is annotation "annotated"] [end] [define last_rev][lines.rev][end] [end]
LineUser RevFile contents
[lines.line_number][is lines.rev last_rev] [else][lines.author][end] [is lines.rev last_rev] [else][if-any lines.diff_href][end][lines.rev][if-any lines.diff_href][end][end][lines.text]
[else] [if-any image_src_href]
[end] [end] [include "include/props.ezt"]
[include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/newvc/templates/error.ezt0000644000175000017500000000357611015337675023756 0ustar cmpilatocmpilato ViewVC Exception

An Exception Has Occurred

[if-any msg]

[msg]

[end] [if-any status]

HTTP Response Status

[status]


[end] [if-any msg][else]

Python Traceback

[stacktrace]

[end] [# Here follows a bunch of space characters, present to ensure that our error message is larger than 512 bytes so that IE's "Friendly Error Message" won't show. For more information, see http://oreillynet.com/onjava/blog/2002/09/internet_explorer_subverts_err.html] viewvc-1.1.22/templates-contrib/newvc/templates/include/0000755000175000017500000000000012265242273023506 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/newvc/templates/include/pathrev_form.ezt0000644000175000017500000000324711015337675026737 0ustar cmpilatocmpilato
[for pathrev_hidden_values][end] [is roottype "cvs"] [define pathrev_selected][pathrev][end] [else] [end]
[if-any pathrev]
[for pathrev_clear_hidden_values][end] [if-any lastrev] [is pathrev lastrev][else][end] (Current path doesn't exist after revision [lastrev]) [else] [end]
[end] viewvc-1.1.22/templates-contrib/newvc/templates/include/footer.ezt0000644000175000017500000000070011015337675025530 0ustar cmpilatocmpilato viewvc-1.1.22/templates-contrib/newvc/templates/include/props.ezt0000644000175000017500000000112311441730461025366 0ustar cmpilatocmpilato[if-any properties]

Properties

[for properties] [if-any properties.undisplayable] [else] [end] [end]
Name Value
[properties.name]Property value is undisplayable.[properties.value]
[end] viewvc-1.1.22/templates-contrib/newvc/templates/include/fileview.ezt0000644000175000017500000000304611020011564026030 0ustar cmpilatocmpilato [if-any orig_path] [end] [if-any branches] [end] [if-any tags] [end] [if-any branch_points] [end] [is roottype "cvs"][if-any changed] [end][end] [is roottype "svn"][if-any size] [end][end] [if-any lockinfo] [end] [is state "dead"] [end] [if-any annotation] [is annotation "binary"] [end] [is annotation "error"] [end] [end] [if-any log] [end]
Revision: [if-any revision_href][rev][else][rev][end] [if-any vendor_branch] (vendor branch)[end]
Committed: [if-any date][date] [end][if-any ago]([ago] ago) [end][if-any author]by [author][end]
Original Path: [orig_path]
Branch: [branches]
CVS Tags: [tags]
Branch point for: [branch_points]
Changes since [prev]: [changed] lines
File size: [size] byte(s)
Lock status: [lockinfo]
State: FILE REMOVED
Unable to calculate annotation data on binary file contents.
Error occurred while calculating annotation data.
Log Message:
[log]
viewvc-1.1.22/templates-contrib/newvc/templates/include/header.ezt0000644000175000017500000000541111305230101025440 0ustar cmpilatocmpilato [[]ViewVC] [page_title] [if-any rootname][rootname][if-any where]/[where][end][end] [if-any rss_href] [end]
[if-any username]Logged in as: [username] |[end] ViewVC Help
[is pathtype "dir"] View Directory [if-any log_href] | Revision Log [end] [if-any queryform_href] | Commit Query [end] [if-any tarball_href] | Download Tarball [end] [end] [is pathtype "file"] [if-any view_href] View File | [end] Revision Log [if-any annotate_href] | Show Annotations [end] [if-any graph_href] | Revision Graph [end] [if-any download_href] | Download File [end] [end] [if-any revision_href] | View Changeset [end] [if-any roots_href] | Root Listing [end]
[if-any roots_href]root[end][if-any nav_path]/[for nav_path][if-any nav_path.href][end][if-index nav_path last][end][nav_path.name][if-index nav_path last][end][if-any nav_path.href][end][if-index nav_path last][else]/[end][end][end]
viewvc-1.1.22/templates-contrib/newvc/templates/include/diff_form.ezt0000644000175000017500000000460011015337675026170 0ustar cmpilatocmpilato

This form allows you to request diffs between any two revisions of this file. For each of the two "sides" of the diff, [if-any tags] select a symbolic revision name using the selection box, or choose 'Use Text Field' and enter a numeric revision. [else] enter a numeric revision. [end]

  [for diff_select_hidden_values][end] Diffs between [if-any tags] [else] [end] and [if-any tags] [else] [end]
  Type of Diff should be a
viewvc-1.1.22/templates-contrib/newvc/templates/rss.ezt0000644000175000017500000000135211015337675023422 0ustar cmpilatocmpilato [rss_link_href] [rootname] checkins[if-any where] (in [where])[end] [is roottype "svn"]Subversion[else]CVS[end] commits to the[if-any where] [where] directory of the[end] [rootname] repository [for commits] [if-any commits.rev][commits.rev]: [end][[commits.author]] [commits.short_log] [if-any commits.rss_url][commits.rss_url][end] [commits.author] [if-any commits.rss_date][commits.rss_date][else](unknown date)[end] <pre>[format "xml"][commits.log][end]</pre> [end] viewvc-1.1.22/templates-contrib/newvc/templates/query_results.ezt0000644000175000017500000000706711661540647025554 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Query results in:[end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt"]

[english_query]

[# ] [if-any row_limit_reached]

WARNING: These query results have been artificially limited by an administrative threshold value and do not represent the entirety of the data set which matches the query. Consider modifying your query to be more specific, using your version control tool's query capabilities, or asking your administrator to raise the database response size threshold.

[end]

Modify query

Show commands which could be used to back out these changes

+[plus_count]/-[minus_count] lines changed.

[if-any commits] [if-any show_branch] [end] [# uncommment, if you want a separate Description column: (also see below) ] [for commits] [for commits.files] [if-any show_branch] [end] [end] [if-any commits.limited_files] [end] [end]
Revision FileBranch+/- Date AuthorDescription
[define rev_href][if-any commits.files.prefer_markup][commits.files.view_href][else][if-any commits.files.download_href][commits.files.download_href][end][end][end] [if-any commits.files.rev][if-any rev_href][end][commits.files.rev][if-any rev_href][end][else] [end] [commits.files.dir]/ [commits.files.file] [if-any commits.files.branch][commits.files.branch][else] [end] [# only show a diff link for changes ] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [commits.files.plus]/[commits.files.minus] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [if-any commits.files.date][commits.files.date][else] [end] [if-any commits.files.author][commits.files.author][else] [end]
  Only first [commits.num_files] files shown. Show all files or adjust limit.
  Log:
[commits.log]
[end] [include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/newvc/templates/roots.ezt0000644000175000017500000000221511652601632023752 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Repository Listing[end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "directory"]
[is cfg.options.show_roots_lastmod "1"] [end] [if-any roots] [for roots] [is cfg.options.show_roots_lastmod "1"] [end] [end] [end]
NameLast Change
[roots.name][if-any roots.rev][if-any roots.log_href][roots.rev][else][roots.rev][end] ([roots.ago] ago) by [roots.author]: [roots.log][end]
[include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/newvc/templates/graph.ezt0000644000175000017500000000050611015337675023714 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Graph of:[end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "graph"]
[imagemap] Revisions of [where]
[include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/newvc/templates/query_form.ezt0000644000175000017500000001667511015337675025021 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Query on:[end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "query"]
[for query_hidden_values][end] [is roottype "cvs"] [# For subversion, the branch field is not used ] [end]
Branch:
Subdirectory: (You can list multiple directories separated by commas.)
File:
Who:
Comment:
Sort By:
Date:
hours
and
(use the form yyyy-mm-dd hh:mm:ss)
Limit: Show at most changed files per commit. (Use 0 to show all files.)
[include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/newvc/templates/log.ezt0000644000175000017500000001670511204262033023365 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Log of:[end] [define help_href][docroot]/help_log.html[end] [# end] [include "include/header.ezt" "log"] [if-any default_branch] [end] [is pathtype "file"] [if-any view_href] [end] [if-any tag_view_href] [end] [end] [is picklist_len "0"][else][is picklist_len "1"][else] [end][end]
Default branch: [for default_branch][default_branch.name][if-index default_branch last][else], [end] [end]
Links to HEAD: (view) [if-any download_href](download)[end] [if-any download_text_href](as text)[end] [if-any annotate_href](annotate)[end]
Links to [pathrev]: (view) [if-any tag_download_href](download)[end] [if-any tag_download_text_href](as text)[end] [if-any tag_annotate_href](annotate)[end]
Sticky [is roottype "cvs"]Tag[else]Revision[end]: [include "include/pathrev_form.ezt"]
Jump to page:
[for log_paging_hidden_values][end]
Sort logs by:
[for logsort_hidden_values][end]
[define first_revision][end] [define last_revision][end] [for entries] [if-index entries first][define first_revision][entries.rev][end][end] [if-index entries last] [define last_revision][entries.rev][end]
[else]
[end] [is entries.state "dead"] Revision [entries.rev] [else] [for entries.tag_names] [end] [for entries.branch_names] [end] Revision [is roottype "svn"][entries.rev][else][entries.rev][end] - [if-any entries.view_href] [is pathtype "file"] (view) [else] Directory Listing [end] [end] [if-any entries.download_href](download)[end] [if-any entries.download_text_href](as text)[end] [if-any entries.annotate_href](annotate)[end] [is pathtype "file"] [# if you don't want to allow select for diffs then remove this section] [is entries.rev rev_selected] - [[]selected] [else] - [[]select for diffs] [end] [end] [end] [if-any entries.vendor_branch] (vendor branch) [end]
[is roottype "svn"] [if-index entries last]Added[else]Modified[end] [end] [if-any entries.date][entries.date][else](unknown date)[end] [if-any entries.ago]([entries.ago] ago)[end] by [if-any entries.author][entries.author][else](unknown author)[end] [if-any entries.orig_path]
Original Path: [entries.orig_path] [end] [if-any entries.branches]
Branch: [for entries.branches] [entries.branches.name][if-index entries.branches last][else],[end] [end] [end] [if-any entries.tags]
CVS Tags: [for entries.tags] [entries.tags.name][if-index entries.tags last][else],[end] [end] [end] [if-any entries.branch_points]
Branch point for: [for entries.branch_points] [entries.branch_points.name][if-index entries.branch_points last][else],[end] [end] [end] [if-any entries.prev] [if-any entries.changed] [is roottype "cvs"]
Changes since [entries.prev]: [entries.changed] lines [end] [end] [end] [if-any entries.lockinfo]
Lock status: [entries.lockinfo] [end] [is roottype "svn"] [if-any entries.size]
File length: [entries.size] byte(s) [end] [if-any entries.copy_path]
Copied from: [entries.copy_path] revision [entries.copy_rev] [end] [end] [is entries.state "dead"]
FILE REMOVED [else] [is pathtype "file"] [if-any entries.prev]
Diff to previous [entries.prev] [if-any human_readable] [else] (colored) [end] [end] [is roottype "cvs"] [if-any entries.branch_point] , to branch point [entries.branch_point] [if-any human_readable] [else] (colored) [end] [end] [if-any entries.next_main] , to next main [entries.next_main] [if-any human_readable] [else] (colored) [end] [end] [end] [if-any entries.diff_to_sel_href] [if-any entries.prev], [else]
Diff[end] to selected [rev_selected] [if-any human_readable] [else] (colored) [end] [end] [end] [end]
[entries.log]
[end]
[is pathtype "file"] [include "include/diff_form.ezt"] [end] [include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/viewsvn/0000755000175000017500000000000012265242273020444 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/viewsvn/README0000644000175000017500000000161210615125156021321 0ustar cmpilatocmpilatoI. INTRODUCTION ViewVC is a Subversion and CVS webbased repository viewer. Templates-viewsvn is a set of templates for a different look & feel. Warning: Do not use this template set if you plan to view cvs repositories through ViewVC! This template-set is heavily trimmed for Subversion needs. Special CVS features are untested, possibly disabled or broken. Please report enhancemants back to the ViewVC project or to me. jpeters7677@gmx.de II. COMPATABILITY This template is compatible with ViewVC 1.0.x III. CREDITS Stylesheet color codes: Subversion Mailing List Archive http://svn.haxx.se/ Images: All images in templates/docroot/images/tortoisesvn are licensed under the ToirtoiseSVN Icon license. See license.txt in this folder for more details. IV. LINKS ViewVC: http://www.viewvc.org/ Subversion: http://subversion.tigris.org/ TortoiseSVN: http://tortoisesvn.net/ viewvc-1.1.22/templates-contrib/viewsvn/screenshots/0000755000175000017500000000000012265242273023004 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/viewsvn/screenshots/log.png0000644000175000017500000021234710615044233024275 0ustar cmpilatocmpilato‰PNG  IHDRRXhÊ×ÕsRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.05Ãà\ÿjIDATx^ìý ˜Ezç ×}¾ûø~wîôôLíûÚÓ=v·ÇÓ3¶ÛÛ½€Y,ÀÕm÷Øî}£išnØ«išE¬±KBˆ­ ±¤’ÚJû¾«J¥½´K¥¥´A¿7ÞȈ̈ÈÈí,UçTýÅÔ9™‘‘¿x3OüóxóÿX³f 58ÿ{ìuw¾ƒ€€€€€@¿$pËà«þ÷ÌYHé4pà`â4ôÞg豑¯Ñ¸—[‘À6€ À`°Øl6è—6Àšˆµ‘ÖI£F=C:5˜"Š3®Y³>úè#™ð@@@@@ú+­‹X#±VbA¥ÿI!ÅxGdžR@?v‚ì¦;ÊmH`€ À`°Øl6€ ô'`-Äšˆµk$>wSLI!uï½O‡"j÷®ý´zõvZºt͘± `°Øl6€ À`°~i¬‰X±FÒbеÿk`o¯…Ú»÷0­]»]zëÍùýr$Ö„aMl6€ À`°Øl6`Úk#ÖH¬•X3ñ>ÖPRHÍž³Fìè¤É-Kè…±³À6€ À`°Øl6ÐÏmàîÇп|¤ìÄõ¸£Úu¿ôåó¨Üä¶›µk&ÖN–Z´¨Æ¿6 `°Øl6€ À`ýÜ´ÐY¾l+>]Z:.ÇåYŒq}ZkT³îáO¼*=ûý¦õúpËÛôáA‘º¦Ð*%~æ|"?—ãò\×gj$ÖL–Z¸¸ƒ¦¾½Š&¼±(5{y]qõÃô_DÿðÏ×ÓÞA·Þv'Ý|í·éÚ_\E¯½:7³Ž¬c`z€øÀ`°Øl6€ TÛXü¬]³KZx¿ìÄõp}ºÍÕ¬›ÅÏÉûo¥w‡ ‘éøÔÑ…“.Ëõp}&kÖL¬BykÒâXZzÃÚ~ÿôêø9ôÃ_Kýåg©íºß£?üÁ3ôß/Gg\ý$}ûÒAttÕ÷‰6_HOÜø}ÿÆM‡[ž¦C“›?s>ÎÏå¸<×Ãõ¹}R-S‘›Ö\òßiߣK«¿ý;ôØïüÿh÷¿OËnø/ô\Gß9Ÿ.}fýtÈ(úúÏ® êü9Ñ–ŸÒø»iÒä…±º|õc[œ9˜€ l6€ À`°Ø@oÛ‹ö¶]´½ó@Ù‰ëáúô9U³n>~}=uÝr#¹ã&:~ßtÁù}™>ñxúA:üøý±Ï:ççr\žëáúܾÈR¯ÞIK¾ÿêzí,Ú÷æÙ´kħ½}ŽÆ}ë?ÑsýëˆitË;ëé’ǦÐç¾}ýÃ%×С‰g1u1=8ô1)0íí ÇÇM6€ À`°Øl H±Ó¾‹¶lÞ'ÓVõW/ò—ë‰ )§îÏ}îo)o2íÖÍÂgßõWÓ¾_^Gn¸žÜxCø]K]ƒoŒ%Þ®ÿq~.Ç幞L!µhI‡>S¦.i Myg -øÞçhý¥L{žü Úóì_ÓÞáBH=ò?©ë3èÅ«þ–κñYúîKkè»/·ÑŸ žL¿÷Ï·ÒÞ¿@Çg7ÒÆ§Ï–u¼­êz[|ž2æVúâ'?Iÿîßÿ{úÄ'Ï¢kÇû‹¤û¿ûIQö"º¿`¹èOÐw²Ú ÚùÅëÞ*Ô¶"ç‘'ïóו‹•dòÝ'z¶­9åo[Ð/=r9Ûž§¬<9ë˜D¶Èׄ¼.¾|+=_²m»– Ÿ_•Û%ï;e%æÜW¢z|ÛJ;Î}êÞsŸl£²WÑ_Ïy¿9FÜÌ{$Ÿ‹}ì"uÛyŸsî)åñ.½8.رZ»W¡=õý[ƒþ+Ö,|Ö¯ßM6ìi¯úËŸõwÞf&½ÏÜlãz¸>ÝnÝ,b®ºò‘\‰Å–Ù«n¡9Xøì¾êJÚÝ$ÄÔµWË¿æ?M¼M'SDq>³×Ãõ±®Ñ‰5k§pÔ’¥¤ˆzgÆ2š&ÓrZ|Ñÿ Î!ÿö<ú?…ˆúSÚsÿgi÷-@Koÿ"ýÞ×®§Ï|”>ß¼’¾2z}梇hõÜAt|õéÀgÐÁñ_u-§iÓ—‡u¾óâmôe1X>ã×SiÑÜ5"­¦Ù"çË›ü ©KhÔÜU4«@9»þ•ôÚ¯ÏõœM7¿æiƒÑÎù­+r·-ï9äÊ÷ðÅr`ýýa«3X=EßL0\𜽒$Ÿ¯ÜFcKf“³/r1²Û–~ÞAÞOüh´<\ŒŒs,tÞ¹Úž“ƒÉ9W½&“æèœåõ0š~Ì >ùSz¸ÚýWƒõó=§¬dðçûЬK]G?z4¸ÎË©ÿ!ëÞ£ìõŒ;iÂ~i¹û½È± Û—v`¦ Ýö±‹ÔmäM¸§”ÃeKì‹2m±ÿq×ãü ÆgH`Ðs6Àb‡Eкu;ÊN\×§ûÏ­»¨2Ûd×½\ Ÿí—_F;®D»¯‚ŠÅÔ5WÅÅ”ØQbçår\žëáúä½WéÖL¬B!µtùš5{Íi–ÈrZtõÔyÇçh¯˜Ò·ÿA!¢îù#úÇ3¿Dÿá~þý/¤ÿò“è·îœMŸþÉýô»ÿt]zÍèôНÒá‰_¢}ãÏPõèúDݯÜN_QBj|èãý;GÄmg6Gµ³hyÎÿò¯"!5Ï­§Bí,¥]ºÌ#? ԣĠj~Úy¿Xæ)ó5Óx ®w³Ëà“Ùö<Œ¬¶6•œTÛYH¶‚ç§í¥°ËS¯¯¿ÌsþS) ,þåØwfÿ•r~U.3SÔ_Vl¥'Gðäû ×Å×9oã‡7|•U¿(Ï¡Nå çZk}/r,Ûö“Ú»HÝQ^÷žR. ”/­À­8·z¼Ÿ¡ÍY¿ùØ_/6¢½FëÖmBª¼¤½FúÜݺYå°§)Ï?Îg¶'ª;ÐRH ü9íøÅ/h× AVr=Sæw7/—çz¸¾@#é´’X;…BjùÊM4wþZš3Hüw-µüü«ÔyËKoÔ¾G>G;îøºæüÿAÿñ/¾NO^ð»tý?ý5ý§¯ßHÿçÅ/Ñøû«è¼o}>ZùUzwÎ9´é‰¿ ëáºær½ãï …ÔÂy|Þv§Øv6ÝxÓ%rÀÃé+7M¥Wo @ú;çq¡žx íœJ7žl ¦Âèíž}gÞI¯Ê2Aâú” Æö¨Mç,l§™G¶9:îGÚõêöè¿æ~}|ó8n>}žzûŒú­òŠÉ'.âcîTúµÁBòÛçÌM?dquá!+{;ç±™Ü8ž¿å~xaÔ/Ì+اû-b¤ÛÌ}§ëãmAÛÖÐ<‡YÐ_F}êx&;{tL›—mÑy'åoÛNJYãÜcý«ÎQÛM&Ý7,¤ôõ02àÍu,˜—Ôwéí3¯»Éõ™6ïç|×§k›Ñwç: ¯EßõÜ{JI¯ÞtNx5ëˆêgqµPnI݇"6Áv]Þ½~Ù¦í{çW,ù¡Å\¾¿¹ß׆eû:G\;Iç¥ÊJ;ˆçI<ö…w9×tüÜ‚ëÑn ·çG#ÓYÈk×l³²K]_)ýƒ2¥Ù5¸ñõ…°Þ²;ëÖí¤U«·–¸®OŸ‹[·RYÇòå3ëæñ ŸÍ?ûí¸üç´}Ðå±äk¾|\žëáúB=£´k§PH­Zµ‰,n³Ò]?HÓ¿øŸhÏ?Bê¿ÑûþˆºoýCÚú«ÏÓÁÛ?OGžþWúâWéß|ãúÿÿÃíô§þçÔ5½‘~³ò|šËŸu‰V©LBg(ÔÂùm4_l›¯¶}⌻èùëh‚˜r'’7Mg­ü.ÏxÎ?CŠ…`jß:Ë9òÇþ&1¸çÁú¨_ó fÈ÷<ýˆÅÙwÓb€*÷ýˆëà}â˜"é²7¿¶–, ¶…É×N½_ï»y-õêö^ø˜(ÿøÏdÛy“Ûz¶‹AÜ‚„cøêÇ)2|žAûcåe{‚ód¡µH°›¿è9ºP{¤ôô ùÏ,$Ãu´`üh%Äã"½o²¬ËD1䢨ÏBíæ3‚6=Ƽ FÔV¶ƒÛ*ûÖi›‘_O] øˆþ›‹ô¦É¶'ÏËËsÞãïlÍ—?Ö}¬ŸÑHÃ&SËfÔ›‹‰¶S£´ ýøQæøœ¿ïÒÎ-ÕUŸÆlA]âÆÙ­ÝÏù®OÇÆ õ]‹þëw-Í÷RÒÈ‹øš?Göûõ¼É6p]x!ßCĵÄv.· »¸Å¹~G©c.®]Óþø\¸`šqïá6ª¾a{'ê}Wõ¹LŽ0~nA]ÖCqïr˜î?¶ÛÚ.Ô¹-z4å>Æík‹ßS&¤³÷ ¾/‹ûм‡ü8x(²pAPô˜ ðuƒ°^±;«×l§e˶”¸®O_ÏnÝZ eË—O×Íšƒë×Bªóç?£í¿¸,–Üé|üÝ—Ëk!5±ç¨ß?Ö5¬B!µzífZ¶b-]±‘–­ä¿詇Ÿ¢‡Îý ê¼üwhï]Ÿ¡ý}– ý =ÞøGtîÿø_ôÿ¯‹èÿ>ëZú·ö¯ô_ÿß?¤U·ý¹ þñòôÃïÿXÖ·LÔÖÙr)~ÏOÛ‹÷Vñ¾eƶEóÛiéÓÕÔ¦5Ažðû:Z²¼•Ÿ ©%jß'>ù3z|¹¨KÔ·Dœ¤Y‡¬“ÃûrÁçI·Ÿ+ñ7¿m“íqÚ¶SíÓånb$Ø÷]¤Öö<Î9Ñx-^"ê2Ú¾xñú îõpÙ'/6ÎÓ(gÖÁõâ“?§Çõñt[¤øÑçµo‘™òü—¯í ƒ¶ÙLŒýª®`?{ñD=ºß΄ï™bðƃ,yÞÜ^·m)}Ư§Ùíñï-Õ_>î‹—¨¶çÖO‹ßòØáÓ?Wv·ŽÒŽ5áÖÀn¼íÐõæebØÎ"Ùê<κ[ŠÉð¼Xh}÷ÖmÉmH·Á1¡­Ú¶ÙjäÝuú™×5f^Ÿ‘}‡vj\ϱkñ©àZ¯ß•êú—NêÞ û£„÷˜'ÄõqÖšðèÏÂk=Ö·+#;_¼4:npýK·Ê‡4ÂÞW:÷]NÛ\컨«98¿Óع™×¨±>Šy˼ Çβ çܬ{Ѝ7vÄXí ú(¸ÏH;-Ü7¥ô'Ê€³ar,°Þ°;Ë–o#þÌLb èÍ£¶s=²>u=»uk”Vï³ò¹u+ ÃBªã¢‹¤Ú~©ðJ‰éyÛ.½T&WDiïo×y8?—ãò\×·”ëæß@5.gí ©µmÂe·v ­–i«ü;wÞrzôߤ9gýêüE ¦ö>ðYÚÓïÑ€/þ-}â¿~‘þó§?KçüÁ§èÔ„ôÑšµ_L·}÷ÏhÜ‹¯©º‚:eÝS‡ÒYZH-Ýìw·¾4RK—‹ýÖ÷9tÇ9Z`´Ó Q~êmÁtùcÎPšÊÛî NkÄüE}Nöß(Ï:Z¾ÊÉãk§bóÌ%Z,ér/ÑO•šp˹ªíªm)íH«gÑâ ìgTPWÐ/v’y.~^ ôtÕ9¸[¯ú3j×Õßn3ÉØ?%èË=S÷©hg¬m™}ÿBÈQ·ÓlÏS?Mæ¾hñØ ¬qÞ©|ß¼'ÑyJÝrÍ‚8ÞÇ£B±ß·HÕ››ÉÚ€±5Ø>çšÈVÙþ¾+ÝlA]cgÝ6CŠáÐ ,órÕÏR˜pÿ¦^Ÿqͺíë÷>š"î=«Ü4ú2CŒ\F;wÍ”™ŸËk!¥õ‘Ö5¬B!Õ¾~+µoØNë7ì9­kÛBcï¹ùŸҌ/üjÿÁ§hÏBL± ißCB‡EXtN§—}›NÍl¤EWýýââ_ÐêuE\—H\¯H³¤s”Z²b­çí3°·½<RËW‹ýÖ÷4ä\=XOk¸¼Hm+Œ)7·R[Xf­ ãäqœ4cÈyáSêX·MFÙ ?­×Ç•~¦bKfÓÝÂcff.yš§ÅÅÛ‘^ÏfóóøyÚç÷§OˆÁž0Ì`Ÿj æn¤6s¦Â|F^c»ÍÄ<¯ ~kÿ´ ßä MM!:ƒÙÇ´ÚæcjmgpŒoÊÝiÜ_ ÊçÊ÷­¡q;T6s‘˜R÷Öí,Äý}<áÖä}‹T½¹™èþ’žÃ‘Ñw¥Û`¼O£~=W®äë2°½À[»Jõ3 )yͦ^Ÿžk-ǵ»~å}ÈIk֌ĵçîŸØãY²Ê–„Géö·øœ‚ï|-wv$ëp˜¨ö~ââgåñ–‹iA[œ{.§mÎù>ý>ŽÁt Á´Í=?§ ±óO8¶¾vUþò˜ÁºO;ò_p#ÛÀg~Q~ÍÓi‡bª×3Þ6ÕÓ/¾¾Â¶¸ý‚IYL±„Sào4¾ °¨² °Ø™·`#͘¹F&-b’Þõ¤óùþr=ò½TªÍIu§Õa¶Á̧ëæqM{Ç)¤Ú/ü‰L~r1± rEoÓÉS¼Ëé:¸>h]#ÿ í ©Ž Ûh˶½NÚCóZÒ?D·üûߦ×ÿßÒÊÿý)Ú|Éïжþ íúYÚuûgdÚ8ðiÒÏþ„Î;û_éÍ×'ÑÆMâå]²¾=*‰Ïó¦sÕ€fÙêÁvwÛ¸+B!µ²]ì·¾/¡¡ô``µo{.½ìõ°H±GaÙêWéR5°žÌul Ê]:N·e͹ÿ|5Xl£µ£íÞ6…ç´78ÎtY.¨gÝ΃—§‚'çgߢûyfê¼ë®Ï/3ÏÓS^pùÄ'/£§­úÜ-ÚLä1#-xD½Q;ÔvÎ+·k¶<€f&ñr³Ö¨/q{¬ÃMé»’l0©¾Ut¿˜¶¦íö?¢ÄÆTZÝZäú ì6aѵ–x-ªºc×ïº=Á}¨@Šl“+ÊŽ¿ŠÎ½]<ÐX¼:E=áõÄ×€ËîÙàúqÿÞ©qç=¼R´e¥}ïQuÊs}<:Fô}¥ÃT‹sÁ”ÛgÛ›ö5;o÷Ø)ùsûÄ'Žgþ6Zî2àëMÃËb ‘¬Î§!oªës|p¯øÄ™÷ÓüýR¤‘·˜Í÷+^<†@Ø@¯Ø‹³ÚhâÄ¥2e )Ï÷—ëáúôõì«;ÉÓåÛn#¬›u‡HZH±ZýßPÛ. uNJ:ïçr\^ ©@#EZ‰µS(¤¶lÞN Ò¡£ÔuðˆøÌé(í…–/[Ecžy‰~ñ½+é¬?o¤?þ½3èÿóWè r{öè®;¦)SfPçöÝA¹CAÙ ©Ï«£ó•ZÑq0Ú¿{­(ܶu‹ôä¬ÛªÊ[ßÒÆ%üã¾…:¹nU6œÊ²dí+^ fL™2¹rxc(¤6îÖí4þºõêztýªMú¸A[×Ѱ‡_•çlWOr4†¼Çàvzë ÚѹÚ8Ïðœ¢6Êý«÷œm^² ²½ŠCVÞ°ÝëU{ãåvwƒNy>n¿ifâ˜ݶ}oO;Rý(Ï/ãx¼,–©ýä±›•a)}“ØoE˜$œstÝø™„ûK²Á$[Ð×U`»ë„[<¼& ]ŸÚnë×}T£î@»œ’BêÕ3¿-…ÐÂoý€~ûGÅ“(Çå¹®/Ô@J ±v …ÔÎ;éôGÑéÓ"}ô1}túcñY$ñ™·ÄÛø¯ØüåmœWÿå2A¾ \°=HAÞþщý´:œ:³‘vSuqÙKÝ´±Šm8ѹў´¾»Ï­'9<Ö±}²ïWw¾ U¶õz´AyÿAØl fz,‚¿Á˜ zΚ_xGŠAå&®‡ëÓýWº-Ãuk1ÅB¨œÄõíVºFé#ÖN¡bwþ€€€€€ؽk—,‚Ì4è†aò;ÿÕŸÝ<îw®‡ëÓÿªY÷¡B1ÅBH§—¿|æ¿ú³¹ß÷™ÛÍõ¹ÿX;…BŠÝl|BH`€ À`°Øl6€ À’m€µ“%¤fÌ\!^¶…°Øl6€ À`°Øl ÉbBê‰'^¦_ýjÀ`°Øl6€ À`° °f²„/ü÷Êté¢:y²;–Nˆm:ùö›Û¼ùNˆ:S’YøYä?¡“qühÿa±_%µ?éQý"ÿÉ ™y½Çtê–yÌ6ù>ytòì?.¶qJ¬ç¸h›LþzŽ‹}2©zä_‘75鼉ùTºnñ÷˜‘¢ºÍ|Á1­|ÇÄ6•Âíâû1•ô>þ«·™åÃm¼_ÖÍùD[¤ËEÛŽŠvêÏ×-BUŠýaâïvÒeÌí¾cºåbߊã„I㨓|í°ÊDåˆíœ¢úø{w˜ôv/ø«öŸu2ʄ۬ýÁqd ËDۺŶ´ä­Ó¬¿[Ô%RV=Ö~ΟVÆ·_oçLÝ݇Äq‚”Yæ°¨7%Ç”Iäq“U·Q‡7oÆþ°Ì!q•Š2Q:$>²ÛpH|SPŽóè¤ë‘Û½Iäåí}z›ø{P”KK²ì!ñ®ÃaÒùˆrQyêdnןãû»D~ûuRõt‰ïaÇêâãùÌò]Ä~NFVy½ßú«ëWeå>ã˜]ⳓö‹ïaù÷Ë$¶ùR—ØÇÉØ§ë·ò›uê2Ö6ã˜jû>ñW'ÝsÛ¾ýb¿N:¯¹­„Ï{E™²Ò>Q^']WÖ6sÆç=bÉi¯(«“·žƒ¢n•̼ág±o¯NA]»Åwºeþ ßn™'Hæþpû±O'W¼è}·NFù¨®è˜fý±zDÙ]ò¥ñAòs—8¶›Âz侨|øY½H•_¦ºk·ÈÃÉ—ÏÚfG•ß)ÊùRtU·>†ú»S”ç[ÖlKª3Ú¯óÙywˆ²œ¬|»Äw'é|V^O>.·C'U·,cä ÷›yÃχDyŒº¼yÍýÉe¶‹²:íØ)Êè¤ë4·e|Þ.öë¤ëÙ¾óؤp›qL³Œï³Ù¦pò¾:Y+±fbíNíƒRBÑ9¶Hƒ‚JTR‘Ób BÊiRRLAH)a!ULXe‰&Ÿ¸‚ Ä„”BR,Œ ¤¤0‚Ò¢)x¤à‘‚GJy±rz£ØK”ò^™Þ-x¤à‘òyŸà‘ÊöTÁ#ˆ5à TàƒGÊðPåðLÁ#¥¦bjŸš–‡©}Ñ>cÚ¦ö©é…˜ÚŸö‡©}r¡oê¦öÙSÿ0µOM ÄÔ¾`Ê•ω‹öæ3ê´¢þ%EøS‘ýÚ†Ó€éM– ¶ÙÞ'_½F¤@ùîGà³¢î©H€mÃDÝÃhï3£ô…‘ˆ Óøü‘þìˆ}œÇµïèÑ.•Žˆ¿œÌvÄ÷¯9%ÏÉÏŒü矺Ç÷TÒk—Ì:u¤>CðDù8:_\YÇñì"ò©¤£ù%FðÓ‘ýŒ2ÚãFÜs"ü‰íRbÔ>åόҗðÙŠÔEúãè|AŠ¢ú…Ÿ­H~ö‡ÑúŒ¨|f]ê³}/ˆÊgFòóGàÓÑûD^uO'í½2#ÿE-ÑOFé‹Gå³¶‰)oûU²"ó¹¢ÉˆäWɨ}{E½œ¬:3¢þEÓôŠi{ž”7jŸ)P׳_Ô§“ŒBGˆ¢½¾¤òZ‘ý|‘ú,QeDë £öÅ#ôyë £øù#ôYQõŒÑ:¥(BŸ/Ò_½/)êGæS)Ê­ƒÚ%Ž©“h{Ö*)o· :y=Mf$? Љ¶§£ìíç"S‚¨ óû³"ôݨ}î1 ~vr2£ êmòo…Ï !󫤣òÙ‘ú¢ úü‹–ˆê6ƒQŸÝH|Ü>µÍZ×dDÿ #ùy¶É}*BŸeÐõÏ £P°XÂNÜ~S8Å„•HaDÔm.OH±@ih + ¢)Z¸T[HµûŽ?€F®Â³Kñç´§…—"/&®¤(Ñ+r„87ë ÍyžhÇuŒ$ÁUm!%„Ÿc›ê\ øÏ;oµ©óo3E” Ÿ®„‹Ž°l/ )n‹cgWµxD%´ÌPé}EH­¦Üë-ø>`ÄšÚRR¤ ³E‘g„”šè]!%C CHI1!ÅáÎ!¤’BCHÅC˜CHÂBÊ™ê—"¤XLiqÔ³BJyœ|ïŽÒ¦j)-†X¨õHURHP©] /³]©ï‰JDQTšrTø~©RìIÒBÉò,™¯~ä‘ZxpÖiïV-y¤ ¤ìðHïž‚G )x¤¢wH±WÊð4yß=åy·”ïQRRðH©0摵VÊu®Å’÷ÝR¦‡*#Ë{¦Î_ ÐCÖ ¼yRxqûÑdýÂÞR§öåôHµ‰¶‡^EöV_Mš|F®¦ãìÉ ½Bƒ¨…EòZ¼ŠYŠöŠrÌõªÉ,ÆØ»5‰®j@¶)±SêÔ>-œ´š<ˆ®šL´¼Uª]²mÂ+b¶—§ò™¬EÔÑp.haA2ˆ®Tžž«ZÖУáç` `Û£ç^×4¢ML·SSùF¨so1§öɺE¾uÝ´îQƒm8õ/>]Ï|á®ýß„©}>!µÖôÜ zŠ!ç5XLâ©{ÆÔ¾µ#ì6®-wj_–âýa{D¬SõÔ>Yv 2ã¥½Æ xyŠŸñr_Lís¦úaj_ÑSû¬—øbj_¾—ïbjŸšÞWçSû|Ê75ÑõN™SMqV¾Ê5µÔB$h¯‘!0ÊRîñåt=>žп­xB¿öNéó—B#O,Ä®ºjPPŽóó´ºJ©XÿÑcNí“/m“E&³à±¦ö½ð‰×EIáÅBF‰˜«&³hRÓ¥Èy+øn =å)ë<´8Sý¬¦ù…í’\E{[‚)}R‰6…BJ¶[ ^Ï$?³8íSH ?þ,×S9k¤x»¨K )qŒ+Å1äš)-¤äv%b¤€áãëªZ[yÌJ¬‘Š ©It%·gR°>JŠ£+'‰céíA$¾µ#‹+ߊ„”_ƒh’ŠÐ7éJñ¹BÊgwrºß[4ˆ§_Ô«E—)¤„ÀÎâŠ×KMbQ*òg…BWk«°FŠ×Ga”SRR2ºBJzÂ,/¯Ò)|a¯¹ k¤äK|±F*X«UÇBÊ·6*É#Åb)O4¿ò…TTÒZ*0¡Þ7u²”`I)7¸DO©P,Þ$éYÒíó®åy„ؘrUàš"ÙS8‹1õ·ì`¹-¤ÌõPZÅD–!¨Ø›dˆ§ãn^WH pÜHR@µ(AłŕÏÌî·"ðq9•´` ÷ž<=.LÊ$óóEdBy‰”øi‘ª`j`èI2D•Œê§…Ô:.{.=ºÎ¨ËRê…º2ÿ0’uKO5/+I)3Ÿ•ÏŽ¾ÇõÊ{Ü.q¼µJüUßyÍ” â “±]zÂäùá¢ËkÏð…‚*ܦê2„Jæû¢XÈ)Q·ÆŒ¶§=OÖ´¿náuRÉ»Ÿ½R,¤•ÊgÖ©>í S…¢ò™QõLq¦£õY‚M‹ÐùÃH{QûR_²ëŸÐQûÌ¿:¢Ÿù×ÚLül–K8V­/ˆ gÕã‰êgEÃóDðÓ^¦ào´ÖJ—3÷GÛâk²‚²:ª_Fä>3*_Ø&'rŸŠâ†LWÑûd?QÆMf$?s½T(ÍòFä<+J^·‡QûŒH|V„?õÏXdzŠt;í}‡Dpx”)¡þ0* /ÏËwÍh~¾)€…Xi[ ó‡ÌöHï‘ J¦rúz’)¸à‘ Þ!!•õÎ(í]Jò$©íðH¢ B B )óSaïT­ ©¬µOIû+/¤x¤œ÷O©ò*ÑÖo=RއªBJÖ”\oz”’Cpaj¦ö%©ð¬—‚‚‚òª€G ©ž 4ÁÇ©E!ešÈšêg«`Vò{¤bÓö ¤ ¤x* ;µBʉÚçL!¬Ä©¾:µB*œ–z«à‘‚GJ­w²"b”ŒÞ‡¨}nä>Dí3£ù!jŸŠ ˆ¨}RüäygTZ$¿ê¯‘ò­‡òló†7÷­kJ 6á¦gxƒt]Yùª°?q=”'8™Çômó´)išÝ Q“µ†É\c¾Ð7È'óêýÞuQÑô½c"ŸNþã{ÖP Ñq_RDz¦Ç÷NÍóMÙ3_èë[e”ÑkŸÌ©¼þI'½ÝZ#¥Ž=]0š:Öi¬›²Ãœç[e›ˆ‡7÷¿°7Êg¯§A!tÈs×e¬:*>딹F*|ŸTðr_NÑt<õ¢\Ywú~³Œžnè«ÇÜf†·^(k¿Ï;•þ\¯ÛJzßT¸ßižê< y­I óšå=ŸýSúÄ; T‰Ãâ¯NV´>-ªT>;¼¹Y.Ú#_ÊÕÇõšyõqÌPæv(tOøsê< sΡÍPçjÿq\šáÑ=ŸˆmœÌz’C¡ÛaÊÍåDÈò0!Ñ»ÄgN¾wK™et>ù×þÜðlùöûBª›ùtXs;Lºq,!ŠX$íÇ“%”Dèô.­• C•›¡ÌË«ÐæÆ{¤Ò¢ýíÇ “j\¦Ø·_쓎\o¶YaÍu(sS©ÏI¡ÑÍò©Ÿ-ÏÓA@ÂMÑZ©=☜Ì3B_x#ê^R^od¿ Ü1#é¨fT>s¿&aFÿ³óŠ€VÐ Ž”ç{¹®û.)UŽËÆÞ3äµ¢òy¢öïeÒ)Šê'‚=ˆ€œÌvèµMǬ¨}F`UÆŒßZ+e—ËDÇôE4ße®±ò½S*Šê§"ú9QýbÑûœPêá }¨|¾—üvËh~:ùÖ=¥¿ÿ)+‚_fÔ>_¨ô„IQúrm÷EðKˆÊE¾‹"ùe½tW·!+Ÿ/êžiÏ.¬µ²¢æ‰6ë:tT?3jŸ™WG ´öë}fô=_?k‘/ŒÆgDõ #ü‘öuÚüÒÖM™ïJa¯/žõO°U´¾.qîaÛ‚è{þ©Û9òžJf`½ÍükŽ¢öÙI0"õ™ÑöÒ¢ò™û|e8pEîä@fyÏ~_$¿,!eî·Å•ˆÎ·ŸSF(tc¿ŽÆ—µ/ŠÐg©p_Þk°Ð÷|ý’¶QútðŸP²£ùEÑþü#Œðç:`„/°„õ^3dºþì½¼—_ä«SôB_;‚Ÿ#úŸŽ®§£÷Y£²Öº¨0Â^5O °XŠÖNù¢å%FõóEð³"üy"øéf>ÐÁ¨o‡ø¬“Žšçä'óé~Q+Ÿª+¬GFÇ3×CŸíý9£ô9áÄe<3Ÿg¿¹öÈŠÜÜæÝoÔï¾ÛÉ-k­sòDøó•ÏŠÐçÛ_Y„„”5žPåRZtAHÉ0ãÞwFy¶'…?‡ Ã¥‚ÊoRAÈóøKz!¤”SáÕ!¤‚0éI¡Î!¤XPyÄ™ÝHÉ¢BŠ‹RBÊ k!!!!¾«*ðL)O5ýSûb/ÆÔ¾àE½¾„©}Ú»…5RÑú(^'¥ÖGa”Z/zÀ<ë§°FŠÂ5TÂû£×9a”ýbßÌuRX#%ÖZ‰õVX#¬“Â){ýÖH‰©Ú[$þbTB¬Ã ÖLa”\ã„5R–wk¤ Nã»ýš¾{y,¨D­›X¹r©+I%Ò¶÷¬Gj硃4zånúÞ„-ôå1è/ŸíéÜqéÒ©ôŒØ×%ÏÇ9„4‚·}íõ-a  ¹n©Ä`ÝGÐÚu«hÍš•´v­øk¥•â;§UrßáÃ]Ö1«l¢kïzuøtïõWÐk® ÑC~I+Èã›A tŒ"Á&Fz†À6€ Àú‹ ,^²*ש^xz+õ—¾ÀyöÞ}gÁ•”4ͯúSûÄ ý‡‹GÒ—fÞF-ÛJ!¥£îñ_3ò^øÙi¾}:ŸYω“BÈèzm"ácFL‹ú7wn+-[¶X<½?PPPéhz¦ØŠ"ì…‘ö Ñ¢›)d,“ÊËÑøtêS¤îœ»ƒÎ|nÝ2w½²á(µl9NS¶£qëÓÈ%ûéö9{èG·Ó9/n¢áKvÑ#‡¤'‹ÿ~íµ-ô…1›,qe ­( „)ÀŒJ”1ŸU«–S{ûZÚºu³J›Äß(mÙ²‘6oÞ ò¬yWСCûB1eGåóDí3=M¾¨|žm,¢^~ÝÝU4|ðõ2 mºŠž»ç×RL-œ3ƒæN›$ÓÂY-""Ÿ•/#‚ßÄÞ}oÀ`°Ø@Ù6À¿)Û;w••¸ô>#H„7BŸV]×±KL%äÄ!/^%bÊÎoFçã},¢øo´~) :¡ƒLØýŒ¨|ÚseFÔ3§ñ…Û£{f]ÜFüëßäµäLñ˳¾)–GE÷3½e,¢¸þ$1UU!µÿè^º`Αî¦oψZ¶.¨y!µhÑ|â4þl1ðß_@LU_HmÜw€ÎÞ§»uÑò®÷è•¶ÃôIJýôäò®0=%>7¯8 ¼Uèñ÷¯o£¯½º‰Öì9@_{e5ÍÚC_xncYBêÈ‘ƒ¡ˆ:t¨‹öíÛC{öì¢]»¶Ë´cÇ6Ú¾}k˜vîÜ®ÄÔòPLUZH±ˆzcä~Óuôȯ®¢i/zêŽ_Óð__M“Ÿ”¶¼@K¦¾Bo>ý0z3=rû¯©ýÞ+hù=WÒª»¯ ew^ !!!€ ÀzÍêAH±˜ò‰µ,ÔÿuÉ~úΔ¯Qã”/Ó?¿þmúú¤óiÀÔ¡oOú2}ý­séë“¿AgN>þeêßÑ?¼ó5ú§–3èÏÞ>GnãdEæƒGªÖuL¯·/MHÍ¿ŒÞ™6›æÌ_J –¬¤­ húŒy´eëÞøt=GJ‹'} WLUÅ#5aûB:cÆÍt™O“vΧ{—ÑÅKFÖ…Z²d¡R,xíÏœ9³hË– 9ÄTõ„ÔîCôe±þiòö“Ôº#Q`Š„ÔÓâ;'SHéÏϮꢻæî¦×ÚS{B*šÎ·Nˆ¡.Ú½{‡N,¤X,™^(ý9V¼ÅTg(¦Ü‰©pšOµS)çÔ>Q“žºŸžz=qË54Ix¡æ½õ­œù–L '¿@„˜sï¯iäƒChöõ?¡i¿ú)µÞpińԋ/½JnzºùùpÛò•o‹õY3zíǺԧ¥(‡'í°Øl ¿ ´µM¢uëÖÒÆMhÓæâó‘&&Þûû²ú‹n ÷><(اß}Ÿ~C»¨K|>%Òî÷ˆvtŠ%âóÑ£§ä6zW¤ŧcÐò?ÿ#©^—&õÕ€$!5sÖZ¹nmÝ×MB$µí8@Dàöm{„˜šSµ ¤îmƒ¾2ý×4´í5zkÇ\ziÛtš·R³·¯¨ù©}ì‘b/˶m›¥0ذ¡]z¦²ƒPTOH]?}+=°ô-Úû.=º˜=Q]*í—âéÙ•i„ØÎi´øüôòF!®VÁ%ÒØu‡¨­D!¥EÔúõmBDSø"ïÓ6%¦¢é|ÌŽ÷ó4?Sh±˜jk[#×L…bªD!Õµoµ<õ½:l5¾V¦a´ä—i…P;ÛÒŽ¶ù´nþÛ´ì×hü÷ÓØ¡·Ðó=Lo^öCjùÕåÔ-P˜ž±0T¹¹Vêhôòܤ5R,¢ÜGŽ—›º»—ðZ²´E¼€v:Äž~Ã`°>hóާÎÛiÑâÅôÄ“OѰáÃÅÚ¢%â7à­Z=ÙÛç=%¤¾<îrzvùÛrÝSôrÝì5RzmS)©ßùÑ{R qê©Cü$ÝE?¸l}ö+oÑïÿÕBú?ÿö-úËŸL¥«Ç¡ÖmGé$K«S{iåŸ4@HÕ—ŽéõÖú„{¢¯6„ݳ€rÓ®®#4Mxª,So ©.~”Ξ¨§7M¦7vÌ¡qÛfÒ‹BH-îZGßYð}é&úÒ´_Ó—fÜäO" … 8Q¥`óæµ Q4‹æ¨4Wx›tbϧŋˆ—!î …OX·nµS ÎM BQ!µC„þû6ÓšCïËé{#wÑ£K‚Äß/ÓõÎûÿò9¹O¤³Äç«§í¤'… )ö?*DÕÈâïJ!¸ÚÄ ýÐ)úÂ3ŧö­^½BŠJQ8ˆÂSûüOíÁ ¬úš ðC²¹ Ò’%‹èÅ—ÇÑo¾J¯¾ö=7ö1nXH‹– AåyÖSBêOžù>ýï7o¤ó_¿Ž^[ÓªÄT•…Ô%«„ê¦÷hŽð6]д”þãÿšF[…“Š=Süÿ.é úƒ/=Hÿý Mâ÷SlZjÍ¿ÿ¿ ¤z]šÔW|Bjb‹°7á‰2Ô’M{hNûNšß±‹VmÛ/v¬ ŽbšX%So )*qnëíôÕÙCè•ÎY4~{«QZH-:°Ž6ÙNÛŽïù4AxªÞpoûŽH!#êáÇOŠhsA2"ü)¡e…)7£z¢öq^ö6gž–Æé°‘ô6þËžÙ½déBZ¸hžxÂ4_ ¬yófË ù×Må £nFͳÎIŒÛ[·Ñ!ž&vž¤¡‹öÑ* A&¾#˜_ðB=7wµ\ÓÅiŒø|öØõô7¶ÒÂC5lå!zDˆªEà‰ÇET¿E]BH=Õî¬‘Š¢òœ8™m:&¢._¾D Ì@Dm•ˆÔ»ïžI‹)žÂÇûÙ«÷î»ÇÃÄLÍé~;vtÒŠËäq¸~™XT©t\üÕÉÝÏÓùÞSø¦Š4öîë…ˆºŸ–M}Yˆ¨7¨sí\Ú¾vuïÞ@Göl¡ë— 15‹Ö ÏÔŠ™¯Ó+ ¥￉Þ|ì>š$ÖUu¬˜§B£›Ç¦šâ*Ë#•$¤´gŠ=W›·¼š)¤Î;¯‰:zKllh"ëøîw_»òäé‰ó©f;*Y·¯®JÖ_MÖÕhgViû³Ê–Ê¢RõfÕ“µ¿Ôö£\æ}¶’bnü[ÏÓê5«èÕ×'ˆ1Ãá±ß™>Ž^s¢xh¸Š&¾ýr¬M•Ræ {Í z; ©–siÄòWéüñ×Ò'ßESÚ—ø£ý‰hy»9bGàSIz¤Tä¿èPA>™Wìã¨}溩Ïüh©œÎÇ©ŸÙ@ü7OÒHnûð7Gä(]NéÿVl9AK×¶©NÐÚ?ýdXwPv)<–˜¦ÕãQûŽ­¢éSWQ0ïÿjÛGÚ3½K¯½9UNãc!µvGÍXÛIï¬ÙFs×ï¢%›÷ÒŠ­û¨c³ð"/Y›Úܬ¯jk¤Öum¤3¦ÜH?š÷Pà…Bêåm3bé™Í“éû ‡Ò?ÎLÿ4ïúßFúǹ·Òã›&Ò·gß_U!Åbˆ½$’›Ó#ñwö>q@„… …x‘û/Y%Q–×OÍŸ?Gz®öŠÁ|i‹«4!Õøìjzmë»4båaZ:åEZ6]ˆ¨Õs¨sÍ<:´³ƒŽìÝBG÷n¥î=›iÛºù´òEà‰hì7Ò#×_FOÞ|)ï›z}ØÔ¾t¶XÃTºzáÅ`j_šâýœoõš1™?ð‰Bª‘Sp@Õ1êoè¼QѾûÝ;ø(2Œå}„®nh ™¾CSu{9_¸½ÁjSâ(w;Ä1-±šÐ“]îºsxz÷4ÑOvÒ:ãQZ·ñz_Ч½tT|:E;á$¼U§? †âüýcá§Zñù†²…Ôñõãiü¸'Ã4}} Þ*òBª"+Y‰OHyuJ(¤´ˆZ´q7µï8H[÷¡ŽRǦNš>{iï ©'×M¦3Þ¸Ž~µôzsg°Чò™i\çLznËTúçywÑ· ¡ï,¼‡¾-Ò·Ô_þü/ó7Œ§ïθ¯ªBŠÅ ©M›:bBŠEßìXD±àbÑ´tÙ"™–-_,½Y:-X0—Z[gˆéi«+"¦Ò„Ô_>²ŒÞÜý>5‰wF]Óº—®áË9gýõÈå4qÑjáÁ9Š¢®Ãâ=QÏ­¡¿z¶þzÒ.úë‘&樂™²‹¾2y]¾è½¹ë}ú°充³ã üÎ(Qœ¶lÙ$Þk!õÞ{'E{ŽuÒQÌŠ({µ˜!GîãzxDQ!5ñ©‡hÑäWé•ûo¤×Ä=öD-{GÌM^§ÎÕ»Ä"Jˆ'XHÞ¹6,k¥&?O“žF¯>z=Üô j¾åRz[|Mˆ©r„ÔãOñ ©wfÎ#NÓf‰€+"JÌ 7Ý]óBjê5C£6DbÀý^i!5õšºzjp<9èºæ‘`°1õ;áöwßc¡c·«Ôv„;CH%¶¡…T.Îy™Uζ³BPÙu¥²)¥yÊäÉ“‡užs›êx…í7ô¦W:ÏyÎÃ×°ñ ¤pyãE¥ú¦œ6eÇ¿þš\•'½úú[–˜JòHÝzë`úÉO.Ž¥W^yÕ “®ÃŸgz¤ÿ=¸dŒLO¯z^m›F,}‘¾ûÖ-b¼p1 žñ4mؾ3TRÿõ{û¥pºú©.1uo²L<Õï7B.±+ŠMpé™âí€üeÙoÿ»²„”Q–Çèu,ª  Bª’¨"uù„Ô½#ÇÒFñqé–½ôöª­4OLçk^«=‡ŽÓÑï‹kñzgöZ¶rcï©kæÚ³w?ÝvÏðÜBj”Ú¾¡ñ2:/ÚvÞ¨GìïüTûŒ<–@ÑÞ=@“Wœ¸ƒõݸÈrz@ÄmŸÍý<TõšO£ƒ¡{Möà*q€Äå„”ã¹ÒÐø±“bÎ9'µ!ÆÀãE“‚Ïî7«š·×+§8ZƒÍ$þúŠIVÿ¸F™ßg+Aÿh»‰ XÍÊéGi{Mجc¦PRíŒÙv‚ýbÙdàisRú¸1o§kƒÂûjÙ²í#Ãþ|ù“Ž—ûÜÌé¼v¿‡\ÏI‰QÃöäu`± Î+Ù#¶W‹‡Sú^Ù‚ëiͰõ˜,÷œÌ{ŽyŒO—³²8ý;7£ü÷Œà<Ó¦Nxýš:éeÚµf,}pèzü‘;éšK¿N»[ϧý³. _\ü/tgÓ?Óž¹¨uÜO„Ô!¢´cÇN8ðRKHÝ|ó-"ÈÓn+_n!uß¿†BêÖ¹ÑY¯\Bßkùµ˜åò8Ý:g}û•èK]L÷MS²â¬9µï¿}s½ 2ñGW½Cðƒ·¤§‰…”TM¡ÿÉÏ;Š…ÔI: #÷mýÏŸ²„”oÌ—6µoɸñÔq,a|Î"(ôTͤ=:[®íÂÃÅÍRV¹”ãVD. ’$>!õÈÓ¯ÑÌ%ëhÁ†]rJß²-û¨S˜`õÁ§éÈÑtç°çs‰(žâWñ©}ÿ:á:ûåëhtÛcb-Ôd½émzVˆ&3½¼u:ýhñýôMá‰bÅâéB8]¿äºaé=t£H7‰ô«%ChDÛhúá¤!¹„£°D¯©Ê±Fн"¼N‡§ïqZ/RÇúuÔ.¢È± boI0È_&§¡­Y»’Ö®[%¬–k¦8:ogQÀŸgÌxG…鹄 SH™‚)Í#õµQËéŽ ïÑ™oí¢3[vÓY"ýíótΰHÀqùÛZÚ韟\Lß|j!}ó‰ôÍ'Ez|>}sÔ¼(=&>‹ô¿Ÿ/Ç#õ«;†Ç„ÔѣDŽ]0‡wéý÷Å4†S§ˆóåšÚ'~¸ÃŒùtÚ€ÇÐÆ`O šøà|ªôÊés>!Åe´GHy…‚ï‘ØàÁÆÕ¢Ž }jŠœn‡)²,Á5‹¬ã†ƒ¬áæøûÖƒœ•¯¢®ðØæ´Á’Û`3ò¢¹"Ä3ÅOóuûÑànzÇØ+'ù‡ù“D_Fÿøxš"ZÛŠÕçWÐoŽI ‹X?¸¼Ý|Ú¶úM{&C¾6'Õ©D­×ÛiñOèOçúý,m&©ÿSêÑ¢ÆõÈ$ØCx,÷Ür;´—XyÇ&Íãk¶ÆƒÙÿ^ϘÇÖ“®wel×Q?ºbÝç•Nâìó`Ú¢.èûŸº/úÎ-õž‘ãšç×¾v4}ñõä tìàh1âëô·ûOô/ßü:|>½úü¿Ðß~ñÓ/yu¬¼Ä+¤6tl"7-^¼„~üã‹dºøâŸŠßÎU±<¦Úºm·弋¶ð_•x§ÏÞÒH÷/M÷/M?îçôÅ~$Ò…ô¯\D×M½.|îJú³ÁÿL7Œ}0¨C¤M"meyTž5RZHmÞ*–OˆôÛïBêO~¸Œþà«o¡Í¾'“ôD…*þ\lb±E§ÄØâ3¶ƒÛ²Y$sÌÇmÛ´…·‹öyÖH%¯_ê¤%BD-Q± ¤çjQ§8r¾í¤T(¤¸œ#ưvªWÔžR6í¢ ›vŠ$ÖA-o§›ï’¬ÞHsÚ¶ÓòÍ»i‡R'N¼G‡¥ÇÇN ›nJÞz[æ÷§ ®ª©uëVÒåo<@_zârqqÞC¶¿BOl˜DOoxS¦Ñߢ!ëÆÒ?‹i{?"ê2! ®êf!œîXvݳü*Ò+î¡{— ¡ÇÛFÑ…îÊ-¤´˜ I!ÅÁ"ØÅ¢ˆÓ:‘ÚDD¾%j*ðµHâH}, ÂÄßE Bw/!ß–k¥víÚ–[Hi1åz°Ò„ÔÏ Ûߥ/¿µ›¾0A¤qÛè "2ßWîn¥]]¡GŠ×x­§†©]|©½}U˜xç+*¤Ø³Ä‘úxý§ŽŽv!:—JÅŠ§÷ñ©èï1ñ~ˆÃ’'WL­]»ZxªæR:Å‚ ÏH!µyÉtêÚ²Šïh§½ÅË~…hš~×u4ù–+dzóW—Êm[…GjÿÖU´ËJ±¦êyÚµy -žü¢P:EÁ.Š›øYÓPKHÑ"ê=þU¨?®ýÆÑŸþÉghÐÏÿ9AHm")ž^ý ºè¢ŸÐ¤I-Þýy…Ô_5€BêÓ†Ðß½ôúÎøïÑ•-â%¹#¿NzÝyô‹á·Òø‰-4á²’…”6¡…Ôÿûã5R0ýýU›©áFÑ©ÓbJŸø÷¾òñ¾ý"¢ßüÍ»h©ˆB±Z¸†–®;.ÅÔ¢?nˆ )-¦¤ˆÊ-¤Ä”¾©j”ëIâÆHo’BîT½´íf=–7J¯Ç2„U¯HŠþyPSHq>-Š-m£_ßû8zþ š9O¼wÙ:zV¬ü@35~ûçtû="pÙ´YÔ:{±WH麪"¤XÈlÛ¶‘FLK_u]þæ/iÊÎfÙñ:=ÖñšSoÈ©|?ér!¢ØÅê>!œ†‹ôØÊ{è ‘‰?mI½vG!!U4j{UX±`bÏÈ*žª'<,¤ØÅ~)°´hj_#„ˆJ¢{±xZßÛo·È÷JíÛ·³ˆJš˜&¤.¸kºðÜ¡|g?}áõô…1¥úÒ°%tå3‹C!e £nZܱSÌ ÝCëvuÑ&1­lÛþ.Úy@¼ÿI¤="Rì}ãudì…b¥§æmÜØ!¯:~ü¨ ÎY”?ÎáÀ†ž<Ó£ÇBjîÜÙ% ©·ž"…ÔÎötPxšØ#µMÑ$Ô é-!¢^»öb¹mÓ’wÄ¿´»}uÌŸL÷n“©J©¹üŽHHápðÂ%EÔtúôGôñÇÓo~óâ|=/¤ôSè,AåNŸ³¿KOÔTå’SôÄN¯cRŽ©æ§pPã7MÄ$ –5[ñi;†§Ê[Þ3@/Ôgð˜w íóú™Â! û¸³¸Ô¬SDœ1pLíŸBBJð]Që´3¯ÀÏ+¤¸ýǬg!UøÜK’í¹ž#¯y¼Ð¾|iýêõè&MùÓH’¼¨)çXXHex¤|Â4‰‘ù(§šð¶WlÚJmë©uÁ’Ä4uælñÌK^!Õ±~“˜ãOãÆ½’¸ÏRÒs³e§ôÞ褽K<ðlz~ÅStÓÌëèÚ)ߤ<ùOôçWÿ=}ïÎA4úå—iÎ<DnÊq"m‰K^”Rì)âôG?Ù(§êÝôÂzjøÜLºå5Y'©Oü=SsWþ†þò‹oÑï|þUúÜÿšB÷<²Zn_ûÙß ÛÁmÙ¤·)L›y»hŸÇ#›Úó$)±‘%¤vÍ´×Z¹õ`­Tͨ6-¤Xø¬ß°C†4×iƒø<{Þ zëí¹4q*¯]ö.¶=þÌ8ºå¾Qbé>š=wÍlãC£œYWå„” AnN£ëêÚMæ´Ðß=zýãèAÔ²íaz¢ãy"æ cÁ]ÒÅ"ê¾UCé±µ÷Ó3mâŨ"½Øn§qî§ãn·Þ!¼OJ„>—)z¿”$¾÷MqÛt¨t[¸¤™B0‰©\b Ÿ™VÅYi‰×úL:Y‰µB0ë“ò¦ã¼–Iæ×ï›ÓéÄgN'xm”J:Ÿü+B‚_÷X+]öƺdþú‹[!õâfú«W¶Ðßß>ƒn#B˜ Ï”.×uè ½6§ƒÆ¶¶Ó<1§sé.öqïÚØuH¼¿á mÜ)B˜ïÝ—(¤\A¦¿¿óÎÛrm¿©EOƒdï{ø…ƒGÅ‹kØ'=uÌ#Ø~P®“:rä°_kDxXN<­Å*{ôøÃðæú}Rî_QÏN/=xC ¤Ä‹w÷o^I„Wjã’©r: )Q-7ÿ‚Æ_÷S¹mý¼‰´eÉÚ¶lµÍzŽ=HoæR¢íÇt2Ž›þüŸ.½-RÇŽ¶únè‰b¥ÿq¾²…T,²¹¦ÃÆ´©«Ã(|ÎÀÔœ*•ãIñÕb’~²Ü1JLïÞ)˜Â(ËOÙe±-ù¸‘*1Ѓ|rîY¥=áú™(9%Ê{ì„`z\¾¦ö9L­)[I^µŽ…ÙÆŸâ;SMļ“úÇô^¸=Ïõ|¸QÖ’ñ¾ú¬>RërŒ5;^”:~h?Yž#wZ`¬o3¦†…×QÆ”eº”mWÒÌÙ³E`¡VšÞ:ËJ3Ŷ·¦L¦‡žxÖ+¤Ö·‹él%$-¤8H‹›wÐ&3mßEúãŸM/®|’¾týͯΣ ~u!=öü³4SCZ±¢-*R°¨òEXh.Ë/ð•ÓöÄ_N: ºû—…oÛÄG¤O‡CMí!Ï?óç 蓟z–fˆwF½O"”ÿã:þÇûØõ;Ÿ¾Æ¾²B†G_ùÛ¿GÅô¬0‰ú6:iÃÆíbÿÝ  6aMÅ+:µÏX÷äÚÕW3ª¢6$RBD­ïØ.ÖˆŠ¿9Gö2â¹PLMÁÁÖoŒÊ¶‹ºø;¿_ª2k¤Ù¤Ëu:¼² z ×±øÂ.»õ†k½C°{„ªlCvÈv®[ŸkØGÎÀÕÚÛ=GwèNôˆÊØú³N«ý¾)uñà¾vZ¶ç¬¥±Â^ë~Ië1a Æ­c&µ9í@R?¹×‘/Ÿ{<ÐLÙï{ÀMs‹›o-’O`%“qÈvù„”qÞIö˜8eÓg“V`•„ûŒq³È9åí¿41•& -Fi÷Éô`/½)f¨ˆ÷DMbiÌË¯Ò ¯Ž¥—^{šŸn{ðQ¯joß ÆÅ“)¤XD±Wi£™„bAôùËϦ¯ ù!yÝ·èæQ4ùíâ7|•xè)„–H\6LªüšËR,n8ýÞE›¥w‰Ãœoßû‘˜Úøý?¿ûÝ÷ÈF)®>ÿÄj¹ŽjÂÚ©á?~‡^2W ©õŸþ„§â|tõmpRdžN±»WHñïnbøó\A%Œéyì•2¨§›PÁ(ú¡éíSÖBŠEÔúõRL±ʓƼ29S»…gjÎb1–uµoØ^}!ÅbêøñC´bù"úÖS7ÐÙý”îX|=Ýq3Ý"¼QO´ÝMW¾z™x'¿›ÉNKÄËneÛ9™^¦J{¤¶oß$‚Ml¢Û7ÇÒv±¿ïÈR<mÊ”I"°ÄT±Fh“øçõBY%JR]]{èûC¦‰õh[èñ 'è £;Ä:)!¤ÆBêï¦ì  fí¥‹Z6Ó¯§m¥á«Ðsm]ôzÇ~šºe?Íon^عVîÙO Öo§uâÆÈÞ¤©}IBj“xG{¥8Ð )½_ÒË>ý)¢ØÓÄÓ#Y(±˜b¡ÅŒ§ñµµ­•"нRï¼3Eäçò–æ‘uËO¥â°ç,¦:W̤µ3^“¢éõëJ/ýâûôÒåߣ-~Yn[óÎ+´Z¨5S_¢“ÆÐу»iìC¿¬¨ ÖF‰®ïë¢JR%½(2é©vÖ[µ?u=LÎ:Jjw?¯ÛåÞ# ³lÅd 7Úi3«ÍufGUåé ©36=r TÉ›Sgˆà›iÎܹôÎôé4mÆŒXš1s½9i"=8Ê~—”ÞÞÖ!‚^O¦bÕ±‘Å…‘„bAô7W}~5lMjy‡,Z.öè¨ÄßäÊwlŽEÙ¢BŠÅ §ß¹b­Iì—â´û=¢ï^?ƒ~ÿsÐüŸÏÑüËô»g·Ò¿ûü2úÿo¡ŸÞýŠÈ#2‰k>ý‡Â£À^•D}nêØ&öw& ©ª ìYTÉàøWkB!ÅÂgý6)¦Ús&ÎûÜË-4dØhÚ)¢gò;?çÎ[&Ë·‰ºÚ;:{FHIÑ#¦©u¬_C÷¼ù„ð>]L7Íø%MØ|3½±áV!¤.·#ë©àz^\@é©|•›Ú§§Õ…ÇT6þ®E )íb±À"Ч íÙ³½d%C”—(¤Xh,\¶ŠÎkj¡'Ö¡Û—‹ðåc7‹õR"Äù›ôåÉ;èëâ…»WÌÛKw.频«ºhŒRon< „TÍd!µc?ÍQG¦,l§®ûL!Œ’Í/ú|ô¨pÙ+1ÅÑötô=O³gÏ’â* â¡Ö˜éõfóçÏ•k«XH"Š=WË„GkoÉBê‘_~>ü@„ISõ¶­œA›´Ð*!’X4½(Ô´;¯£™C~IÏÿìšò­˜ø­"jÙ„ÑtdÿzúîË*&¤ØÛ”'å™ÚWÒ@¢¬¦»Æô!Ç‹f+88)‰AÝ¿—8gØJ|ßíÌX[Sw}m^?•çiz>ûÒ }ëá>p§x’]$™ç¤…TÛºøªx2…‹¨Ž ,.Œ$Ä ¢ oN‹éJ¡¥Óöè¨dnÊo•e‹ )7œ¾øÃõòEQìaÊúÇ/î•ÿx•x¯ÔæÏý¾X êÑIÕ©ëæ¿ë;Dû„˜òMíË:^‘ý{E/õoFè+R òV€R,¢ÚÚEtnþ›3q^NϾ4‘.iBO=÷jXv¨«MØYÕ§ö…"H)S{ötÒK3ÇÓW¸„.7ˆZwÞNWÕ‘êèh“S×xªßŠKÄ´µ]%y¡*á‘bsäÈA=q!÷«·iøênºcåú²xWÔ_ !u†xÙî·fì¡kç…zlõz©C¼xwÓAš,„Ô¬íhÚúôÂ;«hÛÎoT>!Åž*SLñK‹uD>Gˆ©(â¡x÷ÖJ¡‰(va¼»*œÞ×-Ox’'ØÄ}×üzÿ]Z?guÌ~ƒV¶/ÒL ü?þÂù³wÊ9%Îx“›—ƒPÌ_:—<ú úÒ#?£éùê4#ZSït´>ß1ͨyÖ{¤ÔT=ﻥØkDöóóm1•ïw&‹Pß…÷†ƒJè({¾:³¢öú”p°£öé)Q?3jß ^%ËSìXLÍ]¸T®™:ÿÖtý¸6ºoÉAº{Ý1zhíQzdÝazvÃazzÙNzhJ;Ýôô÷oμžðçõ/ pE Bê%rvÑÉ…7yDQ(’Œ2 ×ÿíq!¥ƒP¬Zµ”¾Ýü«ºR3g¾c•ˆ„ÿÅ¿=+¤¤GHLó['^¨û¦ˆƒíÈYô·L¥s~5YLû›BÿxóÛô¯ƒ§ÒåÃfÑ3“–ÒŠµí´sÇVCDUFH± Ób*ˆf8%3Óù¢°ëå ©¯ù*'= Ês**¤øbBØl6è6Àb*QBŠÅTo¤þÐ8ÇÞ½ß$‰(VUõH™ž§ŽõkëBHíµ‚JÔžÒï›: ¢åmÚ¸^¾Tx™x7Ö ñ‚à•B¬¬^½\vXK»vm“¢‹óg“(æ‘Òõ±˜âH†›eÚf$½Mÿ5E Âr…Ô qÞ2­0’Þ&· !)““G}_.þr*"¤´·Ì.½s*Ú.„¦ð°YI¼—Šß­•'ù¢$^/À^2£¬¹ß[Ÿ7T$.¥x–>LÆ1»ÅvNÞýÆvÏÌÛ}¤[”U©[üÕIÕi–±>wó{Ç‚ãNº¬,ÿ°ø^8e8™eõ¶‰<œÌòz›ùWC%ßþÌm‡ÄqܤŽÍeªT¤žƒ¢¾ôtHìçå; ¾ëä+{à Ø¯RPV$ñ=Lª.«žƒ¢~•ˆ¿2Ç1ë ÷ë|òotLßþ.±Ÿ“™¯K” Ó±_¤f2òó¾|IÔy HûENIåôþýâ}ù“¨·‹“YFos·yöí’.··«n3ò™ûÃ|bÿ~•Ì:Ëù¼WÔçMûÄv‘ö)-çöŸƒ´GÔ¯“ÌcåSßÕö=âojb/'3Ÿo›µ_´Axod¹”dzŠ|ùlÏ“ö*UÇ#¥½Wy„TèéRÞ­žôHñKyƒ$¼X:)ï×Nñ7LbßN•B/™Qf§¨C'Ý~s[å>GíØ)<2Ç6?ïû8e{—¨S%]&éoÞ|Yõdï?$Ú®’òYeô6þ«òYž#Ãc¤Ëù)©h-UõTR· ©ª­‘‚G*ô^EÓôÒ‚MÀ#¥ƒQ$›°§ Fë®à‘Ò+0µON±ÃÔ>)æ0µ/˜F‡©}˜Ú—&¬ ¤ ¤ ¤âSø0µ/ˆ’×÷¦öž¤äéêêzþZø„À9EÒ“"Ǭ_ç5½WjJ_â±uT¾„©ºœªÜXå›:¨ëòE4ŽcEÝ3ïFÿ³öùƒVx_âkFÝ3^ô×ðny#ôù„R)SûŒèÂ+tœ“5…0^§9ÏšÚ§ÊÙÛ¢úyŸGþ8Ú_Žr0•t]öZ¥(B_´ÝˆÚ§¼[Gåš$•ó¯éé2¶ëµMáZ)gM”¹Ý÷9s •®/c=”½J¯¡2ÿjqe®2§ÿéíÑ6_¾}f>k¿µÏ]÷Dö ¢ôÙë¦â‘÷Â(€FD@{Ý”'*Ÿå]ÒëžLï”q׊þgÔ©Ö>ùÖ=q;2×éµUžµKæZ,»cíS¸ËG¡×H­‹Ò룜¿ÖT9qüN ×UÉiuzú\Ö4;s¿1åN•·@ñ†G÷ˆ¢¤:ýB)+©ë®L¡$ÚÍ"O&u|K(yÍíy?'½sÊÝž,΂s²ŽçYe½8Üï ƒž´Í|çS°É\¥=Qöº(SHŸ­ðæ¾)y†`’ÂL¥Är²ŽHHÙŸõÚ¨üûÃuS–Š{—üÇñ‹¯Hõd°ƒIÄ…”_ÅK˜‚ÐZ*o†è1…’7…WHùÄ•"!˜„0"ü¬¼ZEPX'‚cWYSûº… ’É¨× Á(G˜EB)B>Q‰$S0yÄ“Vj»p"Y¼/Z¶h*"”‚¼f@ ";(E‚ÈrHÈï*ð„ÀÂ,®½ÓçprVô]‹¢H…A$Œ2¶P ¦äYÁ(¼b(šº•àÀAòNT‚HŠ"•/ø£ð‰«üÁ'8 DF° S9&d0Š0ŸUÚ/þ†)V¡Mð_+…¤B}ö‰ª,¡e %l¨Û+tD“ÎkŠŸj”×Âd¯O:Y¢ÆÀÂ'´|,™<‘$Šôv[()ѥàÇB¡«ðèæ~_^3dº±_‹%SH¥mKW>¡&a˜ð-3…G¨ÙB+*¯ƒR˜Á*ô±¬2FaS|™"É#ʼ'O™H0Ù"Po7ÅU¦ÐRBÉÎWDˆ©¼Qc“ˆ0ÏÃ5J èI“‚Qhác– ·"Ð;MÏ€J ùêIšâWQ!•&€²¼G–(J "áxвËDBè¤ðé‰<¿×Ì/pœ÷I%x”lÑúg‹ž@ØBÇ)áK|£<^ÑdŠ)CäxƒMè¼–òˆó=NFÞ4Ñd Ë; ¢HÔØûƒ÷HÙâ,zÏTX¯Ñ¦è}S¾ìRÉô(¢Ëa/ôl™BLj¾zžŒ:­~Ú3fî÷ˆ¦Ì¨{™B+)Ÿ™Ïñ™ü´2=R¢(R‘ÇÉ6l÷FâKŠÆç=¾ò‡…J§,ï“o¿·Œéòz¤´`JMI^,O$¾Àe×Ãb+L>¯”ÏÓTÄ{¥–YÆ]Z Yâͳ¿h2¢í…Q÷̨~J4^)•BÑ” †ty'Ê^$¤Tt=s¿W–ÀŠ„XèÅr#ó©ïZÙB*.š,•Z¶Ê,9S<±˜ SÝ/b^eŠ-K4ù"øeDÔQ÷|â*)*_(®ì€¡ðÑu%EýËZ‰ÓûX$%yš|ë«|Þ©Œm™BËSÞµÏ=^ñdxœ¼¢)Ú¯PR”¿¼ûíAý^q%ƒ]Ä#ú¦ÜY§¤wR…bKy±’¼G™ï™Ò¢*.”¬¨|F„¾H`ž-ŸÇËlS†(ÒÑ“½PYÑõ´Ç*+Ÿ¹_•Iô^Å=RÞ¨FùÒ¼Xù„–å‘ÊZÁ{ž’ÞõdŠ.¯P³íœÌè~¦GË÷þ(O^$@£mž÷M™B+SH=óÌó„°Øl6€ À`°Øl ²L!UôíÂÈ     µL ûðZ¿y#=2l4Mž<‹ZZ8ÍŒ¥Ij[øwÒLzð¡fYBª–{m¨8R#Cë×o.\7—á²R…Ñ¡€€€€€@=ÐBªÔs€*•Ê€€€€Ô-SHþè#*’ø¤!¤ê¶ëÑpR ˜BêäÉ÷Å«šò'©R©£€€€€€@]0…Ô¡CÇÄ»ítößý]l›Î!U×]ƃ€€€€”JÀR»Ä{§ÌÄ"J'w‡*•:Ê€€€€Ô5SHmÜ´›t2E”þlîçÏRÛ·!l6€ À`°Øl6P+6¥òL!µfívâdŠ(ßwB 6€ À`°Øl6Ð'm ˆZ¾b³%¢ø»N¦¸ÒÛ ¤pÑôÉ‹¦Vž‚ x"€ À`°Øl ÷l ˆZ¶|qbѤ?›ÝíRRR°Øl6€ À`°>i¥)ŸˆòmƒÂEÓ'/<ùé½'?`ö°Øl >làÅñ ²lÀ½ž·m;M;ŽQGû!Zßv°ì´vÕN*šÌãr;6¬ï–mâ¶¹í…‚ØØ À`°Øl6PQà4þ@¶W˜°`éܲŸnù}î»wSÙM½š~÷ëƒià½/ÐömiËæ÷!¤ð$«>žd¡ŸÐO°Øl6¨_€êÛ"ê‘a£iòäYÔÒÂif,M2¶éÏ“&ͤj¦–·gI8>!ÅÞQß¾u Í^µµ×!¶wî—mùõÈ ðHá†\¿7dôú6€ À`õcR½®ªÖ€‘¡õë7—T?—ãòiBŠ=Q­+·”T5 µ®Ü,½c;vį¿¬ãéðç[·íß!å¾/*é;—aV¦q¯L§Ó},?7ðÿø oäÏYÿp㬟'ú }€ À`°Ø„TÖè¶~÷k!Tê¤ )^“ÄÓùøßÇ\‰ÛÂmÚ³§4!uèðIzàÁ'é™Ñ¯J÷?ð$íÚ{B ?(øA À`°Øl ?Ù„T©2£öËi!tú£„c¤XÒ‘èÒEç u¾NQÞmôøâ¡Âõ3³Ëç8F%„×ñþû„¼ººº‰Ó®]hóæ½2µµo§•"ÀÆò›i…Z#V5RÕDçØd ©àæ:pœçfÓçPø)íÍdR¦*ÚÖ¢ù3ÛïôQáúÓØùdçß×(½T^û(Ú^䇗6€ ô€ ø„‘œ‘ <'55Ë€E?¥‡ª…R'-õ ©³ÿîï¼¢ÊÝ^DH½÷þÇ”•¦^Ó@ç=¾Í“o&]ÓpÚ˜]GÖ1ª!¤ö Åi×΃ŽÚÒ B*ô.VSö‚iãÆ©Á½š8.4®GJWÓ­éivC R¦'bß{]ìÎñxŠax-Á9k›žšç¶;/oKïXž¸ïpðc∠½Ì%á)Rb{ͺLÆæ´JÝ–ÑÆ´K÷XIœ=<ä€ ¥_¼}îNóLë×¼õìËb·=‡ ðšÔ4@=5C?aTScÓDÍi&¢¬ùcžT§ÞºÁÿ°L³š"wà8{:Kºøs¦¾„?âYçÒsOJñô¬a°Òl ¯Jºòï©8æ}Öai÷ˤ}¦GÊÉŽERêýf4ÒÙîTA9NÐU] Ë’=Rjº^sP§Fž»×ê|—¢Â™ÚgäárM­¦òà¼Ñ4z{_(”:i¦R»v•ƒÄbI'½-i{!uòÝ)=ͤ«…Xz¬#+ïßFg,³8ï)ZŸYPoU„Ôþ#´_¤;Ù#µG&é‘ZÝ©pÍ’Zó~×ë$áä_wßèP`˜ŸÃ¼VãZ,¡’%¤¤Ç!i}”ëñpÏÁ#„"a¡×vi‘—pžæ1œ¶û~@rµ7\“æz 2ÚŠÝV§¼ÇdóMÈï¶ÇX÷–Z¾H{ÊeçRúGV ¦PÜØsÛí§œ® s§q8Ó½uºƒ§N·Lø]ÿÈgÍÁw~XÕ.Oo5=pðÆa[Ú ÜÀ­·m ÏÔ¾„ûª~¨»¯Æ¶¥Ýû“ï¥ÎýÙã‘JýMñüf¸÷j»|9BJ hµÒél¦F@Ö÷&!¡!%óéí¬±‚‡ÕA¾NjntëÀÚªjé2-¤Ì°Ý¦˜âíîw·ˆ:zì#JMmOÑ€†ë©%+ŸØßr•°«f„õ¹ßÓŽS !eMíÛ"„”HÁÔ¾^õHiÏ;ÈÏ'¤Lo“éÑ =<δ‰t!•c}{´‡K ÓÉ)àÁj“ëµI]U°½YBÄÝŸ%”’„e’°h}%·GyàÊa§…TZ° ýf ï¬Á¼û½b>áie¬Î,ñæN‰ ž´úÖêäÞšf‚ho@q|Ø`_°„`æ}Öx€¤û< ΃3cÊ%P2îíÉ÷Ò !UÊo†U¦ØZ°L”pBÿÜ€æw÷³ò^Å‚RTK4ùêÕBjÍÚíd&S<éÏnž"Bjמ£”šcŽ+éU#ßâÁ½Ð‹¹Žº´AVuzÊ& B*œ"¦=9Sûôô· ODºÊXo”C8¸Só=R†øò ©0ÂaZ”‚íu^ÕØ,VC©– k¤jIH%‰(Þ^i!µQ "WL±pâ1‡RûˆWÃ%¯‡‚Ëýž&Ø ¤2=R•ŽÚ—ï}LZÀÉ— S/v×âh™|Ù­ˆèg½H8ßtºØ:¬ØzŸíõFÉ3§ f´%+ Ÿ/zÉríDíKöNæ`W†Š¦j”µÏðH…âLOh¢ûdö¤é€E…”áåÒÓNµ¯B2cÞ7éèÇÚïÇ´¨}f¨qg-•wý«µÏ}‰n¹Qû[Ò¢+ò$å‰Úgÿf˜õØë_{XHÉ|ÁkCt²#ó)1¥ö#j_õ¤U–GªRBj}Ç>Ê—^§Ÿ:Siúü2ºëÌs论þnDí;ó!š‘³þ¾/¤zéÝYQûjÿ¡Àvá÷*¨»—ú¯OõbX“ï2AÿBHÁ`°ÈòN®!f–,g»ðBÞê ™Þ®¹§„ÔÚ5»©RU„Ô~ñB^‘vqøó0jߎ^ŠÚ—ó¢®ôÀB b©Ò6U¬>'ܹ^hœr¿—®“bç›/Øl Û@Ý )'(QÎߩޖ;Õ;~O ©Ë;©’Rû÷ÇïMY”»ï!Íëý÷?_ZÌ"ª÷…”ŒF羄µnÀáúœ^:~ΛXEŒá‘ªÍ'ÊzÍ“ï庽a'8fmÚ úý¨¨'!eüƤ¿ˆ=>ö‚Êb×ï~[·íëvN\N ¶w¬ºaýúݯ¦ÖEKiႎšHSfΔm:x°¯)ü ÔÎú}€ À`°Ø€cRõ+”²Z~èðIzàÁ'é™Ñ¯N÷?ð$µ¼=KÂ'¤6m8A?¿çúÚµÒÄ™óz]HÍ^¸€¯F—ßÿ9!…~ì`°Øl6€ TÙ ¤²äöû„ÔÖ­Òî]'èæÇߤÏ}÷nj8SDkìÅÄž¨+x™Ž{—Ž‡Â³Ê7ΊN'D[a¯°Øl6¨KàA2dÙ€;nܱƒhï^k‰ˆŽ%)^Nœ(=:ET4™Ç;y’èÝw‰øïž=•Rû»º‰ÓN'ØÄªU[åKŽW¬Ü†Š8p0{e:þècñ>·ÁÔÀÿã/¼‘?gýÃà¼Ö}ᇪ.¨pmàÚ€ À`°Øl6Ð;6¥a’‚M@HAx@xÀ`°Øl6€ Àú­ @HÁøû­ñãéMï<½wp‡ À`°Øl /Ø„„„l6€ À`°Øl6PÐ ¤ ë ê瀧@°Øl6€ À`°òl .„TV#±@@@@@j‰@f°‰]hó–=2µ·ï U«¶T>j_-A[@@@@@@ ‹„T!쇄L@@@@@ €* ÙA@@@@@B 6      @H†ì      ,¤ŽÐþ®#´‹£ömÞ+S›ˆÚ·rõVDíƒÙ€€€€€ôoRý»ÿqö     %€*Š€€€€€ôoRý»ÿqö     %€*Š€€€€€ôoIBª««›8ÙÁ&¶ÓÊU6Ñ¿-g     @R0(HÀ'¤’ªhk‡Gª ^dè‹’–Ÿøü…7ògü¾D ø)©¾Ôÿ8À#U4èßà‘êßý³(@’ÚßÕMœvíòÐ\šÖæ>ËÖ&ųñêtJu6¡zbÌí xÎ' J‹¬²±ýïPch«C¨Ù…[¸Ââ*Þ–­=KõpùUÜ&|×›qO í¯¡wú¼¸• €€€@5 @HU“nu[ƒDc0ר¼?g­‘0µÊ" ÿ©9IÖ”ªVÿõ;!屉ÜB*x@‘ÿZ,ÕÐP@@@ – $ ©ýGh¿H;wÞ¨=2IÔê­ÕŠÚTd(û©¹1òÎñB…uxÜõ˸,ïC¥ÅG¯y¤ªØýMHùl¢ b1UO#jù‡mz$!U“½Vþ€9æ4­òëí-\å)?‹2ÎBª x•+Zq›È!&Ã)¢ÚK\`ªmåÎ5€€Ô¾'¤¬A®öêäYÓàxŒ©;æà©¤'Ð…ÛT¾à Û¬z± ¹öCå1¦ÆiƒfûÜâ}í²žy\Õ–£¤µX^!e÷Y.oE»ÈÓÖ ¥@Ý1 A¿Æ§—æX;Uô¸f£ –­ªM(ñ›´sŸ0l$×õSÆùFE‹ß«Š´-iZ±ïºŒßÝëÀ¶·îr¦Gú®‰äúâÌ‚5oy~já§m¨E}JH¥6ùG3×ÀؘRÇe„È(GHmSjþÜAâë?ò Ä­ÁV£ÜA8Ô€¨bBêYj2§/:‹úÝQâ ÙbñÁ^Âú(KH 1‚G8ýï›¾å ©fOPu>I»ŠÚ…ã)Zw\H )}“ô0¡¤ãª»b)e«j‰BJhpÄż.áÙ˜tý”q¾²¨Wl*›M¹W庶ùù‚unñkÁflÝ›’®¾w$ ™Öš¥1pú$“YÆuZ‹?Úh€€@íè3B*s8'ù>ŠÒUZ4ÁRÚT!4¢§­E…TìÜõ`½RBÊ Í¸¥<É¥ýôÛ+Z<,âÂÁ3`´Úçxb2rÉçÁÇ®¸]Bª”ºËæQâ9éÛ`©mö ©JÙD’÷-ŸG*ñú)‹Ué÷ª$/“l§¶—Vj73b¦ãÁÉÝw©×{Ogø›÷üúîÓ‘àËË ž©Ú– %  P?’„T—}Îi×.lb‹6!Rí†?w¶ŽçÆý±·ž¨»Sˬ§û%L×Ò}_N›¨¼©}Ék‚Òë ¶|ë?**¤ìÁKZ?ùÍfþ$ÏO"‹Ø”B»-©SbB*í<œZ5í¢œº+É£ÈõWF›«kþÁ}L8×Hñë'î%¯Ö½*OÛ=ïN™÷O·½Ñuè0Ó4q–öóéžGÔ÷xúXæ=ÏéSÇæË™fX??ùh)€€@% ô !eÿ¸ú@ÉQð¬€oê\Ê "ÿ~±6QYBÊ8ט*"¤žÐVPHŦˆ¥°vÍ­æSòÄÿ),2§:ž#Ó6œvÆ`yÏÃ;},-bc‘þ+hsß']+å\++ú§Ò6áõtXž•4ñígXÎù–s¯*U¸÷¸d»KkS²èa7—ù.¶¼Ñ3¢™ª:íë1þ0 ‚©’CÔ ý›@RùB…û§Ùeý?°¥x‡ÊiSúÀ%Û\ÓÞ™T` žk=YÁ&¼‹¼“Û—:5)qíX ‹Ú¬€¦èñ›0{&é<ªieÖƒ‡_”sÜrÊú‚aS*˶‰¬éžñÁâš­Ð4Ê9ßòîUÙmsï,ÉSâ=RîT›*ÿ¾š/ðOöÔ¾|õdßy‘@@ ÿèsB*ñi£÷G=ÏàÄΓïG7O½ÂØ¥ 2”ñ†uúžˆWZHyŽ‘6x*Y€d š“Þç“Æ"‡p¨¶*f«Y»L›KXcÝK¾†’l½¼6§Šë²m"EH%ˆ´l±RÎùæ)›|¯Ên÷t¶èp£QZ©ª ©Ö4Y§Õÿ 8c?> ¤œ)X Ó¼zÖ#UN›²Ì馜¼>*»Þ\ƒ­,1”[Hù.E‚ë˜2ÖΕeseð(縕+«½£•²‰âŠõ=±ÌóÖúøà&M¥‰Ûж©”cf­Ò-ò â )YSâT"³í=$¤XK™B¨éjnT éS¾©L®W&—ð.l£¨]$ˆ0o; Öm )0àñål<®u¡+›%*b~*²Ú¯ªØùFå‹ß«ò¶ÍÀCÞ¢:öŠR ¼Â/ñrÉžÏbÇ ²‚€ôy5$¤j•u)¢¦VÏí ”¹$A öà^U{}‚€ôeRi ¸EÏû£Åõe“è'ç!ÕO:º&îU}¨3q*  }@’ÚßÕMœv:QûV­ÚZ­¨}½„3s¾½±F$×Ô¦^:¶©b¼»÷ à^Õû}€€€€€AB*æu*t¦T¿ ¤ê·ïúqË3_*+)äYÚ!âÔA@@ B ¤B)auS0W¨PMO€êiâ8^Åà^U1”¨@@Ê !U<èŸ ¤úg¿ã¬A@@@@Ê )¤v Í[öÈÔÞ¾ƒV­ÚÒÇ‚M”EA@@@@ú'©þÙï8k2@H•EA@@@@ú'©þÙï8k2@H•EA@@@@ú'©þÙï8k2$ ©#´¿ëíâ¨}›÷ÊÔ&¢ö­\½QûÊࢠ    }€@¢ÚßMûEÚ¹ó -¤VAHõnÇ)€€€€€”C SHíBJ½GJz¤ ¤ÊÁ²     }€R[·í Õk: %.3ò±14pà`÷Êt:ýÑÇòsÿ¿ðFþŒ      З°:|ô8=ðàSôÌèñžôšû³ãé¡Gž&.!Õ—,ç     I`ÏÞn:Ô}‚9IÝ"9ú.=ö8ñ>|÷zÿýèÔ©ÓôÁAúðÃéôéß„‰ËCHebF¾DBª/õ&Î@@@@@ G@Hõf@@@@@ /€êK½‰sèR=‚èK ¤úRoâ\@@@@@z„„T`ÆA@@@@@ú©¾Ô›8!!Õ#˜q¾DBª/õ&Î@@@@@ GÔ…úÓ[ÈM=BšR, þêé:sZôgÞŽ      ÐjNH™‰?¡Æ0i¯T*¨ÎÅ4ºa( µÒhZÜÙƒxeZh“{HoÛ†RK«‘1©l©Í¯t}¥¶å@@@@@ ¨Y!Å¢éJú¬Qü7·Ê'Z[hhãb:ÜS—*¤¥Ä•%¦*ÙN©JÒD]     Ô¤ÒÓ÷X@ñt¾Ü"ŠÏÈ+6Q‹é!rE f…‹) ‰©Ìà Á!kýöòdz¤Œ¾¯¤J[#[Û• syÁŠx¤ˆ7¦–¦–h*`Ÿ2}œ €€€€”N &…”ï¼¹ÅT–ò  4¯Œ±¦(ï´»\¢Fušµ/Óƒ”Ðw”®§Ð)_DALë+ýòBI¾J æ„TÙ ³„”8{ZÂi}*JŸôPyËšQîÞæ¶%%j_ìÝW\6{šdÙ}† @@@@@ Îô=!UgPÓÍeQ§Ã²×tCÑ8èYR=Ë»nŽxí0­¯n: èQR=Šè  ¤úB/â@@@@@z”„TâÆÁ@@@@@ú©¾Ð‹8%!Õ£¸q0¾@Bª/ô"Î@@@@@ G @Hõ(n @@@@@ /¨ !ÕÝÝMH`€ À`°Øl6€ ÔŠ d‰½šRYÄ~¨%RµÔh €€€€€@]€ª‹nB#A@@@@j‰„T-õÚ     P ¤ê¢›ÐHZ"!UK½¶€€€€€Ô©ºè&4@@@@@ –@HÕRo -     uABª.º ¨%RµÔ=Ü––¦æÐSsÞ£'fž¤QÓŽÑÈ)GhÄÄC4ì.» ›6mß_r«înh N "က€€ô%R}©7 žË3BDé¿ù Ñé‰ÞÿˆèÄi¢#š¸—Z:NѺ-¥‰)QBEAHìd¨}RµßGUkáÓÂÅÿ|"êÐ)¢{ŸÙFM#zpò)Z¹¾¸˜*WH 4ˆ¾üå/K!Æù;þ€€€€@-¨=!ÕÙL MÔ꣓¶ÏÌŸ7_R”R>©Œ¹]~<4vj¤æN³1­ÔÔàlK(Ûä•Ï´x:ÿs=Q,¢º„ƺmXÝx_»L¿ÚFK—.ÍW±ÊUŽºñÆ©e/Ñ©S§èàÁƒò3'ÞŽ     ÐÛêKHå¥UŠ*WˆåR "ÑÒQMÔÔÔD¦ºòÕ¯ÄU©bŠ×D‰}Öt>-¢ö µrË š:¿Lƒ‡µÑ˜1crõ€^e )-óTÀž'-¢8?‹)-¤†m"x¦ò@Dª¨/!åzw›©¹)òî„‚ÂÔÜ(òˆ¼egs£á r=AÌš½AºN-z|Ûœ~©˜âör»Ä1U›å‘’êom¢}â$–`!¥×D™"j× ¢Lj¶!z{~ÝòàZ;vlncÔ"Š×H]'5bÄ¡âÄ4¾gÑìٳ颋.’ÙÅBЧùက€€ô&úRbzmXPhá Š@D…y\¡a–1{ÁÉ×*Äš÷8)eÂ]±©})ί΃ë‡Æ Š'³É×Gq`‰$µ¹›hÊ‚tÓ}« )>N)Sû¤ˆÚ4L”ni˜œÖ§§ö±gŠ…¢öæ-Ç`u.¤ Qâ,M¦’ýíY{ä³KœpSü$Ô‘¸þ‰½[ª|RÃóÄ3K´ez›Üöå7lqÎB*MDm8D4yÁAºqÈŠT!uóÍ7“/±èIÚçkéí·ß)-¦„WŠ=RÏ=÷‚ÌTþþENêèÃBнHÍöô8æèˆïú¢T/’žzçtJE¦ö©iˆV0 S„y¼Yex¤~mŸRX‚×D™ÓùØÅ"ªýѤ‡è†;–& )J .,”¸ŒÏ³$#ó ñ$§ö 15mpƒ\#Åÿx{¬XXက€€ô&>,¤Ñayx\ÒyÄôbåõHe,ÑcLëÓM §÷åY#UÐ’|y·Ri"j­ˆz>Q¼˜·éÖÅ^!Å‚¨è?í¡ò )IrzŸS,žØ ʼn·úÓŸ¦[¾Õ@ž—üåŽü    •#Ðç…”R*xƒ€æ®‰Ê%¤¸XϬ‘bÑóé`UˆÚwߨRH%y¢XD­ÜCôÖÂ#tÝM …T^oTÃÝbš£ø/MH±i2„xŠŸû)Yg}¾6>*D”ðV± Â?è 5*¤<ïZb…‘6å.kŸÀ!z‡“/jwƒžbW¨}¾÷HqP >¦/…Z“Õê{•ï]S9«+K»÷ÙN)¤tt>s:ŸQ‹wMXx”®¹a^ÙBJ ®$!ŧÁÓèJú¬œÎ·mÛ6ùýì #ÁtþùçKÅSþ ¦zã–c‚€€€0ÚRè—#0¤y«RâÓ•8ž"!ÕS¤q>CBªÏt%N@@@@@ §@Hõi@@@@@ Ï€ê3]‰è)R=EÇè3 ¤úLWæ?‘ÇO¤þ˜LB}tšÞ=qŒŽ=DÇŽ¬H:¸'•’Ìãs{Në–mã6 €€@m€ªÍ~©j«XDõ·î9Kud?Ýúäô¹ïÞM g6ÕDúݯ¦ËïN?H~ð~ë&œ/€€€Ô ©ºéªÊ5BФׇEÔ·oC³Wm­Ü2kjïÜ/Û4øÉ ðH•ÉÅA@@@ šjTHµRSC5„©‰Z5…ÎfjlPßÍÏI”òäñ••åÌ6èÏF[ŠöLÞ¶äÍWôø*?„T ¤ØÕºrK‰«W¬uåfÙ6ü¨]5(¤Õ*'¢ÎæFjhl¦NæXTdÍïlFÿYm©Ý~Mm„ɵQ<ÿ}üñÇ5•¸Mºmujbh6€€€ôyµ'¤¼Â‡ÅU#5wšž*áŠå5÷s~WxuRs£ð, QÖÜdˆ5®G µ !rN›´Ø³êããŠö´Þ4Ù4!C¯—j¯W,zŽa‡ÏE× Ð !x¤ ¤úüý '   U#P{BJø´Øaû—8µ/( ‰V<€ÅL˜?Øß(Õ•ø'ö{?g)Û#¯3ðœ)á¤O@ µ´i‰º½!Õ*„’ÙVÛ;—°/Åd ¤JR3¯·§z^?³:ž,î:x¤ªvÏCÅ    P5(¤Ôy±°ÈòÖäY/¥ò4™b$PRÔ¤¼F­M†7ÈRž5R¦§)æõ‰ .-~ÂÏV~íeË‹ÜN½&L·[zçÄ÷ø9§1BHÙBêÔGSzÚF_ láú™F¾™t­°ÏóŸÚ–QV×Íù/ Ç·fëcÙËR¹¿¡¨ÚRæ)Kà®r§çq*hDcss(œ‚ªµè‰•EÙ%RØ9‚ËÂh§ëò ŸŒiy)b B*ÿuáŠGsjß{ïLéi&]#DШN¾w®§†óŸ¢ ™å¹\Bž²Rùû9A@@@ ·ÔžbÁbFšPdxz›Üœä…JòÆÛM/Q ±©©É˜âov„¾`MSBÁXë¢òÊHô®Á#U‰‹#MH|÷cJO3éjá}:oÔ¶Œ|Ûè±ó /æÕ3U~sûôXGúñ ¤*Ñã¨@@@ªK ö„”œÊfGí“Óð²„Tuoz¨@@@*A …Ÿ–û)sJ3ì*µO –p וð^¨T—àÁi«åL3§$ºb‰ÏÒˆ¶gML=¯á”g͘8.„”-¤ví9J…ÒkW†ë÷Î~p*ÛB—6  u-Ag7\I¯Êú=ûŽ !U‰[êê¨Q!UÝ“kgáá™FØCGïµÃ@HÙBj[ç!*)½|…TWÐK\~þ0:ËûçséÞù\ÿ[ôóý9ýxR½viàÀ    ›@¿RÁ´;O´¾Üèê7#„”-¤6nÚG©é…ËE½‡h–›oöCtfÃ94d6—. ?ûêËÚ•ªßk -è?ú­ê?]?S©‚BJФ:sÈ2KpÍr5\òz¸mÌ%"Єñ}# °†ËiŒ`RýùšÃ¹ƒ€€ô=R}¯O3ÏBÊRë;öQvZFwé¼W짯;åÜ<—ÓsaÝѾŸ>Ÿ~uèѽÏl£‡¦=8ù­\_\L•+¤ D_þò—¥ã¿üÿ@@@@@ ¯€ªÓžäé|üÏõD±ˆêë¶atã}í2ýjh-]º´Ð™–#¤n¼ñFjÙKtêÔ):xð ü̉·ã€€€€ôRuÚ‹¼&JÌ賦óiµWh¬•[NÐÔùûe<¬ÆŒ“ëLõÚ(SH±W)ïZ)ö,ÁBJ¯‰2EÔ®D;Žm;Bôöü.ºåÁµ4vìØÜgªE¯‘*ºNjĈBʼni|Ï ¢Ù³gÓE]$ÿ²7Š…OóÃ?¨wRuÚƒ×Gq`‰$µ¹›hÊ‚tÓ}« )FRÊÔ>)¢6 ¥[D&§õé©}ì™b!•׳U§Ý‚fƒ€€€ôµ!¤:›©QMÓÓÈ‚¿MÔZjGÈ:s”Ï›¯@;7¦¡ CUM‹;‹itÒ¾Çàç,¤ÒDÔ†CD“¤‡¬HR7ß|3ù÷AÒ>_So¿ýö@Hi1%¼Rì‘zî¹dvx¤ t0²‚€€€Ô4RqÑÓÙÜH ÍdꚦÉÓB©I¸_è0-ndAÕBüM( ja%÷‰oMâsãb‘«ø¿‡_Û'…–à5Qæt>öD±ˆj?@4iÁ!ºáŽ¥‰BŠ…ÒÂ… %.ãó,ÉÈ|B<É©}BLMÜ ×Hñ?^'Å+Vø    õN ¦…żE­Ôz®”ðâ<–Øê¤æÆFjnµ=RR”…eÅ~­ÎòC ¤à8ÍMAàNM­Yݯ„”K­-ÒKÕ¢ËÉïŽÇ*«JµÿÁ—wK!•&¢ÖЍçÅ‹y›n]ìR,ˆŠþÓ*Ÿb‘$§÷)1Å≽Pœxû§?ýiºå[ táÙxÉoQîÈ    P[jZHÙ)H Ô¨Pk“òV)ád #V¦@rÅRXV $c `«Jñcè| û¼}ª¼O¦ÇÉ+¤ aUÀ6î»S ©$O‹¨•{ˆÞZx„®»iA¢Êëj¸[ˆGñ_šâæ2„xŠŸû)Yg}¾6>*D”ðV± Â?¨W5$¤"OOè92=M1ÏQ$ Xpiñ~¶ò³'ËðB™½ËgN14ʹÇϹ¶JNßSSûôÚ)Û#Ušº÷ÙN)¤tt>s:ŸQ‹wMXx”®¹a^ÙBJ ®$!ŧÁÓèJú¬œÎ·mÛ6ùýì #ÁtþùçKÅSþ ¦êõ–vƒ€€€0R†€a‘+|¼)”8 §÷Þ)¯ðñLËKó\‰ÕYrš {»JRdz¡rz¤Ø+ŠÉ„ùƒCš·J!Å!ΓDÔÜ-Do,Å •¾F*‡°S^§ÑÍA,>sj_%£öÝûäF<¬M¾l÷¦¡«èÆ»WÐ/Et¾ëE`‰k½€®ÓùØuÅU³h॓…T^o”™Ïh‚…‹'þˉ=Qg õ©O}ÊkëZL=}9‚OÔÊÍíÈO †…”|eš:MÙ&k£ó.µà åûÖܲ)³}vxûä2ehد8°¯>—R®Ùôæ°U‚…¯ž¸ý™ý­^$îvôÜ¢|üðÚHµã 4©ªÔ—ª—sü¬kÀ¬Ûøm(ç=]–Ù%þ~TþœŠ]¾û³ç¾;‡JØuî =ݽ8€@ý¨!!%~ð­Á²øqhÛ*)v*òCžò£Åõ ÖäŽÊÔ&i°_Ø|ªzÏ‹Ãð¼?|Yub3u€ [P(‘ræ>ñ °s¶7­œ÷“Ù‚«2ç™$âbBJ´³IØ—ÅÂ}Ùt̘’ضº([l2ϵ”&Vjš]O±Ái)çâ”1¯-ß Ì+¡‹4³"÷ßøµXÍw:ÙýTä”z2¯%Š­WúœŠÔ§SڟЫp/ªÊ=¡'{Ǩw5$¤xh¿P—¶2ŸV»gc‹™˜GÊ,+ŽÓÊ^0³Žêv{Ÿoê|lëÉaðĬÉxêm=¥¶ò²xž—ðÉq‚·&çq¬—›Ç‘囥'#æ…‰ý0™?ªZÜê§ê Þ´:²öY?ÈQŸX"4m@&Ï­UõLŽöªùæ¯DÜ+d ²ÎÅí[Ÿ÷-m `‹zòjm eê2S¨ºƒçü;I´]Ñ®ÆæÖÀ¦Ø{ç<=·Ê%6Ø7ÐRö'¯WU·*ŸhßüRí”ë(*¿¹fÒê‰ÿ0xÎÍ:¢Í­ì… =µæS}ûZËÃ3Õ6¼×#dÝ—œ[¶šó~iÞc}³”Øž»<^jEX2ä{nÚuðÛ ŸÓv%¸kÓ´Øý”» õ­6ÛÃ÷aó|³¸úî¹Á5¿Ÿ‘í¨#™÷‡”{A²-›íq'ÜŸï‹Îƒ-ù@PÝ«K¹˜ŒU=‰×Eì7/í¼ê}X‡öƒô$šR­üÄ]ÝW;››bb‡oöÖ4ËSÝ䃛©gjŸùCîùQ/\wì0O£áÁ¤ñc' šCaçzbçdLqLzlÏqcÀjÇâ㜃՞ðG_±”/:7ùãë§Õ‘Z¿Õ–ÈÛãqÃbÙ?h«Éj¯õ¤4ßàÍì¿òÏ3ãɯ)šD[›š;Y9EÌ3EÙ–{wqëˆ #.i¶ Ä¢¼1{K¸VãÏ"’§{švà ܼב}j0eæs^3iõxo྾’÷ѧrl->³^}w½,æ½À>vÒ´¶ô§ÿñA3bÇ1ÄE´ÏñÄZç”}á=×ãÉ5Wn/ƒ˜§Mí³÷Ù÷>çú3ܾá~qÅgb;Sêu¦âº÷âäß80ŠntÁ½Ð%რßyx~·¢ö5aµ'á7Ó½?gÝ£ç\‘ý—zÿˆÝòŒ äåæÌŠ(iæKOÕp,Z%P[B*|B¥ž¾[7nß“sß:(9* …‹õã—$¤bOg'g±}Ƀ_}So5­Ž‚õC;[˜ªH$èœAYÚ•fµ×°§<‚D v£ã–žIý„RiGº †G&k:Sô¤Ù^eØ-oŸ1ð²í$Ív=S(C—P.­áRí²¥OÓŒ 7£þˆs¼-IÔÑ`+‹EZ=Z(ù¦§šs¿{FßsòL²éð)~ƒ£œ¼‰ZA»Òî‰Åö±wÞžæœñ@!ášv…פ Ü¿ƒß ý Hy5¥hV¥µÓª7µ=)¿qá-×Xóf )׫e´9¼‡øú<ãšP×bò´t÷þœ~7…z$‚J½˜åÒ¯‹Øo^æyÕê í¨5µ%¤øÇZ®“7EþULô é¶!¤bÓé x¤œiÖôˆØ€$i@mxM/‚žjä.6„”)Àô@.>8Èq5Œ$˜ƒÒ”©G4¥ö}ƒµ´:ŠÖ/»Ù]díyúœåuÑW]X—à Gy9P6û®ìóÌ1(Óí 0˜<¢^òM%]|øl,qpã{únó´E35D‘eY P÷}(¦ìA¦O F¢Ä0†çä9W,åºf2êñõ_Gö)›}ãöSÊ÷¼<3½:Iö§9«¿ÖC %2²î—a`-ÞÍ{1‹(á]uïƒN™æaÝR³n³7›º ¯îNÁ’õ?Œ².ã)¯ëñð ˜òÇùq žºÅ§†y†'R4DëÔÌàŽHâà[6o=cP’ïDìs™F£6ë\,»²N<ÍkãØE¬\>Vöš–Hœå{ê.¦òÄ삃—ÄÃÁÇú3k0míW‚QË;@J°]ÓŽå8˜×(S¤b6ï›bäcm HÍö¤Ù·{ÎæwÇV¬§ÑE®™´z¼wx uÛ•ví[ms͉סsÌTñé4Z=hHZ¯¥.„ƒê¼÷ÒX>cíe†·7ý‡3Å#¥îGvø„)áî½Äw¿ÎÓNWLXõØm­ä)«¿bíL™Êé¬m ëÚ7%ÜïiMº/"ÙA†0Ï}ÿ°îQ)×…{/Ëq^µ6PC{@j—@í ©D±ÃƒUð²hÔ>çI§÷É`Ò;}ŒãšÍ~5oÖ±Nwê9ßÍÞ7ÒÎysZ‰ùάÔ'ÁáÓठ&ÿ”à òüêHÚóÃþVÂýù'Ø÷»è|}÷2«3ŸhÕî-è}µ!¤zŸZ P¬AlI•¢P%dy$+qŒ¨# ´ÒDzÁüª=È.ÐG™Á’øôÄyì{-fe^ˆ]° È !!UŒ¨ú+¼ÞµþʧwÏ»äÁwï6Û8ºêâU=ýÏrW¹ã»^×|¯]ˆÝÉ×¢êG¾ãÇri/Rè Æý£D’( P# ¤j¤#Ð ú!!U?}…–‚€€€€Ô©é4@@@@@ ~@HÕO_¡¥     5B 6„TZ˜éÔpè9(ÆÂœç(S$K¹í“ÇrÂPwúý®+À”ÃÔ|Ê}‘cƉd™k¡{¨UEºyA@@@@ šjHH%¿ç"Pp h~§QŽºK¥\!e½ï%öâÌ aú]>Aßç$÷Å’á¹VQHH¹W©ýƒr     Pµ/¤J~A¯¢•úâ]C`©³x9`ð¢W÷ÝæK›Å‹9l­óòDu¬&_‰‚.GøWÙ¶&jjô¼o÷Ïó‚V÷åœ@H555ªsŽBñZùå1£—ì†Â4ùå‘Å^šƒAŒU‚€€€€@©êXHSÝ›;•&h¢†ðÝKÀ¤”QSáüõÙÓê¬wsÄ„^Ž6™½pî{EôÛã}SûÒö™Iò<)!¤Ý\¦7Ìxó<¿1^rÉËR¿½^7Á‘¯›_è•jÖ(    Õ%PCB*ò±7(4I©˜‡G‹ XªGË(ãÖ—é S‚'-_ži…JÀ…Söœ)qÑ´·¸JÛQpÄ£…ÇXæwñY ÓVá±’zµKKÔfO!Äô¾ê^è¨@@@@ ²jHH%¬cJR¶øjÓÔ´ƒ*Ääñé©{Á_HàÂn9K$°41–Ôo±µD†è°ö9B*m_x¬ÀãŠÒXÜ)u¦ÕŸ#AR ü}ë¼”`ôO› Ö€%·³²FÚ@@@@@ \u.¤r‘Hõ¢øzË#ÅÓà¼7:ˆ„-üa”¶Ož•ëéòZJšG*7MMjZŸO\&ñˆ‰CçàO„T¹—2ʃ€€€ô$úR*d¸w  I0m]OîiyEÖH9,rD Œvhgçþ+þ<—ˆâúm•g¦¨“õ^¾¼,Ý5R®‡Ê#¤°Fª'/{ @@@@ \u,¤"!4]L‰ ÚÍHsF¼ÌiyfÔ>A/úPB@òj™]—ý.Ê•_H%y«â¡Óƒ©}Í, ä4G×ÃÇÇ4·ç*–À’÷ˆÈ…æñø{äYs§`"j_¹2ʃ€€€ô,ÚR={Îå-kêZùG¨ø<ýoòÍn_F˜Ö—9@@@@j‹„T®þ°=/Þ ¹ê©ŸLVˆ÷›K \%6Å@@@@@ â ¤*Ž‚€€€€ôuR}½‡q~     '!Uq¤¨@@@@@ ¯€êë=Œó¨8ÚRê½Gî‹g­€qžwqþÆæV¢;­¾SŠ1yÞUTqzE*ì\L£ZhS‘2åä•ÇJCUji5*³öõ`›Ê9”"PCBÊy‡QÚKeÓ^¤«áÖšêÑNßD- £iq§:¨Nú»½ïpóhÚ¸˜÷hûp0¨oµ+¤˜kk5h·T(ŒÜPäúE¯æK^}/¸ÍñÒ[BT,nÒžÃ[#÷µPK#ïÓÛY”8yU‘09L‹…ˆiu=Rž²|ήçÊù.…OxLC,™vØÚBC›lß×&qNÒ+k·^©ú¾ŒÑzž&PÛB*ÉóT‚GªµÉ˜úÇ­±™´Ã&„®¦¼ndå­Qû¢)r,†’ÎK,^¤gG ']¹.Ž ba/›!¤\‘3ËlL/£™‡¿›,U0¶ŽÉðÖdxŠ„ì ¹Íð†aZ_.ŠÈ    &ÚRyÄSÒZ¨T¯U§P‘WH©és^s¦Ûy]ï”Y>&Ø /V–`K "³í@0…ž/Þo­—bM)wã„@@@@z‡@ )}Ï…/Å •朧ïq]:¬zΨ}B´´$Eí‹M3#難xºÝ)tÞõWžè€¢¼™otób+ª©ÈŠÚgGöó¼KJNçÓÛ"ÿõŽ=⨠    PjCHÕ ª´©rµÒF´@@@@@ × @H™]!Õ뉀€€€€@=€ª‡^BA@@@@jŠ„TMu     P ¤ê¡—ÐFš"!USÝÆ€€€€€Ô©zè%´@@@@@ ¦@HÕTw 1     õ@Bªz m¨)R5Õh €€€€€@=€ª‡^BA@@@@jŠ„TMu     P ¤ê¡—ÐFš"!USÝÆ€€€€€Ô©zè%´@@@@@ ¦Ô„êîî&$0€ À`°Øl6€ ÀjŲT„DD,l6€ À`°Øl6àØ„. \°Øl6€ À`°Ø@A€*¬V\‰hÜÚ°Øl6€ À`°Þ³:R“hPC5È4€†D†¯¡îIƒÔ¶A4©;Ê3h’‚ºf8 Ë ’冯é=àIÆ>i>7>Úk.Rô l6€ À`°Øl ²:R@rÅ‘RBtL$„•G,œMR®I.gì“¢Ëø^ Áb¿øÅ6|„.Ðâvf`€ À`°Øl gm >„”@Z4ù $QHe”ãºÖ  =Z¡HË#®/×-~Bï˜ö.‰¿JÔiSx:ï€áÖ|TŸÒm ½q5èQÃ…Û³.xƒ7l6€ À`°ÞµºR,$Ò„N4-N %^²Ê•*¤,á&‘áEJôH±wÌö6Yõ(R\Ÿ)¶Üïy„ò`ñ$l6€ À`°Øl ¢6ÐG„”jŸ)¤,±Ný+QÅ:ž'Kä¥Lí³„]B>WHÙÞ(c¼R½ðD§Äk7dØ!l6€ À`ýÔêBHq@‰JMíóyÊDû˜A8Á`°Øl6€ ”cõ!¤Ä`ø;H{˜´'(=ØDútºâk¤œ)z©Sïœ jMÖ áÎt=CÌÄ×Hŧ–Óá(‹l6€ À`°Øl |¨!Õmxˆ‚8DSßìðç± :ºã-*MHÁ$<*Ìi„qOZ\\uaÛƒ€*Ä»ž¾ bÈ~¸øË¿øÁ a°Øl6€ ”nõ#¤úÌ´hz ·tÃ;°ƒ À`°Øl6èM€ê)æxÔÒÖ|õ¦AàØ¸!Á`°Øl6€ À²mBª§„Žƒˆ6°Øl6€ À`°>cu!¤²‰ý      PKöìí¦CÝ'èБ“Ô-Ò‘£ïÒÑcïщïÓÉw? ÷ßÿ€N:M|¤?ü˜NŸþM˜¸üÀƒiÜ+ÓéôGËÏ ü?þÂù3þ€€€€€@_"!Õ—zç     Ð# ¤z3     З@Hõ¥ÞĹ€€€€€ô©ÁŒƒ€€€€€ô%R}©7q.     =BBªG0ã     }‰„T_êMœ €€€€€@€êÌ8€€€€€@_"!Õ—zç     Ð# ¤z3     ЗÔ„êîî&$0€ À`°Øl6€ ÀjŲD_M©¬Fb?€€€€€Ô©Zê ´@@@@@ .@HÕE7¡‘     µDBª–zm¨ RuÑMh$€€€€€@-€ª¥Þ@[@@@@@ê‚„T]tS¼‘/,;MÍ > §æ¼GOÌ$5q/µtœ¢u[JS,¢„Š‚ªSû@³A@@@ªKBªº|«VûÓÂÅÿ|"êÐ)¢{ŸÙFM#zpò)Z¹¾¸˜*WH 4ˆ¾üå/K!Æù;þ€€€€@_!!U§=ÉÓùøŸë‰bÕ%4ÖmÃ:èÆûÚeúÕÐ6Zºti¡3-GHÝxãÔ²—èÔ©StðàAù™oÇ?è  ¤ê´yM”˜ÑgMçÓ"j¯ÐX+·œ ©ó÷Ë4xX3&×™êµQ¦b¯RÞµRìyÒ"ŠÈbJ ©a›ž©\½€L    µNBªÖ{(¡}X‚…”^eЍ]'ˆv#Úv„èíù]t˃kiìØ±¹ÏT‹(^#UtÔˆ#„ŠÓøžD³gϦ‹.ºHþeo )žæ‡     Pï ¤ê´9:¯âÀI"js7Ñ”è¦ûVRŒ¤”©}RDm&J·ˆ4LNëÓSûØ3ÅB*¯g«N»Íè'jCHu6S£š>¦§‘›¨µÔŽuæ(Ÿ7_‘vˆF+gŽüÛdž„³¯¹³HÅQ^qÎB*MDm8D4yÁAºqÈŠT!uóÍ7“/q$íóµúöÛo„”SÂ+Å©çž{Af‡Gª´¾F)Ú#PCB*.z:›©¡±™JÔ½C[4¶Qˆ'ÑlùOè4)¦´`jâÙrMÁ¾Vñ—÷™:+o£~mŸRX‚×D™ÓùØÅ"ªýѤ‡è†;–& )J .,”¸ŒÏ³$#ó ñ$§ö 15mpƒ\#Åÿx{¬XXက€€Ô;šR¬Bl¯R+5…ž+%¼8%¶:©¹±‘š[í²R”…eÅ~­ÎòC)">N³PBºËÓ”d B%…^© ‘UĘ|y·Ri"j­ˆz>Q¼˜·éÖÅ^!Å‚¨è?í¡ò )IrzŸS,žØ ʼn·úÓŸ¦[¾Õ@ž—üåŽü    µE ¦…”í‘bÔ <=J wNà­RÂÉF¼ÝH®X ËŠÎpöµ ¡?†Î—°/¥O«å‘ºoìN)¤’*D”ðV± Â?¨W5$¤"OOè92=M1ÏQ$ Xpiñ~¶ò³'ËðB™½ËgN14ʹÇϳ¶ªU­•RSùôaåô>•D–’þÝûl§R::Ÿ9O‹¨Å;ˆ&,žˆ     POjCHÕ1´@@@@@ ß€ê÷&     E @H%†ü     ýž„T¿7(JBª(1äè÷ ¤ú½ €€€€€@Qµ!¤dèróQÁi!ËÍäæ==a®B¡[ï›Rï¦ÇjljÇ,³nosìí¥µ?í<ƒúÃ÷e…YÕ¹éçEË1j0ÃÄ;ùä{»2%êjjoSÿø}^žpêÞðÚ&d~#¬=WeîKkƒu<§¯+Q‡~×™¯Iûr2|-0Î{Ô$Ò`[r}³/íÐö¥†Ý/Ù¬rL»vø\J¹^ÓÌ÷˜J°ðÕ£ßmç·yõñðâȨP&>~xŸH» ÕZbæÔª—Xg®kÀ¬Ûø](ç=]–Ù%Þç*NÅ® çoôI`÷Æï°u•°ÿêÜzº{q<ú&PCBJü蛃n~ŸS£ØVI±S‘ó´®øÀ6Ÿà(`D|B6¹£?5PŠ ¬u«¹õRcßx®c¹/TŽ·#™M0 Ö§håsÞf½@Ù<„“¯âu¤²ò¼<:a ”Ä &¤ó&Ñï–¸r_4CÌ+/>ŠXTþ¼×Næ¹æ?R”³RƒÐìzŠ NK9§Œ9øö½¼¼ê±H3+rïM¸‡÷Š Ù=`+ÒöÈk‰b«=Ù¶W¬ùEêóÝã“î}U¸UåžPŒrƒ€@ )(Ú/Ôå£-¤^–+ûÑØÇB#æ‘2ËŠã´²Ìd–Pw®«ùÃäþ˜˜ßY8 ïMøäØñÂècñ~>¿ØÓ=æ‰ëÄf^Y¾Yz2b¥Ø“çGU КSŸäG,…{,Ù®Öà,;EyWT«¾Š (Ì£QGÜ£±N­ÃèÓ´:„»Ôág°ÊÃQš¬[‡qp“‡mÂV­mÇ6u<œú$[_3î5 ¾{ìÄò°86ÕØÜØ?…vD£U.±Á¾–b*¯UU·*ŸhßòLòu•‹ßŠ\3iõÄV<çæ ÑæVöB†OñMo¢-†óðLµ ¯¨q„¬û‚sËVsÞ+Íû«of€²Ûs—pßóýVK†|/L»~ä}ư+CŒYìŒ~ÊÝ΄zƒS0ÛÃ÷aó|³¸úŠ×ÿvZ y{1¸¤Ü ’mÙlË8áŸxïsóÇÜóÃnMƒ ƒ)u{X?J ƒ÷ÇÌÆôƤ'Áv´?ªò‡¨9®0¹Ylœöǽ;Ž Û›öÔ2z"™<˜+òÔ“Ç;Ñ4·X9ŸJšžŸJÕa‹Äèœ29†ª”A£%,Eß6wÂ+º@R¦û¨Žu„WÌ\Sìѵ×sfž£kó1{K¸–Üö¤];fŸ¥Ù·d”pÙç Sæ`Þyi×LZ=ÞÛ¯o+¯YѧJP7²÷]}×BÓ#kíËä™þô?þ€ h†yÏInƒãqð܇½÷Ñð~ëñX˜‚+çõLkåë'mzkÚ½ÛsoÓípû†ûÅŸ‰íL©×™Šë^cÉ¿oÚL|‚QÙ±!J¾óp 4Í–­ö$ü^º÷Ò´{Ÿ•×h[©÷Ø=!çovòyÊ”[¶à n¢}žs ÛçYë ¸„ri¬½×NúzŠ˜è1êl-ÞÉ'¡-É×L±z´PrcöŸÛw0}ÏÉ3éú Ÿâg?è‘÷—f-ÜÓî‡Åö±×<éD‘Ÿäè^š"¤ Ý»ÍßñY Ê£ûlŽû¾÷œûfl„î‹”ß7U¯ÿášÛ.ó»ï<ÌFfزº“§‹»÷øô{ŸÙþÈþK½˜åÒ¯‹˜¸Ë<¯"–ˆ¼ ý@m )|Ë)]âÆÈœD’¨çXÈšG<9S1*¶HVÕ=,´…•=˜²7þrôãþÀ‹c„ÞŠðÇß8óùmHz„X4uÊey:Å“à1): qJö”§øÀ̾¤å 5¶>©Ì:ÒX¥rÔmËÁ@ó3§À˜Á<>¬iCñ[ïi²×N|OßÃmÑ`-<‚¶CY²D«¶ŸÐníA¦O F×XÂuä9W,¹A!ò²ˆ?…·YóudŸ²y=¹×dÊ÷¼<3½:Iö§9«¿¡-»bÀ½¿˜´Ò÷IϹk»Îý7Ó<¬{K†Êp`Û’~Ø¢ûÀ±ííôÖ»'æ¡™BÊœòç³-Ï5ªÌ8}á³e×cZ·Ï£í¬É´š…¿Žrí!×ýðÉÔë"ù7/ñ¼úû¨ç Pˆ@ )ž½< •OÁzÍ#e0ÌûT3Á#d?w×£ä´(úñЃ3-¨Â§}±A”1hJó$y½rJ€åÀ™¦—$¶òˆ0Ç„ýOb•XKŒ,¨ÄRÖ¨LžwÒ»„:ÒX¥í u”4Âwɪ¾jvÖ rô®ÖØÀÜWû„ÛÍ“òܵ“4Ⱪp0–9 GˆÞ 2áµc#;ë#º^T߯Êð"×LZ=Þ[¯o çx?b^Š”uk™:í!FÔÀ$¦Ü.ÄN°~Eµ=ôLék.!€IÂ=PÕØ—x=;ù’~Éô´CWøÆ<'©÷nîg ‘ãµ–ktõôK·1‰çšRof{’~‚ƒ÷HeÏ#Å–}3$b,Ü{yÖ½Ï|¤Y—zÿH½'8¿ßÆ5“ë¼’ ÛA@ÀC æ„T°ÆÁ?eÏzZfýƒ";Ôok¤¬„”º- N>¹ÏøQõÍJçŒ?…—CÙO™b‹Ô;Ԣ'M(ùúBµÌN%Öá[ë6??öTO•ßÓšm5d¬¦¡ëûšy/Qb1š†bïÖ=*o>õû˜q^)‚€@µ'¤|#a<‰tŸŠ?(ÁBs_Ô>ý£¢ÊyŸFÓRìÁ~BÝ1ÒF>Õ–¨s:™¹þ@ÝÜÍæñæÄ~ £A€9­Ä|_Vê“àð‡Îy-:Õ,O‡L¾sBYç|í'Îv=Ö¾$f’M£ðÇÚŽëÊŒ:Rßee– â<ü"[·ìPÕkmK³Ï;»¢@(ÉöèµË+]{Á‹hÞÎu™P.~“J¹vtÆCí°Þç²H±+YÎè›"׌iƒn=^;òL]²ú¤\û±¾ÈÉÓt§]SV›Ý·3ÐUCz£¿“£¹ESŠ{¯¾×x§"»b&+|ÖÙä{·Å$ýëµmO×K“^oÚo‰y ø~ß’¢ö‰ÀLaÄG7Oü<ÌþN³eë|cO¿¨å¹÷ ›Òö_òýù'„Afœ±ï^–~^E†OÈ  µl= P‡²±uxJ}¥Éž)Sõxja •žn<óËš\n› ôQæC°¤¶ôÄyä‰|Ü? ¢Cv$P©ƒ&dÈë]˪û«A äÁw5SRb -^õÐÓÿ/MÊ« JnëuÍyŒ‚ËlZõΣDÚ‹zhqÿ(‘$ŠÔ©ê 4@@@@@ >@HÕG?¡•     5DBª†:M¨RõÑOh%€€€€€@ ¨ !•j:5z’…ßK’£N3K¹í“u9¡¨;}mÈ~_Š?ÈTZ9çýY§^e®Åî%.¤Îj*öƒ€€€€@5 ÔÊz_ë ~?PŽ|IB§$+ ¤¬w¾Ä^ž4Z¿ïÃ'–Jݼ08g䨠ùùH¹W5úu‚€€€€@‰j_H•ü‚^E$õÅ»†(Sÿfñ‚Qý²Ïä—ò6‹—5²q^ ¨ŽÕä«#Q„ä+Û&^2Üè¾üR‰›RöI«î”:SŒêÏ‘ Š ©þ>ï Œþi“¦`ì ³Ç1@@@@@ <u.¤ržHõ,𲄔äÂð°”ë‘âip–H‹êÖA$láxëJÝg›KšG*7MMjZ_à‹‚M¤yÛbâÐ1ROYx¤Ê»Q@@@@ g Ô¯R!ýSM†iëzr‹ "k¤RÖ%ô­µ>(Q„T+üy4RŠ3SÔI>Æš­¼,Ý5R®‡Ê#¤°Fªg/| @@@@ <u,¤øÄͨqžˆv®E²J(“9-Ï('¼4QØpý(!  yµÌŽKŽ~媖j¢fP2jŸëáãcšÛŒsMcÉ”EäB3ê!£™‡¿›,hQ›X€Çw~çRRÔ>Ï{ ÌÈ{ö{¯‚5Unr)ˆÔ?oÔ>±ÏŒÌÇBÌô^±3§ú¢öåyWÖG•vÁ €€€€0ÚRµÒ=üÒÜZ9o´@@@@@ )„T!ãAfè¯ ¤úkÏã¼A@@@@J&!U2:è¯ ¤úkÏã¼A@@@@J&!U2:è¯ ¤úkÏã¼A@@@@J&!U2:è¯ ¤úkÏã¼A@@@@J&!U2:è¯ ¤úkÏã¼A@@@@J&!U2:è¯ ¤úkÏã¼A@@@@J&!U2:è¯ ¤úkÏã¼A@@@@J&PBª»»›À6€ À`°Øl6¨ÈRXRq±°Øl6€ À`°Ø€cR¸(pQÀ`°Øl6€ À`mBª °Zq%¢pkÃ`°Øl6€ ÀzÏêHHM¢A Ô Ó>| ¾†º' RÛѤî(Ï I êšá4 ,7H–¾¦tà“ññùXÅëÊ–^Jqæ`f°Øl6€ À`Õ°:R@rÅ‘RBÐL$„•G,œMR®I.gì“¢Ëø^‚âã PšÒXnùjêÄ 6€ À`°Øl6ßêCH ¤E“¯s…TF9®kÍðÒKŠ´⊅Рлd 3]_è9óx¿\!5|€ðR .¡öXk >¿ÁƒXÁ`°Øl6€ TÂêBH±8I:¦‘Fy¤²Ê•.¤¢cÈ©…ÚÆÞ0!ˆÂŽq¿+‘÷H©)‰Iž´â®Æ€:pS À`°Øl6€ ä³>"¤üSûL!e‰­P°äƒä“-„„RâÉöF빯”_H9ÓsxÓ`ä¥õ¸l6€ À`°Ø@¹6PBн>•šÚgM,ÑÓ“$¤²Ú©;Ë+¤LO· B !8K´Ïro (Øl6€ À`Ù6PBJx°×"±‡IO÷K6a{z\!Uê©(j_ä‘ê–Q³Q$Mí3Åb%.€ì ŒÀ6€ À`°Øl ¨!Õm…1·×AE!Å£ðç¡( ã«©vδ¾¢B*œ"(=HFHv]¯{¼PX™áÛÝi“A't¨ö2§–b (ƒ›l6€ À`°Øl Ÿ ÔêÓÓœL¯V¾Žƒƒl6€ À`°Øl ÷lBªšé‰*†Nï]8`ö°Øl6€ Àú· @HÕ€ÂEØ¿/Bô?ú6€ À`°Ø@ýÙ@]©¬Fb?€€€€€Ô={»éP÷ :tä$u‹täè»tôØ{tâÄûtòÝèý÷? S§NÓéÃ?¦Ó§&.?pà`÷Êt:ýÑÇòsÿ¿ðFþŒ      З@Hõ¥ÞĹ€€€€€ô©ÁŒƒ€€€€€ô%R}©7q.     =BBªG0ã     }‰„T_êMœ €€€€€@€êÌ8€€€€€@_"!Õ—zç     Ð# ¤z3     З@Hõ¥ÞĹ€€€€€ô©ÁŒƒ€€€€€ô%5!¤º»» `°Øl6€ À`°Z±,ÑWB*«‘Ø     µDBª–zm¨ RuÑMh$€€€€€@-€ª¥Þ@[@@@@@ê‚„T]t      PK ¤j©7к !UÝoä ËNSó‚è©9ïÑ3OÒ¨iÇhä”#4bâ!öF]ÐM›¶ï/ùìînh N "က€€€€MBªN-â!¢ô¿ßü†èôÇDïDtâ4Ñ‘‰þ¿öÎ,ÊŠã¼ã÷I9ÎqŽNÞý<8~ȉsN,ËŽAŠ£œ±ås/'–eÉ–æ-!¡!@00HAbd#ÀK,B4ˆe€a‡`†mX%Y^b)Z¾ÔWÝÕ]]]UÝ}™îþ—Sܹ÷v×ò«¯«ëß_-/¼ÙM+Û>¦GËS,¢„Š‚ªRû@¶A@@@z—„TïòíµØçO¿l"êÒÇD“^>NõïM[õ1í>T\L]®ºûî»éú믗BŒßù3^     Ð_@HUiMòp>~™ž(Qç…Æz¬¡ÆL9(ÃC“[©¥¥¥PI/GH3†Vv}üñÇtñâEù7þ/è ¤ª´yN”Ñ—ΧDT·ÐX»þ‘Öl>'Ãø†Vš7o^®’ª¹Qºb¯RÞ¹RìyR"Šd1¥„TC;Á3•«p€€€€@¥€ªôrä–`!¥æDé"êÔ‰N|DtüC¢·6Ÿ§G¦í§ùóçç.©Q'ŠÉ{¼Pþ9Rù„ò8ÙVêãIQúª~å¤Ðø†V¹ÙîÓ÷И§vÑ(±:߃ba‰ûÇn¡‘b8{¢îº÷]2t…SHåõFéÇÙš`!Åâ‰ß9°'j€Q×^{­µˆJLÍŽÅ'ʵœ    påT°R‚G[àA,¯Úg¬š'Å‘{5¾Äp9Ïq¼yje@S|Ù>«:l²ïéÁð÷rWëSÉtttмyór>ÞöJîÛe™§fÙßË/.ñ·B8©ÀŠÃ5×\ãµnÚÇÃñj"PBªšˆ!¯     W=©«Þ@@@@@ (©¢Äp<€€€€€ÀUOBêª7(JBª(1     pÕ€ºêM@@@@@Ѝ !%—.×÷Œ Š,Y®o›·x¼„y¸zb¿©po*‘VM]Hó2ã¶f'¹D{yù÷•3ˆ?Ú/+:4,›Ú8/ªÔq1£’¾L¼qœÜ·+3-W]#o¥*TÔgz‰uëRðÊ&äñÚ²öSâ7c|=Ÿ¾8Ô>e¶øõ8x²(Ϧ½øX9~Kij0H¾‰/}ÔdƒïÜKéëyHr¿Üå÷Ë6/k‡ËRÎõêÏ)·1=ÁÂÚÛÎnóá&âÑÅÑóD9ý¨ðÚqϧnV¸ïùú˾s|_èƒ"÷XÜŽ9ÛZí^×C »&Œ6^k—»×îÉ2ô„ý÷N›ÐC €ÀUB ‚„”¸éënÞÏ©F|WÖÍ×qs16ñ-¯Ž}7®tÇ6Ÿà(.ƒufï/ì(¥V¸ÃŽybScÛ /ݹf|ñ¤o+–²™BG义Euô_÷&&Åpž^Û°ŠCÈšœ'l5g[©‹xÛÈ€ÐN’ž»¼xy O ·…¾ëÀq_íŒfWšK°Óê)w>½s=?ÜëåÍâj{(\#|ïLx€øŽ¨·ž¶ÀmËz~LÆŽ6ÞÙö(ù@0lïÅ9…ÛqóºHÝó|åB§@Ѝ(!ÅÅØ Q—;Üà'†ªD¶d' hP-Cûô›¹åÆ^8n ëà¦äè ˜73ý³PÚMÁ&B‚á:ÜiÕnªòFÔ‰GÓ›¡sK°1òŸà‰Ó¤¸ †˜øžZÆO$ݹ"O=C¯Öwvè=¶¯3É,§#Ý{ä‹#_ü ¢PÔmc'÷Œ4±éîÀ^©byìÑ´Ós¦—Ñ´ù”½9®S3?¾kG·%Ÿ}KFÚ0a}x[Ú³‹¾"׌/«éØ:±òšu*õ¬ø›½ïágeߺG6ñ[&OÿÓÿ”·3ÌtŠ•áò`xgÏoQ{kñXè‚+·—AuÌ}Ã[=÷³ 3È$ê†ËmŠOg>ëÚrŸIxÚµ{…ûþ¦ÌÄ&CQ ‰’èA†icÊÞ4#õÙr"?Žû¥ÙÆgµ‹Q3®å­Üö#Õ&äé„31ê¢j¼öÅ:w8@ oT–ŠžR…Oà·Í›c›%{&÷4/”STñùÚÓ³ÔÓÛ" ò*%o|zG%ê@é'S,XžÞ«›G“æ½kª&ëSæø¬né24‡÷Éæ Êôt(†öae®Ž\ÊäN±Œ51Î6oȈ%GV9žŒG•ÇG¾ø3h5Y¿a‡[yí¼‡° ‹67Mï”'ĺѹ‰³”)ÊŸe®C$àçùÜhÖkÇ?Ÿ"%ܬ×Q:/’#/¦Pô±ðÅ£®s3½þÌóÍk4þœ“§ëúŒžâg?è‘íK£î¾ö°Øoì5O> ÊÑžZ.ɸ-õ©Bm·~ ½š²Ó­X•—Ïà^^¿Þüxîo¡#1ó¥¶•C‡™qM„×¢{¸¸9GÉßöé÷¿ØþËm?ôóü×Eêž—Y®¾é|!þA ²„ 9¤K4ŒÜÁ)"v\Yóˆ'c(FM’ ã&…U²3•ìÜØ;ÊñÍ#ºÁ‹4"oEÈÀÖòàéZ„XÜÙ7ÎËòtŠÇ1Aºh‡Dï(ùnº¾ R‹Ã[N{’§n_¾8rÅŸƒâ=\ЙÆ$w©ýâ#Ðe.{´Ô·9‡,R–¼(ûÐDQbÁ…<ós×N²“é¶o½ã‰Êhñ ˜bÉ\ÂzÍdÄc«¾Ž’EÖùšõäùœ—g¦WÇeŠsøÙ²)ÌcôZþߤçÜ´%£ýÍ4DÛ’!¤28HÚ’zP¢êÀ°íù´Æ›jsŠÐL!¥?°²Ù–£½Èi˦Ç(²q³…—ßJa’¹@Bj’zÜ¡HØ€¬'Ë0%ó†ž8®H9mȨ̈Ëan˜˜‘z*žcÉmiB$šž0ž\ŸXˆÃÑùD³êØs ô¹ ñhr–×€fSZ}ó{C ˽vô›Ï¾}בQ†ÄÓè"׌/kUX­žÏ¬k?‘7£Óìäi¤éŸF¦Ã‡&®ùZ©9RyÛÑÔqáukæ-‡JæØ×¦ä¼/DBϘª·-yóY´íé¡9Rö9tJÀz†réG÷^ý—·•t[h÷´ºV[ÕÄ´)‚Êi?"Îs]˜b/G¹úG×¥è+•'¤œb‡‘Oƒ§‘æüí7ëª}ZCÝ8Í'€®}}q§jIÏ_W,Bï†Z•KÊkkìmjC?.%’bC=±Õ÷ËÊôÞÈ8í+¯ÅEÍòt(¡a»y;Î5ÊtÆU]˜ñ茵ߊđ§œú1¶¼øâðÆŸ‡_lë 3ãM|gÚŽª¨°Cœ~r¯˜¹íÑùôÙrí,^èÄq]&òá[ŠÝsí¨8,+ö%öƒË¸Žt»’çi"UŠ•uÍøâI5 6AËA´…&RKÚs¼Ìylî}Í´«Ô¶é¿¦´\ûDstX²®’M•ã7›À°E6ÅLÖòýYgÜmw‚Ijõ?Ž7™vr¸ž{uA¼¾{IÖý͵jŸX˜)j3m{ݹæ½&Òs Ôò´}šý—Ý~mBr`\^[[–¨ÇÌq¤}ÕC: ÕJ 2„TµÒC¾Aàª&Õ‰½ªá\ÙÂgy$¯lîr§-´’ûŒ:ùõv'»@e>s»/ÊQy<¬íGAt8@  @HU`¥ K Pòzת£4ý-—ew¾+„èh‹­úúxi ìg•;ƒ¦×5g—ž•Þ+Gî'T^¤ÈŒö£L’8 @ ‚@HUPe +     ÕABª:ê ¹¨ RTÈ €€€€€@u€ªŽzB.A@@@@*ˆ@e)ßRÓÞåÐs,¼/IŽ8õC.72.c)êN[²÷KI-‹í\*^Åonæ˜Qö‚,sMv/s"uÁZÂá     У>xÿ ê8B3~M«V½K+WrXŸ +Âï¢÷ëiZ}£q€€€€ô%¤ÊMà ©`¨[Mcgw‡'G¸r„PtÏ9áðB{|Éau‰ý9RB/GžtÚ)gŠ µƒ¼mhŸï·8¿GJ¥˜t†ÚîóÌVrÉË’ qh 9K]™Â«\ƒÄy     Ðt!õégŸ‰yù篅Tì b¯E$h\©”—E‰ ›×£¥cÆ—é w«"ËIDATï¸ýìsùw‰ÿãü%ÿù²‰žP\YWßöm¤««4!• ¡g¨ÓR(ª”`ûãÆÂJ(;±ç®ØÞ/¼XH]zÿO4uÚ‹ôò¯— ÏL}‘Nu¿Ÿ-¤æ4Î¥™³fÓŒ†YQ¨ŸþœH´ž™: `°Øl6€ À`°~i¬yXûèZhÖó/k¤LTëVjiÙA[¶4GaãÆM´aÃ{ÔÔ´ÁÖ¯o¢8¼+þ~—Ö­K†µkד o¿½Žò„5â¸(¬YKkDxë­whõjX%~GØl6€ À`°Øl@ÙÀÊUo“V¬\C*ðo|ì[kÖGaÝú´ióöl!ÕÕu’::ŽQ{{G>Bmm‡e8x°M„CÔÚš $[Ø¿ÿ éaß¾V2ÃÞ½­¤‡Ý»÷Óž="ì=»Ä÷vîÚç ;vîkÀ#€l6€ À`°~a[WÉÑ[ý¢,è£&뱯êVK§eÇjÙ±7vìÜGÛ[öÈÀ¿ógvíZe_[¶:þ;wžº»ÏFáÌ™n:s欺éôé8œ:u†|áäÉ3ÄáĉӉÐÕušôÐÙuŠdèL†ãÇO‡c¡ãè BØl6€ À`ýÇZšWK!…:í?uªê2oÝnܵ•¦¾^O#_½‡/ºƒî˜w y©–ž˜û½»uS¦m¨tÚ;ºHGÚ;ÅÂéÐvø8éáð>¦+[H]ºô™áâÅ÷éÂ…K…ùs…(»HgÏÚCw÷!Ö.vÞNŸ>'[:œ‘¦ýñ1zúÜXz¼³ŽêZFÐ-/ý˜Æ7>NíG»œiÅéœÇpH³<Þy†ÌpôØ)âpìøi2çHe &%Ž|ïgÏ^â)J0¥…Ó9! ÒBé”J¶pâd·K\p! £³ó´ðf…¡ëŒðn!€l6€ À`°j¶íÍ+¥ªæ2ô÷¼¯X¹–V¬ZM/¾ÔHƒk‡ÓÏo»ƒ^zée1¯h3­]·ÑYw¾ºm?ÖE.~”Nz2î‚qñ·f7 á4‡õ0M«Ž&L˜L÷Þ?šfÏ~‰_~E‹³Ù¤¯nÙÅ"êz–^¢i4ë‹§iú_ž )ï?BN=D÷ÜN?xþ»4rÙPºóºcÑ-4zæØBéôÚÕ@7›ð_ Û³x¤Ð€÷§e=Ã`°ØlàÊÙÀÖ-oöJ‰ˆ%Tõ‡ÑB%Æø8­o<`ʶlYn"ÒL?Gta˜;îõX0®—~óÍ_°€ÆŽ{B¬†·>âðêkoиñOÑüù hî‚¥c¯Ûæ½-r8{¢XD½ðÅdjø¿ÀÅÃúžèEß™ýuZøi‹ÛÐÝkEß›ðï´vó†TZ.ê;!ucíó¤¤pÚ9ƒnÆãSR9ŒôJ?AúÙ l6€ À®¸ \I!µpH‰jýºÍSn Ò%Eâï;»XèÜ@“7gôÿr™ 4MH9óP!Bjð=в¥Ké¿õ@ÊVn­}æÎ}…†?øX!!5mÙ jøp‚ÎÇž(Q<7jÒŇ…7j4ݳýWT3s}þÅçôÉgÓØÍ÷ш·ï _½v =P?ªÂ…”S7Ì š°’_º‹–òü¨Bl‰ßë‡ówAþz¸¸Ä¶@ˆßß@S·%çIaŽ„žüÁ`°Øl6`Øºé ¿GJ(ÒS¤Ï!¤ Ï•Hé´µºN.i9òb Ê¡+_\>ˆ‰|(1˜Cð©ksâÓÏȹQy“O?›8Žº½oÑH¹°ωzúñÒ%EÔé‡äܨçü -Ùõª´‹§ß¥»× ¢á«ICÞ¸nusît®ŒGJé[NÃ…hZ&ßcïÔÎé7Riøò@Hñ÷KñÔýú* aÕÒ½œ†•FйRŸXlbÛt8p:mÓVçƒBÉ›'l6€ À`°¶-ï-ÏÚ‹ µC”·("§„.²‚ë -çÈa{>#…ˆ.LÂúq @¸(V·+m}Ø`$ØÌ4³òL3ö¢"1Ê‹ÍÓÊ"“=n„Ô£ãFÓä'ÇÒêu´ç½Ñtç­ÿIßøÚ—hMC‰ÖO/Ñ?þݵtó·¿LoÏ,QÃC_M WÝòçìšxq ]7çktûªÒøãHuïöAô­gþ‰>ùôé‘zbëéºâv¼ìçô­{¿™;+ÏE”]®˜³¥Éåv‰#›Ç¬€Z´`$Ýÿà(T;Šš7Œ¤«¿$8ÿ}éo®¡ÝK4väµâó_Ó¿þk‰¿ò©¾®«nœ ußþÁôØÚ1ôçOþLã׎"êNúîœëhö{3¥Mì>ß"çF _%¼Q¼rßë?£ëG|#w:¹…ÔªWdzå-nRJ@•#¤„ Ú¦ÿLÚA÷‡gäû‘÷ÛhÄ[ñ¾A‹F?kü! {|Dît¸|G”›îê¡ãèI1”ï$µwáHû 2…T ”Œ®Ò§(•n¤ú!ŽºƒÀóœâázÃÅü'ËÜ'9ªž¶‰ÍvO6×Ó@}xŸø¾Ù"œt!ÅíêñÊw!¤ !–a°Øl6è×6ÐÒ¼:[HOŠ–c¯ D‹=‘`„Æ€IBHéË—G¢*.Üç DYp|ÐŽ–EÆT%Ðb!%ç#qhoï$:’A«Þ´±²ÃáÃÇÈÚÄ÷A8„¶0„ߟó„Öƒ„°Øl6€ Àªß¶nyS )Ôeõ×¥Y‡yëvź5tïäûéû£¾/÷‰úæ]ß ïÞýoTûè0z}Õ›™¶¡Ò9xè(q8ÐÚ5ê€Ðü½ú¼ÿ@;qÐ?g ©ÖƒGDfÂÐ*ÞUп7~?pà0 ûö·Q‘°w_ùž½‡È »÷$0€ À`°Øl6PÝ6 :Á¨Çê®G[ýõUÝêéìÚ}ô ÏâïõÏ;wµ›#µW¸Åò†={[…€q‡]»ˆŒf‡»ö“vìÜO¶Ð²cqP¿©Ï¶÷mÛ÷À`°Øl6€ À`lÛ[ö%‚² 5'Êöyë¶=ù÷‘boÔÁCíb]‡^þì êxõ;¶…„·ËâáRç^°žÅ—§´F)Ë4ôm) èå§è“Ö×èã-SèãC"˜Lù)ô=çùù8>žËáòtÄšÉR‹Ú:hê”U4îÝÅ‘iô›s醛ž¦øfú‡¹~ÖûAº÷þ‡è7·|Ÿn¹¾/½óö¼Ø2âêÀþè>ð À`°Øl6(µ °øY»f—´ðaщËáòT›KY6‹ŸSßKïì'Ó‰©#R'u,—Ãåé¬Y3±v žËáòì¾ÈR-““Ö\ý¿hßs_¢Õßÿ]zþwÿoÚ}çвÛÿ;ýáe·Òw/ k†Ï¤«ú ¡oþò¢®_m¹ŠÆ<ÜH'-Ê+ËU>¶å30 À`°Øl6¨´ °Øi_¿‹¶w,:q9\ž:§R–ÍÂçà]·Ñ{ÞM'G£¿÷_è3]Aß~v:Ý3m]ýüdúÜ÷¤¸úfêžpžSWÒ“ýŸ‡rÓJ_¨7eØl6€ À`°$6 ÅNû.Ú²yŸL[ýWõ9Í+—“'¤¬²?÷¹/QÒ¤×m—ÍÂgßm7Ѿ_ßJo¿Þy{ð]Kî»3/ñvõÇùù8>žË‰R‹—tHá3yj›HKhò´%´ðGŸ£ ×ü íyñ/iÏËC{ !õÌŸÑ'Σ×û~‰.¸óeúákè‡o®§?¿oýþ¿ÜK{_ÿ"˜ÓHÃ.”eLñËš"ÞOy/}ù·~‹þÃüô©ßº€néíO“ÿáo‰c¯ ÇS—«c(ý ¶ ãé–¯Šz¾z/½Z`=¯ÞzAÁ瘆‡—ëMÂWrüáÐTì‹i—<Öïÿ/ß:>²Þämóú²,ç‘°í©•¤\æb__®mé®=unæ5è÷Ap­ØŸ“×1yjîÚdVIÝ+ø¾4À¿þð}¥ˆôŠuSŽ-®/Àüâl õ}µÀïmÔ“ü~ V=‡ Ÿ vÓÆ{DÚë¿ò{õ™·éIíÓ÷{Û¸.OÙ]6‹˜¾7>“(±ØÒÛc”-4 ŸÝ}o¤ÝMBLÝr“|ÕÿX4ñ6•tÅùôã¸.uÒH¬™X;k¤–,Ý(EÔ´™ËhºLË©íŠÿM]ýþ'íyîÏ„ˆúíyü³´ûž?¤¥|™~ÿ·Ñg{?GÚ¼’¾6b}úЧhõ¼>tbõÏéà»çÑ¡1_e-§é3–eN{ý~úªøžw×TZ-E¹‚¡ôJ .ê:c{ämü#F¤m$<‡Ü5hsLÉU«o–²E¿?g‰}®ÄáFå0!ûÃŽSÛíë$.?ö»û\À¥\6ø;&áý åÅ}·b?l$gÊk´nÝv!¤ŠKÊk¤øÚe³ÊaOS’?Χ·'W¶§/¤êý+Úqýõ´«O#Ùž)ý³—çr¸s˜Õ/N^¦ÝäÎ[Ùc®LÅ׳S».ÝÞ"Ž ì<¼Ü(&®>ŒÚ¦®鹕×Yî|Y\™×^~›ôkOgËý” z}`_+¹Ï¦mæ_7ú5ä—ÅBj¾wÒ“Y7ïóó_þ°e§bŸ¯1ï!k׉_‡•ÿ§ƒ½íÒë"q­ûùŒë3¿v›ñŒ`ÅÛMÀ6P`±³nÝNZµzkщËáòT_Úe+!W—+Ÿ^6‘Xølþå/iÇu¿¢í}®ËK.±æÊÇÇs9\^ g|­ÄÚ)R«Vm¢…mëôðÏ{ÓŒ/ÿÚÓï3BHýO:8à3täÞ?¢­wü)zàOéè°oÓ—¿ÖHÿö;ÏÐÿ÷Ðþâ/èÀŒFúd奴àž?÷ʬòR;-×Îó=R‹¬§bÛÛ§Î{˜Þ]°ŽÆ‰)wò—ó»g³V~–‚d çŸ)þÞÔ¾u4æž‹ä çn1ˆç÷»Xø¬ù^¥ŸñàîüGè]1”û~Æeð>Q§HêØß¼³–.ò¶å’_ ©ùZyüY´‘ÈoØc%Úñ¼ÿE~»þ²lsÀHµÍ*G±ºüy¯í²ÉÆ;çE íóâÏ^,´‹sY°øº\ËéT ^öøIîëhá˜4DÎ`;tå±6G¿ýaœ´þw÷E’lGp®VÛ´üjš×ŸyÛ»ÇåÎE Úóú%’—ã¼Ç<âÙ§ßOß¼¶(ø% Öì8òؘrÝL\}½mðÏÙö/’çòóçD?ɶ_D—_Î×’°î»[âkÏcž³u¶«E f×`p­¹®yíøm7ï:Ô¯ë<Ú^‘vgcq ÷‘2éu¯Ÿ½üyvªúñëz{^´G”“wŒóûÝÏôû¯Þàú׊¬ïçžÐ_´Ð+ `%¶1†X€°ŠØ‹Õk¶Ó²e[ŠN\—§®g»l%âêråSe³æàò•êúÕ/iûõ׿%{:våãã•ZÐ&Æ-þ÷>ëÖNZ½v3-[±‘–®è¤e+ùu#½ôôKôÔÅI]×ý.í}øÓ´ÿ©ÏÒÁþŸ¦?CÿïÿCÿæÿ\Aÿæ‚[èßÿù·éü·?¢U÷ÿ… ~ny/úé.Ë[&Ê Êly”Αó…G¢M<·Š÷-Ó¶-^ÐNK‡õ–©Ÿ?·ÆË|^GK–·Ò}æ„Ôß§~ë—ôÂrQ–(o‰8I½ Y&×ÃûqÞû‰\,ä¿y'·M¶G&¿áÅ'®Pkqüò¼ãÙã$ŽWç ~±_,„Ü«üñ÷{uÝ#ĈwÞ¹òÚ–p}ÞgÏãò+z°‹6ømQmʵ9¬œ¯ÔØç’;^ž›`¦êÐë–b$`¥/Ÿ3[¾A¼jÛýö™r:ÏËç‹+H½v8Úa/çÝ5Ýl£_Æû}ìæå·U;ou.Îüã¶;ìW¾­®£¨ºÆÝÑÿªÜ0&aýºÝ·]1Ø"„µ¼Ö†Š>¿ {î—ÍG³ÉÙŒgSÓ½òÇ ¶mëT¶pÌ窮áœçØ¿nìsX©Û¶>Šûv%ß—¬ºü9;ͻ޴|mK;ɸND¹yýnå_æλ>e›À6Prc $0€ TÂXì,[¾øû36‰ñœ3¿Ë‘åù׳]¶HQeð>#Ÿ]¶¯aXHu\q…AÛ¯^)1=oÛ5×Èd‹(åâí*ççãøx.‡Ë[Êeó÷¾?~aí©µë…ËníZ-ÓVù:oþrzî'ߥ¹ü'êºÞS{Ÿø,í¿û÷©×—¿DŸú_¦ÿúÇŸ¥‹þð·éô¸^tvÍOˆÚ¯¤ûøç4úõwü²¼2eÙSûÓJH-Ýäí··¸&RK—‹ýÆç¹ôàEJ,´Ó qüÔû=φä\ÔŸ¦ò¶‡{ùÆ5bþ¢:'ó5—g-_eçñë‘Bjƒhût•Rme»ã›smnóÏKßÿÒUæ¯ìA{ٳ㗷zDN@.žç›×¹4üj>÷_Joœ×f³]Þ~öH¹ç²dž+_ƒb•Ç/C„ù\óË͵!†Ãª˜ý“½þΟëTvàj[¬½¼Ù/Ì=œ×(ïXí¼#ù¾÷h¨íò´¼¡Rp¸ûfÈíðË cbØ€-xúkh¸ÃF”M\À¢þ! „ ßó‹ß¢ îŸI‹Çô“çÀ^ªh69çº~ñ‚'¢<›³¯AÛ~¬Ï~›eýB4ç<µù×ݪµoÒ•ÁuÖ)l‘ïIzšOh×ÿòüÍÂÆu¯–y½uïW×ÉrQ¾÷Ùë»e«¸>G;¬ë“Ëo¶ l 6`â³{\.àR `±Ó¶d3ÍcÄb—#½PþøÅ.[ ¤¸z\ùTÙ<Žå¤„Tç•WÑ–_ý*H¶ˆâ}öš)=?¯„”ÒGJ×°v „Tû†­Ô¾q;mظC¾rZ·~ zt=óÅ/ÐÌ/þ[jÿÉoÓž…˜bA%Ò¾§>O‡EXtNg–}ŸNÏj¤Å}ÿœ®¿òzZ½®S”Áe‰ÔÁåŠ4ûIºÈRKVl£ ¼}Öæ¶Q×Bjùj±ßø¼ú]¬Ah /ÒúÚ”Àß´Òúà˜u´J,Œ“õXif¿K‚_çóóøõ°ZÔIë;Þ¦_ªž0.Ë8þe¯Í< –m¶öO~„ëòÖK™‘ÈÄg.Ïg ÖÝ?5ªÍ\Ž:w³]#•ÏÆ—ÊosXÙQeèmÐó©²y<ÔÞ±C ©öË!ÓÆ_\I,ˆlÅÛT²ÅoçãT\ßÇ•®‘¯B;Bªcã6Ú²m¯•öÐüÖEôÄ/ûÐ=ÿñ?ÓØ?ø÷´òŸ›6_ý»´íöÿN{ú–v=ði™:{ÿMüåçé’ ¿MïH›Äûdy{ü$Þ/xš.ö…Ô²Õ;½íö¶Ñ7Bje»Øo|^Bý{©AÐFjß6–®¹vlPvXÌ,[ý6]ã’'qۼ㮭ڲ‡æ>~©? [Ok;sÛ½öúõ°Z¼™6r=J·$ç1÷÷ oЋþyû[ŸöÄ+^]|νž¦¹z]ÂÃðâ¼ôsÙîGhj^›¼ã¼sœ!ËñêèE°@ízýZ}NËOýÖµ4L Œ%[½~ò\y{þùzyýíœWrPýÁcæÃIpPý/ÛûKo}Ë}S…-¸Ú–g/ª>fµ&×Ï,Ì\ýâswóZàÙ’~ÞQ|[·lWµ¥·\“¶rfD߈cCûM+7Ipí8ú2lŸ½°¸ãkLÚ„ šÂýã·YðZ6ÕûQÃÉÆoÓ§.zT[Ql {‘žOÿT‚T¿V|®³…zSusõç>Ûçfw–­Úuký¯î)ž`w]o{ò¯“ÈëÊ·“$×g!ý…c´ïˆvnà€ ÀÊb,vZçn ©ÓVÊ¢œÅ•Êçzår¸<¥ \eÇ•¡Ú`çË•íi>+Ä2£5?ù9­¿üJúîç>mxž6\q5ÙIÏÀùù8>žËáò M#ê`í©ÎÎ.Úµç íÚ-ÒžC^ïwì<@s¦µÒ=×ÜNßýô_Ò}ÿúßѨßþ·ôÎþ{óßÿ=ýô§hœxýúgÿ]ò_И·ÇÑæ­»reíæ²ürÛÑ%b€Ã¿–/_¿×«c‰µmlß@H­îûÏ«è©K•Xè¤Mþ±ÁÔ^S ÿz.Ë~[NÝRû.yzmh[C²LQoÛÓ—ù¿”o  ]þùªóÞã×ÃBªmuíO×+!µrgÈñf}Þ¯×üK¼_þèkµè‚—ÑS ¶Ñò¶ô¸<ŸK©ß{ì­ç4özï—ïó§A{´öiåp¾k_a…WÇ;7hlÇÊý½_óêÑ÷í“kÛ¥ƒ¨Íq¾Møí³#åyçÃiÕ×¢ÿXX}ê¼~Ôï:GÛ¬þõ˜ö¡‘¢½sHÐ/¼vå·à–ßo»qÞ—>C3ƒ¶DËöSnpMhLfæÙ¥m§ùŸsvíÛº¸~.~@Ló“v¬ÙÛqH›f>~™sjÜ%O¯¶o]ƒ~™R½«#÷y=.¦ã)n×?ýŒ/à¦×bî~óžyñ½ÈH+ÍëwD~çõ&Ú'Ê3®U¾Íâe¾®fR?ùÃv}ŽÑ®Ï¼öÙíÅg³ÿÀ< ´C l "6Àbgæìõ4aÂR™â„”Êçzår¸^ )O#å´k§@HmÙ¼:æ¥îctàÐQñžÓ1Ú-Z¾lþ]ÿ£é‚¿h¤?ùýóèþë×苟¿„.¾ð'ôðƒOÓäÉ3©kûnï¸nïX/ùïW=O—úBjEÇ¡ÜþÝ;h…@Á¶­[ät¢u[ýãχ¨s ¤·P—íL™[²ƒvõŠˆiõÔë\9¨1:»U;õW¿ž <¿¬Õ{‚vïîðDLîx³>¯MÚ~ÿ<Ìíê|ùŒsÑÚf”£qçݵZcpÈ+÷kçô‘^¦¬7ÿ|y¾ªý1œì¾Vý'êì´Û¦Ù‹9%Òï{y~ ú%‚×Á¼óe†å·mÏÝuéöÁ$gË.uo˳KÑ&Ýö  i¯WFþTT¯ë ì.g‹^ê³Êïo.wãz×î;»µþ÷!¾™I«[î‹ÈßåÝG‚ëmO®¬€^¾‘_´½‹ók×§:^åã¾w¶ÑÕnlËïK0“„6ŒGÔ¸¯Þ¸ Jo,v&O[Ccß]$SÜçãçUǨm\—§ú-mÙvݪ|®/W¶76RBjÙ÷~L+~ðSZùc!Œ~’Nï{¤=ã_{¬!µmë:~ü”LÇ8ï9ñ¶ïÓQñº}û.Z¾|LÓéõ×ÇÒðoÓkoŒ£–)sÄBôvÚ³÷=ÆÇ¼{Lë•!Êl}!µrÓQQÏûKë†|ÝR´å`åÚQIU_÷úè²*±—ªgUÁk lpÿ€ ÀJkþ¸DOðêÓÀ  6Àbg¤ÕôæèyE'.‡ËS¶[š²= ÂBjÞw~*“CÊ»”ô•SepyªÝR+‰:X;BjûöôÁ‡ч"ñ«—NËÏŠW~ïí÷·} >{y½|"Éíæ¾ ¼ÎéëÁ¢í¯ÓÐNí8½Œ’¾ŸI·mà©w©ëH%Ú:› ëïΗ¤½ð´·U[Oæl¬¤ö~‰í—ñÏÝwô{Þƒ l §Û@¥îI¨߇°¤ðé7à=j1»èÄåpyŠk)Êö´Ëi)¤Þ>ÿûR-úÞOhÑ÷–>‰ãøx.‡Ë 4¯…X;BjçŽtæìY:sF¤³çèì™sâ½Hâ=o?ËÛøUló^yçU¯|Œ—Ï;ÎÛî%ïÞ~öä~ZL·é¤Çý²øØ²¥#ÔYñ6”ó|k¸®ãû¤½¬îz¿ŒöQüÊv eÏHÞÀ6ȳ5Á«7&CƒòÙ@ókÓ1ÅB¨˜Ä‰ËSýWš²=-Ãe+1ÅB¨˜Äåxíöu¯X;BŠÝUø&°{×.)"Xé©Ïíåg~Uïí<ög.‡ËS¥,»ûàÁ@L±RéͯyïùU½×÷»Þs»¹<ûµS ¤ØÍÆ'„°Øl6€ À`°Øl ÜX;Bjæ¬âa[H`€ À`°Øl6€ ÀÂl OH ú&ÝqdzH`€ À`°Øl6€ À6ÀšÉR¼ðkô[3¤‹êÔ©#y館¦’k¿¾Í™ï¤(3"éåïEþ“*iõçöûýäï«#W¾ÈÊKz^gVÙ2Þ&×û"JŽý'Ä6N¡åœm“É]Î ±O&¿ù*òF&•74Ÿ_¦*[¼×R®l=ŸW§‘ï¸Øæ§`»ø|ÜOj¿ªmúñÁ6Þ/Ëæ|¢-ARÇå¶íT)?ߪRì6“:FßîªÓ>.ïó1QODǬäj‡qLîø£b;§\yüùHÔv•Ï{õ÷ïUÒŽ ¶û½zd ŽÉm;"¶E%g™zùGDY"Å•cìçüQǸö«mâõpÂtäH·¨ÇK±ÇåF¤nQ§L"Œ²µ2œycöÇt‹zütH“KÝâ}·Ù†nñ9HÞqœG%UŽÜîL"¯8Þܧ¶‰×C⸨$íÏã:$•ÿ 8.—DžC*éÛÕûüýD~•ûUòË9 >IÔu€ëÓòéÇ8(ösÒÊ4ŽWûWU¾¬Ü§Õy@¼·Ò~ñ9H"ÿ~™Ä6W: öqÒö©òüz™êc›V§¿}ŸxUIµGß¶o¿Ø¯’Ê«o+àý^qLQiŸ8^%UVÜ6}Ìû=bÁi¯8V%g9‡DÙ~Òóïž½*yeíŸU Ê–ù½|»e/éûƒí{Ä>•T^ñõÝ*iÇçÊÊÕ©—ŸWŽ86x:—”Ÿ«s—¨ÛNA9rŸãóþƒTùaª»v‹<œ\ùŒmZ=þñ;Åq®”{ø»_¶ªÃÝ)ŽçÔ-Ëñ¶…•™Û¯ò™ywˆc9ùv‰ÏVRùŒ¼Ž||Ü•ü²å1ZÞ`¿ž7xß-ŽWI+Ë™Wß~Ìvq¬J;vŠcTReêÛbÞoûURålßÙ-¶y)ئթãz¯·)ØŸâxW™¬•X3±v ¦öAHùBÑ9¦Hƒ‚ TR91¦Ä„”)Ò ¤¤˜‚ò… „T:a'š\â BÊSRRAH±0‚’ÂBJ‰x¤à‘‚G )ß‹•ÐÅ^"x¤|ï•îÝ‚G )—÷ ©xO# OEå³"æEû òieQÿÂ"üù‘ýÖ¢^½ÑzyŒ·Íô>¹ÊÕ"ŠÈwÇ9Ÿuϸ~ ({ ­ã}z”¾ 2 10dŸ;ÒŸ±ó¸¢ö;*Ú姣╓ގ<ÑZnï Zëç5¢þù"ް'ÊOI#ýÅy®ôýz™GÖx}³ÆP‹ÄW~¬J±v*ª¬Àó¤¢ôÙ¯ú”/å¢úïH~)öÑú´¨|zYþ{3úž•OäçŽÀ§¢÷‰¼"êžJÊ{¥GþËy´üˆ~2J_~T>c›˜ò¶ßOFd>[4i‘ü²ŒÚ·W”ËÉ(3&ê_nšÞ!1mÏ‘’FíÓ#ªrö‹òT2‚QøõQ´×•ü¼Fd?W¤>CTiÑú‚¨}ùúœeQüÜúŒ¨zZ„¿Ü:¥\„>W¤¿ z_XÔ=ŽÌç§\^-*Ÿ¨s—Ÿ\uº¢ík¨üHy»…QÉéiÒ#ù©H€V´=eo§8™BDMOÛ¡Ï‹ hGí³ëHùY|ØÉI.¨¶ÉW/ ŸBæ÷“ŠÊgFêË‘PçŸ&°D®l=…öÞŽÄÇíó·ëš´èA$?Ç6¹ÏÐgDtEýsÈH,Â,a 'n¿.œò„• H¡ETm.NH±@ih #õ¡ÉJ¸”ZHµ»êïEƒ7ä³Kñgµ§…+D^ž¸’¢D®Hâ\/+4khð%¢“ý:ÂW©…”|ŽëõPçbÀÉ%i½þëu%çûBŠEGpl……·Å²³¾-Ñc-=Tz½©Õôl/ûzó>÷zvMõ))Òš¢È± BÊŸè\!%C CHI1!ÅáÎ!¤ÂBCHå‡0‡ò„„”5Õ/BH±˜R⨼BÊ÷8¹ž¥LÉ'*<E&©(!e ¨àùRU*¤Ø“¤„’áYÒ=^=È#µÎóà¬SÞ­jòHAH™"à‘òž=ÔÐw¢7eÐðVùí’m^½½<•O`-¢Œ†‹éÙ$}èFßÓÓ·e =¼÷¦®îbÍëÚ‹ž]/¦ÛùSùžõϽEŸÚ'ËùÖ¡uÏilƒ©ùÓõôîšð ™ÚçRkuÏ`¡¦r^ÅDžº§Mí[û¬ÙƵÅNí‹R¼?h胵b ¡šÚ'íC}4Ï[Ÿ‰¹æ˜ÚçGñÃÔ>í¡½ÚxyŠŸöp_Lí³¦úajŸÑSûŒ‡øbj_²‡ïbjŸ?½¯Æ§ö¹”kj¢íÒ§ êâ¬x!•hj¨…HP^#M`-¤ìúåt=®O è§øžªROícñ#ÄîÙ“4Y ®ûNÖ¦žôÏ_y§ÔùK¡á‰'b}ûöñŽãü<­. !•×?BôèSû¤Àˉ¶I¢ “XðSûÆ{<…@âuQRx±ñELßI,šüiRäŒ÷>낇EO1BÊ8%Îü~ö§ùí’\E{[¼)}R‰6BJ¶[ ^Ï$ß³8íó~ü^®§²ÖHñvQ–R¢ŽErÍ”Rr»/b¤€áz¼uU-‚­¬3‹5RyBj"ÝÈí™è­’âèÆ‰¢.µÝ‹Ä·öYÁâÆñ9!%ÅWšèGè›x£xŸ…rÙœî7žúp}êA½JtéBJÜA,®x½ÔD¥"\(tmÖHñú(¬‘Rb B BJ†C×C¢CHIO˜áÅâµQ*ìÕ×@a”|ˆ/ÖHykµjXH¹ÖF…y¤X,%‰æW¼Jâ‘ [KÅ&üçM*$ØD˜GÊ.Q!ˆ%Ï›$=Kª}ε\"“ûzÞ©Éb=™ó±ó_‹6‘È#å‰éQ [#¥ÖU© JˆbK­¯bÑÅbìMbA)DVÑÁ&l”úlOñÓÛ%íRâ&ð^y‚F›ðËX§¼[*EP6‹J݃"X±gI÷@)!ås µ]½ú‚‚×JÙe¸Ø«¯wRùõÈt'ýå'Û½À í"8„±F+LHÙSíüc9¸D˜3ê5Q˜Ñòuœ*G›‚'K)uÏÜn>„×L1ýD¤¼ƒVÊEÎã(z^2Ä—#Ÿ^:FÀ—Û–+Óˆ°'êà¨}FÒ"쩲ÂöG›ˆŽÔçŒÎ§EåSQüä«ß&g¤>=’_XT¿˜|QÑú‚è|\†ð É/3ð,ñ>­žb¾«Ê5¢å9šëõycCwçB¨«õPzÙúñÎ÷ê9QìAr%ÿ»Ê‹$_õ|jþ±úú,=‚^N`¹ÊÑžÓ¤¢æñ«µÏónéQ÷Œòý‡ïÒÞëÇ…ÔDëó"èå8¢úÑðü”—É{Í­µRÇéûsÛò×dyǪ¨~1‘ûô¨|A›¬È}~¿ dº½OFðÇØI䧯— „¢~¼9ψ’WÂíAÔ>-ŸáOEýÓÖ#éž"ÕN3B_·ÁÏ€2…”D´¢ó%yø®ÍÏ50•Gª`!%¼57‰_½oSíÊ"¤äÔ¾\ô¾öçÅ4'žò—·f‰=GÞz¨,„”œÞÇSÊäôcjOA´„”ZOuÉsbmU)„”œvÇ'Êa0…ÏŸrx\µ×žÚç‰no‹{½ž‘îtÏ’îáÒ…#DH0M0BHqøó–ý(zÂt#GÓ“!Ó9âžXÄë«©¢…Ô1OñTCc Ÿ¶ë]§¦ö)!%ÌÏ®öÄ×Q¿bZ-˜ÔÚ¤¢…”F $×=)ůaBЧöɰèRøaÁ!å‹©¸çRþÔ>#¼yþ6)*_ Ê ¤ôé|zØs]p™ï!¤Â„Y5 )%†âžåÚ_A!%”#øƒÍÏõOó`…z¤òÖbøjŠ'j8]r“þ` )-“Ü/DL&BÊZ •R¾¨ÒÛÊí‘.ô_,„“'èN°Së£TØò’z¤”'ÊgˆªÃrjžÇG-öJ…›ÈRB4 ¢a° ™ )+ľšš—TŸJè 6‘RÇ}±ÕKˆÖàùO.!%Ä #eKAþ@€™)ï9Ržª×³BHiíî%„¥œÞW!¥¼MÚt95UOz¡¬íGJy¢Tu%ªL1•ÊžÞÇBIRV_OÔ…ä-jj_ð¼'kjŸ+º3 ¹ŠÎ—‘GʪC­qÒ_ó§ êÏ­âìæÄ[´GÊ!Är×Õ<^ús¦‚÷¹gG¹òk<°×öH£Œ„”š¨OýÓƒ@ï뱜ùØ»äå5®ë‰¢`M¿gO”JÁC|µõNÁwµ²Œi}þÔ¼˜5R©=RJ$©©}š8 ¼FZ]¼ßuLá ó8¥Rþ:­#±ÒÕ¦ö©éƒ®ˆRÁv]SòòÜ+…¦öÉéx˜ÚGðHÁ#嚈©}ÞT:Lí+xÒ½Sµ ¤"È«ìxðpqSû„`1Æ›ûå>/ˆD‰„TXýyÏ•*± k‡-¦ªQH¹Úž\*”¹)¼¢„”·F*·nªè5RBH…ñåév¹Ô3„T %–ªAH…µ‘·çB˜CH%Z+…5RÖZ¨¸5TÚt?)©˜)}X#Åkšrú°FJ¡ÈZX#U¸èª6!Åž¥8áTž©}ðHYÏŸòÈë‹¶둲DíÓ£ù!jŸAQû¤øIǫ̀¨H~¥_#åZ娿 oîZ×lÂ9MOó©²âò•`èz(!8NrÒëtms´)lšÝIQ“±†I_c<Ð×Ë'óªýÎuQ¹é{ÇE>•Üõ;ÖP Ñ Wòë2¦jõ;§æ¹¦ìéôu­‹ÒŽQkŸô©¼þI%µÝX#å sº`nê`P¦¶nÊ sžl=”l"?¼¹û½¹|æz*B…<·=QÚ¨câ½J±k¤‚çIy÷å”›Žç?(W–½_?FM7t•£oÓÊÛ5/”±ßåŠ ®Öm…=o*د…4 už yž[“äÕw¼wOéÏ€òƒH¯*Ñú”¨òó™áÍUÈrÑùPÞ\y\®žWÕ£‡27C¡;Ÿ«PçA˜sm®…:÷÷õªtHîxPl㤗 Ý S®‡,?(B–I ‰~@¼çäz¶”~ŒÊ'_]áÏ5Ï–k¿+¤ºžO…57äku QÄ"i¿¨;H†P¡ÓphõÜ1A¨r=”yèñ~hsí9RQÑþö‰º‚¤…W)öíûƒ¤‡#WÛsÛŒ°æ*”¹.€ü÷a¡Ñõã#ßž§C"€„rk¥öˆ:9éyô`j»¹M;ÞœB…7 i#Bš‹¶ÉÃïE›¬d<ëɦ§çÉ­}ÒÅTNH©òÏŒ2B¦;¨ëÓý´öª°æÁ|e¨s=T¹ú\ƒþ>&Ô¹ §®…'êÔCžëáËÅ1;­d†2÷†k› .ÎI&? ¹÷êkn„2Wùõ|zaÑ5¡WÏÑ•TÞ0ï’ …®ïwm3Ÿ¥ž'ÅÏ”òR)¼WFøs¿3$:?üW%O"Gyº´íƺ,;¿õÙ%Š‚ò5/Z\ˆô¢×H^*)#XD¬h‚’"BÊS®È~R,r ¤X$å¢öAHùâ BJNá bRÆs  ¤Ät?×3¨´mRR%öbAH¹¦õAHAHÁ#eFtLãƒG*çÕ²=TðHy^$x¤\ß…G )m*ŸíÕ‚GÊGðHqsÛÅŸá‘2=M¦—‰l«#*Ÿ½OÐÔ£EÝ ËëŒìçw\K*j •O߯‚IèÑÿ̼"à„t‚#å¹®k?KÊ?ŽÍ{Δ—׈ÊçˆÚw\<—I¥\T?ìAD䤷C­m:nDíÓCøÇèуúµRZp‰à˜\®H€ú³£ô5V®gJå¢úùý¬¨~yÑû¬PêÁ}µ¨|®‡ü‘ÑüTr­{Š~þS\¿Ø¨}®Pé!þ¢ô%ÚîŠà•/ù.É/î¡»ª qù\Q÷ôH{æñÞZ+#jžh³*CEõÓ£öéyU¤@c¿ŠÐ§GßsEð3öç"òÑø´¨~A„?-ÒÞ!­L3‚_Ôº)ýRÑìuEØ3£þ©âÕÖw@œ{Ä6/úž;Â_äv޼ç'=0„Ú¦¿š#¼¨}fòHh‘úôh{QQùô}®c8pEâä@úñŽý®H~qBJßoŠ+o?§˜PèÚ~/ã5w¬±.*ˆ°—‹š'X^Ê­rEË êçŠàgDøsDðSþô|* ƒVÞñ^%5ÏÉOæSürÇøü²‚rdt<}=”÷ÞÜŸ0JŸN\FÁÓ#ü9öëk\¡ÈõmÎýZùö³ìcuNŽ®ãã"ô¹ögë‘‚‚R¢ÆªBJ‰.)fÜùÌ(Çö°ðçRA¸tOPéâ BÊ yžÿ^)_€ùáÕ!¤¼0éa¡Î!¤XP9Ä™ÝHá¢BŠ‹RBÊk!!!!<«ÊóLùž&x¤H=£)x…’â)SìÀ#åy­à‘ò¦ú)OsZ%÷>|À¨³TÁ&ìÝAozˆ»íêwó 4¢ß¯©cåBY¿BÀHlbÈá„°Øl6ÐSl mɪDk¤^{íªTê)}ó¬Ü}gᢕ6ͯôSûÄ ý§mƒé+³î§–m‹¤RQ÷øU¼¼×Dš+BŸŠð§—sò”2*^›œðÑ£FEý›7¯•–-k¿ÞL)¨T4=]lå"ì‘ö4Ñ¢›.d “Ÿ—£ñ©t@L‘zhÞ:ÿ•Mtϼ}ôÖÆcÔ²åMÞrœF¯;Lƒ—ì§æî¡ŸMØN½¾‰-ÙEvKO¿~ã-ôÅ‘› q¥ ­\@]€iþ|QÆ|V­ZNííkiëÖÍ~Ú$^siË–NÚ¼y£È³Nä]AÝÝû1eFåsDíÓ=M®¨|Žm,¢Þô0=~k_tßm2õoêK¯û½rùOdzY¤1·ô†Â@¨è„5~X€ À µ%¤víÜK…$a¥Rº˜‚êÙB¥ÚϾ”BŠÅ–Se ¾îÐ&º¸õAú΂'hæžeô«¥CjBH-]ºXˆ‚Ò+ÅÞ©ƒ÷$S¥R,¢.µ‰Z¶¿Osw’ž§¡ËYDí'O*Ù¢ê•Õ‡èÞÖ]ôWÃ;è—“¶ÓÆ#Ñ›7,¤lµwï.êêÚê§-âu‹!¢x¤ª]ÇT¼}QBjÞ‚e4múš»`)-\²’f¶.¤3çÓ–­{ó§ë9Q_zœ^í? ½çfš(¼PóÇ¿B+g—iѤ×hœS#»‹?ÙæÜö š~ÇUÔzû5™ ©×ßx›ì4¬ùÕ`Ûò•SÄú¬™û².ô×R‡_Úa°º³ã“éè”ïÑþáM{^ø ™ö†¤"ç=~´%Ñý{ýú‰´nÝZêÜ´‘6mîï׈4!ôØzRùÓôÁLJäûÌûÒ'´‹ˆ÷§EÚýÑŽ.±ä@¼?vì´ÜFï‹ô±xwü#ZþŸª¸4©­„ ©Y³ÒÊuië¾#Ô!DÒúi£üѾmSsóÅT5©ÇÚߥ¯Í¸‹ú¯‡Æï˜Gol›Aó÷¯‘BjÎöU?µ=RìeÙ¶m³7¶KÏT|ŠÒ ©Ûfl¥'–¤Å{ß§çÚØuÀOû¥xzyå!zVlç4B¼¶ü –Gˆ«Bp‰4j]7­/PH)µaÃz!¢Ê)|9ïÓ6_Lå¦ó1;ÞÏÓüt¡Åbjýú5rÍT ¦ Röí –—ž ·ö£æûn‚i -™ö&­jçúE´cýZ·` -›öú8ê½úüÓôÞµ?¥–;®£#"…î B•ëk¥Žåž¶FŠE”ýwôØ ¹éÈ‘£Â%¼„–,m ‘è˸î.øÅý€ T‰ žãï^@GÚî îõ¯ÒÑŽ‘ôaçKôþâ«èãÿHgþÓÒ ‘·{ü7cûoÁ¢1Ôµc;-nk£¡/¾D k‹–ˆï€nZµz’óør ©¯Ž¾Ž^^>E®{Ê=\7~”ZÛTˆGêwöHœŽˆÔ!¾ïq€~rí8úìׯÓüõ"ú¾4žþêSé¦1G©uÛ1:ÅÒêô^Zùù©ÚÒ1o­KH±'ªm…øaCØ= (;í:p”¦ O•áaª´úiÛst¡ðD Û4‰ÞÝ1—Fo›E¯ !Õv`ý`áSô•iMô•éwÑWfÞíN" … 8Q¢`óç· Q4›æúižð6©Äž'Nmm ÅÃ÷BŠƒ'¬[·ZŠ©E‹æE¡(Ú!Âÿýk›iM÷‡rúÞ³mè¹%^âÏ׊ézŠýõŠˆÜ'ÒâýMÓwÒ‹BP ûŸ¢jð ñºR®õâ†Þ}š¾8<ýÔ¾Õ«WHQÉ"Чé±Hb±yòä1‘Ž‹÷æ´>Þ·oßnúàƒ“B˜î–û•—ÊSkiõꕞ)@H±ˆš2âi7ôIzåÁ&ûâÓ´|ÚhZ>ó=Ú¾n¾Rm´»c9m]5‡ÖΛD˧¿Mï éOo ¹ÂÛtYÓRúÿ3¶ '{¦ø_—týáWž¤ÿõÅ&ñý)6-µæ?þk©ŠK“Új€KHMhö&@_ŸÓÞêšMc¶·J¥„Ôâƒë¨óèvÚvbÈwÒ8á©z×J¼í" …Œ¨§…?%¢ÍyI‹ðç -#L¹ еó²·é°8ó´4N‡µ¤¶ñ+{J8d÷’¥‹hÑâùâ¦R`ÍŸ?G¡H¾n*Yu=jžqNB`<кú ñ4¡ëõ_¼žðÓ dâ"Œùe¯­§Wæ­–kº8ï/µ~ðîVzHx¨®ì¦g„¨zRžxADõ[|@©—Ú­5R¹¨|Ç'Nz›Ž‹¨Ë—/‘ÓQ[¥0bõþû'eRbЧðñ~öê½ÿþ‰ 1S}ºßŽ]´bÅ2Y—/‹*?¯*Ùûy:ß41…oªH£¹Mˆ¨ÇiÙÔ7…ˆz—ºÖΣíkÒ‘Ýéèž-´sÃ2!¦fÓZá™Z1k,½õ|zýñ»é½çÐD±®ªcÅ|?4º^nŠ¡.®â&JHÿ*­^³ŠÞ;NŒÞ òN›1šÆ¾7Aüh¸Š&Ly3¯Œ¬„”þÀ^=(„ÚÎBª¥s=»ümºtÌ-ôÓIÓäö%îh"ZÞnŽ˜Çøü$=R~ä¿Ü3¡¼|2¯ØÇQûôuSŸþÙR9=R¿ê·‘þäo_¤ƒ‘Üöñ'Gå(]Né+¶œ¤¥k×{©NÒÚ/üVP¶W¾—vj)¨KLÓ*{Ô¾ã«hÆÔUäÍ;Á_5`àH{ºwé÷¦Êi|,¤Öî8@3×vÑ´5ÛhÞ†]´dó^Z±uul^ä%kCC›ëå•lÔºtÞä;égóŸò¼PBH½¹mf^¾yýxQúÇù÷Ñ?ÍþYKÿ8ï^zaÓúþœÇK*¤X ±—„CrsÚ¢%þÌÞ'ˆ°h‘O"r_Û’…¹$ŽåõS Ì•ž«½b0_Øó¦òÅU”j|y5½³õ}zvåaºoþ>z@¤‡죟ÛL—‰}ËÖ¬¦ýûwÂg‡ðþ|c䙯ƒþzìvúʸíôý»é¾e‡èé5‡Åz5!¤ž_ZH±Ý¿ïyò„{šXD:Å‚ÉS+W.ž« Ò¥{­æÌ™%¾LVˆõfÁt¿eâWº´BŠEÔÌQÏÓ¬ÑÃèþ· /ÓZ:ùuZ6Cˆ¨Õs©kÍ|êÞÙAG÷n¡c{·Ò‘=›iÛº´rÚë"ðÄ4ê‰;é™Û®¥s Ï›;ðAj_:G¬a*\H½öº7µ/JHñ~ηzÍÈÂ…T!"'倪cÈßÒ%Cr_øögç`"Í`0/ï3tSC5ÈôšªÚËù‚í F›B4‰Û!ê4ÄjHtv‰ËNð+º£¬HÎYÖÒlÖA;³lSLY™³IÒö$y’°Lxn|ŽÞ5ÐÖ3¨;•Ç++IÚ]â<{}žÎM½>žt!sžHG¶ˆ©~âý¶géôûû¤ :´ð1:3ùB™—‰â5àÙió¦ tÿ€góòõ¶Y|/.£§‡6WTH Yñ6½×Ñ*Õƒ‹†Ó߽݇n5˜VlÛd ªŒ„Ô]»Þ?Côà‹]ô_Î{ŽÖu~DJñ´—މw§i‡'œ„·êÌÇÞPœ?Ÿ~ªÚP´:±a ýbflðÄ[&R™`̲—ùöä@H)µ¸s7µï8D[÷¥QǦ.š1giå„Ô‹ë&ÑyïÞJw,NïíôÖCñT>=îšE¯l™Jÿ2ÿaúÞÂ~ôƒEÒ÷EúžÿÊ￵àazzãúáÌ%R,ŽXHmÚÔ‘'¤XDñÍŽE .MK—-–iÙò6éÍRiáÂyÔÚ:SLO[‰˜ŠRõÌ2zo÷‡Ô$žusë^ºY„/ÿõÜ}ô7ƒ—Ó„Å«…§;E‹çD½²†þúåvú›‰»èoZDš´›þvò.úÚ¤]tÝâƒôޮ鋗§RÌŽƒpð3£XDqÚ²e“ {®„Ôœí9*Ô)CD1+ ìÕb†¹Ëáii…Ô„—ž¢Å“Þ¦·¿“ÞSôصlš˜›.¼N]«…w‰E”O*±:¼s#mlk¥&½J_Ho?÷ =Ýt=5ßs MŸßbª!õÂK#BjÚ¬ùÄiúlpED‰¹ýîGª^HM½ùoiÈÆœ°?g-¤¦ÞÜ@7Mõ꓃ɛŸñSlÿ:f» mG0`Õ„ThÊ(¤q.ñ3ÉÀ:hg–똲"ÙÒŽ$Ç$É“¤?RŸÛºöƒB’:ÂòduÅ´¡ Ýû´R…€zó+êÃîµtrá=ôáØ¯ÑîwA§Oí•ûNN¿ŠÎм|LØõ1fì;rmT’ôöØñF9a©{ï½~ñ‹+óÒ[o½m„IWáÏc=R/ü€ž\2R¦a«ÆÒÛë§Ó3K_§Ž¿GŒ®¤ûf£Ûwz‚*#!õ?~´_ §›^: ¦îM’‚‰§ú}"仢8Ð'1€ž)Þþ¡˜È–ýçÿP”’"Êð¥ŽÅz ¤²Ô@™”åR E"âãÒ-{iʪ­4_Lçk^«=Ý'èØÉŵøM›³„–­ì¬ŒºyîPºðÝÛé±ÕoÑÛ;fÓ¨­SóÒk[§Q³X/õ/ó’‰EÔDºbñ£tµH½Û¥kDºzÑ#ôdûkô£)&Rê—;ýÕx8oÈÔ>Iü€×úpêôӦΠrpÏbO‘ãAÿÊUËdZµz¹Yœx;ïç¼³gÏÂ+‰gêþûï';äõrˢžÚ÷íWÖѰ­Чî¦+¦ï‘éG·Ó϶‰)ˆûd~>NЍá+é‹Ï-§¿Ó%ÒVúÒ»]tÁøíÔ8±K„í¢ŸMÛ%¦ú¡/>Ù–ZH±xä(},žTb1ÄÛÝ$"yâIO'Åz²n)8ùXæ»bÅR™$[á¹*DHM|e0½÷Ò@õô#´sk;­Óù¶-Ÿ!„Ô\ÚÓ¹L¤•´wó*¹FЧøu­],‚N,¦-bû‚‰BL½Lï¾ØŸ:–Í¡'nù%=û››©ùÑ»‹RýŸ~É)¤¶ïÜM;w‰çƒìÙG{öî§û”XH CÙ·'4Þ¤!—ä¶]2äóóÍ?Sò„øÐòEyw”€[œØƒ8ÿ³> “Ç©·I¼×÷ Ôà—«ÿÊÞ ëµÏ¡É4†üø¸!ey®¼z|QfŸ³îí „”uÎamÈcàð¢IÁgö›Á@ñvzå|ŽÆ :Œ¿ªÃg×?ö`VæwÙŠ×?Ênò¬beõ£´½&òlÖ²®[µÏͳíûñ®:G›ÃÊTõæy;mÞWÆ|ÛÎc¤ÙŸ+X}IÎÍè³Ï¦¡çãóMržQ¢Æ:ß›ÄTê~#íA;üëZõ“}ÏJà­€ÐÚû”Rc¿BÇ^ÿ[:¾c±˜µ›Òwl…ð½õ%:0ÿ)¤>ÜÛFù2ñ1aBjÜØ×hêÄ7iךQôQ÷kôÂ3ÑÍ×|“v·^Jûg_F×_ù-z¨é_hϼ^Ô:úª!µG¤\Ú±c'õî}!¤~ó›{D§ÝF¾ÄBjÀ·!uï¼çé‚·®¦µÜ%f¹¼@÷ÎBßëvúÊóWÒ€é# R<Õ§öýÏïnA&>ÓwýáOÆKO )©š8BÿÉ÷ž;Š…Ô):(#÷mý¯¿m)ט/jjß’Ñc¨ãxÈøœEPਗ਼E{T¶DÛ…‡‹š.¤Œã"êÍD. 0.!õ̰whÖ’u´pã.9¥oÙ–}Ô%L°ˆúè£3tôØIzhૉDOñË|jß·Ç=H¾y+Xÿ¼X 5‰FlšB/ Ѥ§7·Î Ÿµ=Nßž(P,ž®Âé¶%ÒíK¥;Eº[¤;–ô£g× ŸNì—HHq0 CDñšªk¤Ø+Âëtxú§ "ulXGí"Š "ö–xƒüerÚšµ+iíºU"PÀj¹fŠÃ¡óvü~æÌiB(ÌH$¤XðèBJLQ©o YNnü€Î¿‹ÎoÙMˆô¥W;è¢9ÇÇßßÒNÿòb}÷¥EôÝ¡ é»/ŠôÂúîù¹ô¼x/Ò?õMœ6‹Ð®ÁoéÒ6)ˆvïÞ¬—òÖM 3¦JÁ©˜17fËbŠM,^¼0µGŠëã4ãõôñÇѦ%b1¡ðFuw­£½›VR·ð@yi‹Ÿ¼Ï]kЮ5´qáDšûÖó´^š˜õÖ‹Âã(Ö–‰>/Æ#uǃƒò„Ô±cÇ…]0‡÷éÃÅ4†Ó§‰ó%šÚ'EÁ`–…‰.~ô)iyhm°'Mþà|ªôÊés.!ÅÇ(ïò>çÄ¢nexíó§Èée% Á5›ŒzÃDŒV1h Xžp X¹Ú!Î#¨;‰ŠmƒYg΋f‹Ç RñµÏGã®{ÇØ+'ùùÃD_Lÿ¸„”.¢•­}îyÝö`¬•Éë›·/@òl;¤ß”g2`àjsX™¾¨uz; þ!ýi]²Ÿ¥Ý†õD9|œu=ç¦õ÷î|mQëòÞ†µÇê£@”æÄNî;TÝ'üû‡öcˆ´Õ~Çyx×VÛ¯€` >{ŸBê­¿¦³"}øú_ÓaIk_íMú^¨£3î ##¾HÇ÷zëäØoV^ûÚ4êõ×hÈ‹¯ÑñC#Ä ˆoÒ—¾ôOô­ï~ƒN¾”Þ~õ[ô¥/ÿ3ýú×—QÇÊ«BjcÇ&²S[ÛúùϯéÊ+¯ß«òòèBjë¶Ý"”ó.Ú¯~âmœ>{O#=¾h=¾xýýè_Ñ—_û™H—Ó?¼uÝ:õvºü•éÏïûº}Ô“^"mi³8–×I%Y#¥„Ôæ­bù„HÿùÊRH}þ§Ëè¿þžÚüØR0IOTà¡òß‹M,¶è´[|ú/ƒvp[6‹¤ù¸m›¶ðvÑ>Ç©ðõK]´Dˆ¨%~,(é¹ZÜ%jN¶”€ „g‰1¬ªˆÚSBjã¦]´qÓN‘Ä:¨åíô›Ç_¤…«;iîúí´|ónÚ!„ÔÉ“P÷ácô¨qt÷ýýiÜø)2¿;ye•DH­[·’®{÷ úÊÐëÄÅù(Ûþ Ý8‘†m|O¦ã©ßºQô/bÚÞÏ„ˆºV¨[…€úN.{”]þ(õé‰ÒcËúÑ ë‡ÐåãN,¤”˜ I!ÅÁ"ØÅ¢ˆÓ:‘Ö‹ˆ|Kü©|<ÀW"‰#õ±€ É Ý½\„Lœ"×JíÚµ-±RbÊö`E ©/ß7“îlŸ¾:~7}qœH£·ÑEd¾¯=ÒJ;ÿiêó« R›…HÊOcǾKW\ñ š8±Å¹?©ú“¾½è !¤®ŸÞþîŸÐÆüˆnlÉüMú­—Ðõƒî¥1ZhÁ¢e )lB ©ÿöó5R0ý}ßÍÔð™!túŒ˜Ò'þ>ô=E¼o¿ˆè·`ó.Z*¢P¬®¡¥ëDE!¦ÿICžRbJЍÄBJLé›ê¯“²=IÜéMBÈžªµ]/ÇðF©õXš°ªˆ¤è™•êBŠ£ð)Q´xézºë±hÈ«ïÒ¬ùâ¼ËÖÑËbíÔ}O4Sã÷E<*—MŸM­sÚœBJ•U!ÅBfÛ¶Nzvê(úÚkéº÷~M“w6ÓàŽ±ô|Ç;BL½+§òýT¤ë„ˆb ¨B8 éù•ÒP‘^‰ßjLW¼ó`*!•6j{UX±`bÏÈ*žª'<,¤ØÅ~)°”hj_#„ˆŸÄqìÅâi}S¦´ÈçJíÛ·3•ˆ ›%¤.{x†ðÜ¥œ¶Ÿ¾8v'}qd§R_¸„nÞ)S¡¶Žb^èZ·ëmÓʶí?@;Šç?‰´G¤´Bнo¼ŽŒ½P,¢ÔÔ¼ÎÎ) xmÔ‰Çdxt~eQrâ‡?xòt ©yóæ,¤Æ¿ØO ©í éð4±Gj;š¢I ¨Û{Óx!¢Þ¹åJ¹mÓ’ibŠßBÚݾ˜:L¢C{·ITBê[×=˜RG9¼ðDIõ9sV|Ÿ£O>ù„8_ù…”ÌÄ *{úœùYz¢¦ª þ”>µŽÉHMÕ×8ƒtMÔD ©°Á²¶f+ÒSå<Þ!¨RµA¸ê¿ÌBÉ1ÍåõÓ=/ pqgA¡XçOsEtŒìŸTBJ ðmiµ3là]¨<òíG«³–…TܹÞ"+ØJØ&m…L£t Æ$)—¸tÚ¬>•/Äöã„[÷ïí/DѰÏÓG=Ó™Ckéã_ îÁŸ§£»VËmŸïÇõ–ï/zöð1!m7EŒ+6m¥õ¨uá’Ð4uÖzvøN!Õ±a“˜ãN£G¿ºORÒs³e§ôÞ¨¤¼KÒûBzuÅKt÷¬[é–ÉߥŸ¼øOô7ý=ýè¡>4âÍ7iî|Dn÷Žã2Dê‰KR”Rì)âô™_tÊ©zw¿¶>7‹îyFDMÂIFêÿ±gjÞÊO课<ž~÷OߦÏýŸÉôè3«åöµŸýý Ü–M~â6i3oísx¤ò¦öåy’|±'¤vÍ2×ZÙå`­TÕ¨6%¤XølظC†4Wi£x?gþ ?eM˜Êk×…½‹m/ M÷ "¦‘î£9óÒ¬V1>ÔŽÓËÊNHù!Èõitì¦qs[èïž»–þqDjÙö4 íx•ú‰ùÂßYø°ôD±ˆ°ª?=¿öq¾^<U¤×ÛÍ4zããÔ{ôÆ3¤¼çI‰Ðç2åž/¥‰ëySÜ6*Ý.‡…i–Lb*—˜Â§§BD±Ç„EVTâµ>S§NBb­ Þú¤¤é¯e’ùÕó¦Ät:ñžÓI^å'•O¾Šà·>ßJ×¾»…®^p”¾øúVOH½¾™þú­-ô÷̤FЦÂ3¥Ž;Ð}ˆÞ™ÛA£ZÛi¾˜Ó¹t—û¸÷ uèÏo8D;Eó½ûB…”-ÈÔçiӦȵQü¤vA< ’½{ìuâ®=xpŸôÔq0oû!¹NêèÑÃR|­áa9ñ´>«ìÑãs ›«çIÙ¯2¢ž™ÞxòvOH‰ïîß¼’ ¯Tç’©r: )Q-¿¹žÆÜz•ܶaþÚ²d2m[6ÖÏK'Ž¢)£º…”hûq•´zßÿÓ5÷BêøñÂVßU*Á/à7‰uJêóŽ!bzŸðNÉÀÚ±ü+»Ì#¶…×›0†z°¦VƒcŠ”é­ ¦_…ÕíœÚgzŒ€z]Ö/òùSÆÓ›,¦zû‚óŽ_óÕôÉ|ï„5•Ñò²…ö>ðv }úh׊—Æóaô‘¿^-L„ZÛû±Ûi×oO9ËëÛ˜)oÁu3%Ï/7gNíó=P¡§Db·€©}IW$õH9¯­›Ú÷èçiï‹—Ï:*„Ó‡/|žŽo[$·oê'>ÿ¥|ß=íŸÃ„ÔØÉ3Ä ¬Sü¸¸’fÍ™# µÒŒÖÙFš%¶Ÿ<‰žú²SHmhÓÙ HJHq›wÐ&=mŸEú“_^H¯¯|‘z¿qýí—Ðew\NÏ¿ú2ÍÁV¬X)Xüã;EXh>–à+§í‰WN* ºýÊBŠ·mb#Òÿ€CMí!Ï?ý é·~ûeš)žõ!‰P~ü'ÆuüÇûØõ»üzk… ¾ò?ÿ>uŠéYAåuZicçv±‡ ‹n‡?Ï 6aLÅK;µO[÷äœÚ—+¯jTElH ¤„ˆÚб]L§¯ Göë÷ì+˜š!‚ƒmèÌÛ.ÊâÏü|©lÖH9„‹™£Gм%sè¢A×ÐÅÏ÷¦×Ö=B¯ë/§ôÝ"¦óõ[9€®óWô•þ¿ ¯=5½ƒÆl:HS6 ¹ÛˆùÀiÕžƒ´aÿAZ´n«˜î‘ RÓ§OÞ¸%RHyÞ¨Eòy[ü\.Rû÷ï•"JMã㩆,¤ø¹\,¤ø¸U«VHÆïY¸rþB…ÔðGnð„ÔÚù´§c1힦õsÇKÑ4áÎk¥ˆšzÿMôÎÍ¿ÛX<µ‹Ô1÷=Z9å :qt?úP¶BJx£Nœà°ïˆ…ŒKO”þW´òczˆd¹¦‚§ñ‚} ™5PÌ ­l •ø°çֻе[jŠ’ЃcäBªm–OyÊLÏ‘¤A m Òå6Úz Ö±¸ÂIÛåk½BC°;„ªlC|Èv.[kÐG–pp†ö¶ÏÑäÚSoƒµFÎ5QJÆc ŸÜÃÕNÃöôºDYF8oÕ/ab=ÏCè‰8£Nƒy˜‡Ó½v+/´¾}¹úÓ®/e\ÿ»~àÈMßËçi]_í Š‘ןæôHóZ‹˜j—THYU ahZI²yö QtX8µg•Jí[M !uFx¤ý{ÍïJ:%ŸÞ‹vÌ{•º[î¦}Bê÷Ä ñœ¨B,|ómzíí1yéwÆRó«¯ÑýO>çRííŸ#}Ò…‹(ö*uêIˆ!DzÝ…ô~?¥óoý=Ñ<„&M™)¾ÃW‰=…Љ ’üFšM+¤XÜpúý+6Kï‡9ß¾÷¬˜Úøý»ß{<Ó)ÅÕY1ñO¬V‘ë¨Æ­ý˜~çôöäyRHmøãO q*ÎG%QÞF+ulìû·;…?OTB›žÇ^)-Œzt° ?E2•>e%¤XDmØÐ%Å‹ $iä[“<1µ[x¦æ.$SÁq¢¬öÛK/¤XL8ÑM+–/¦ï½t;]øÔUô`ÛÝ4¬ã7tðF ]ÿÝøöµâ™Lül&3-»•Ilç¤{™²öHmß¾I›ØD;¶oÎKÛÅ6Nü¼#]Hñt´É“'ŠÀSÅ¡MràŸÔ e”(PH8°‡~ÜoºX¶…^Øx’¾8¢C¬“BjŒ'¤þnòºlö^º¢e3Ý5}+ Zu^Y€Ævì§©[öÓñäæE]ûhåžý´pÃvZ'nŒìÝ ›Ú&¤6‰gd°WŠ=°RÑ ù!½ü<©3¦IÅž&žÉB‰Å -`sf^š9k&½7q=9Ä|–” Þ¾¾C½JŸt!Å"ª£“Å…–„bAô·}¿Iw ìG[¦ÑÂÅ+<ÁÅ?ñç ùÇwtŠÇâØ´BŠÅ §ß½a­Iì—â´û¢Þ6“þàsÏÐïüÙ+ô;µ~ïÂVúºŒþèï[èªGÞyD&ñ·æÿHxØ«à'Q^‡:¶‰ý]¡Bª${U28þª@ ¤XølØ&ÅT{ÂÄy_y³…ú A;EôL~æç¼ùËäñëEYí]åRRôˆijÖУï Þ§+é¦q›Cïn¼W©ëÌÈz~p5 /_@©©|ÙMíSÓê‚:}V¢‡…”òF±X`ÅSÐöìÙ^°ˆ’!Ê R,4-[E—4µÐеGéå"|ù¨Íb½”qþ^}uÒú¦xàî ó÷ÒCKÐàUh¤RïuBêÍb!µc?-QG&/j§÷["Ê›B˜Kz4¿ÜûcÇ„ËÞSmOEßcñ4gÎl)®‚ þ3µÞlÁ‚yrm )OD±çj™ðhí-XH=óëÑlj0©bªÞ¶•3iÓÂZ%D‹¦×…€šþЭ4«ß¯éÕ_~ÇR“_£^¡•BD-7‚ŽîßAù63!ÅÞ¦$)ÉÔ¾‚E 4\À@,öaÊéúÎ\£ "%ÆVòù•hgX„Â2ð)¹W‚g=pËæŽ3žûíPLñ‹IœçÀ³_£-"ªpصýø%;MÒËQBjýºø*}Ò…‹¨Ž,.´$Ä ¢qïM‹éI¡¥ÒFöèøIßž;~«<6­bqÃéË?Ý Å¦¸?~p¯üãTâ¹R›?÷b9€(G%¿LU6¿nèíbÊ5µ/®¾4û÷,Î=ÔwŒ¡/M!È[rJH±ˆZß.¢sókÂÄy9½üƺº©½ôÊÛÁ±ëDYë…•|j_ ‚„b1µgO½1k }퉫éêÑ}¨uçÔ·†„TGÇz9u§ú­X±DL[ÛU*  œ£Gш ‹è’;¦Ð ÕGèÁ•Gé«âYQ-„Ôyâa»ß›¹‡nY°BêùÕéñàÝM‡h’R³·¤évÒkÓVѶ;Þ¨dBŠ=Uº˜â‡«ˆ|,ŽxÊž-¤TÐ~Ö”QS§zž(%¢ õH=yË·é£ÓRçÂI´yñ$Z;ë-ZÑ2JЦW®þ¶ŒÜ7ûÑÛ饟}]n[5q-}·™–½;œ–ˆ×Ã{·Òó÷\ž‰â/FHISºÁv6ƒÔ ްØl ºl@ ©uk7ˆ(Âé“!¤„€Ú°Qˆ g²–È»QK¦óÊØ°q‹|M/¤¼ºÎŸtIÁiË4¡$ÎG%ynfjß Ö> 1Uj!Ur€ 2! „‹¨uëÅr~M˜8¯Jã'Ï¡urÇ®eñç² )VŒaÜœñ¼£kéG¯ö¡ëF_[3)~ÞGæëìlOT"42_)1{öì áãJÏÔã:éÅ­§è's÷ !µ›~ „Ô¯ þK¯Õ!z³ó0½·µ›& !õú¢NzaÜRêØ²Eˆ†CoTr!eŠ©ÉÄbЧðqðOLå"ªPñžwo­Z9ÅÁ.´gWÓûŽˆÀŽä61àæ¢Ó¾O掣Ž9ïÒÊ–WiÙøR4½|õ·¤7j΀;©ùçÿä © £hÉØaAâmoÿ^fB ”ê  ?аØ@µÛ€Rk׈Dzt!Å"ª½ƒÅ…–„bAÄûìÔ!=:^2öùÇ·‹pì,¢Ú–¬Ll‚Å—ríhŸ½´Å™T^c¿¼¶«¯Êáïf± „T&2¤ö QBŠEÔÚu›¥˜b”$q^•Ö¶‹c8ùÇ®e±ˆZ¸Ø»8zŸz÷¾F¿5ƒÎœ='¤}5ðü7ò{ç”;}Jœöü&;/¡X°tõzîzúÊ3¿ ¢é¹ÊÔ#SïT´>WzÔ<ã9RþT=ç³¥ØkæEösÕ9ELå›6m’õÝ)¼7TBEÙs•µO‹Ðç 3jŸšò—‹à§Gí;Éë±äqÞ;Só-•k¦.½w&Ý6z= XrˆYwœžZ{ŒžYw˜^Þx˜†-ÛIOMn§»‡Í§a–‹È;›e€ O¼¸¦î¹¦ö…M÷Ï]emÚÔ.×9±çŽ…’ž”Š_9±È2E‹¥ÜÚ¨¼fKŠ%±]¥˜ýOÜüuúP<ô÷ô"BžH¹ðn‘k£^øñ¥4ôÇ—Ñþ½±Oå;$„ÔS·}S)áióÓqy“]µ¯Ú¿¬Ñ> (a°Ø@õÙ€R,ˆ Mòa¹MÏ™ºÅön1vÈ¥Ý"ÊLb{Äþ Ÿ*Gäikó‹E` ƒ.“È+“ÅÏ‹äwˆ¼¨}¹¨~A>™_Dü“I;>ئöÙ¯ ó:Ÿ׾,À¤%à ©nCä좇 !grˆ¢@$iÇ,\ä]üZv!¥‚P¬Zµ”¾ß|GM©Y³¦iA%rBÈýàßò )éÓüÖ‰ê¾'âàß2x6ýã=Sé¢;&‰i“é3…¾}ßTºnàl>q)­XÛN;wlÕDT6BŠ™S^4ÃɱɛΗ »^¬zòæ bÒâxNi…_LH`€ À`=ÁXL"*FH±˜ªDê ý€s¬ìý&LD±°*©GJ÷Wÿañ9u:,Žá¤«¶%xíy8éÇ«mú«¬ÃO®ý±ÛºE=vòëæcù)M9‡DyÑ©[ìç”ËwP|VÉuìÁCb¿Ÿ¼cEŸƒä—e”sH”遼âU&­½Ì`¿Ê'_suºöû9éùˆã‚tPìé ž´ü¼/YeôÒ~q §°ãÔþýâyÉ“(÷'ýµÍÞîåÙ·?—Ôqûx»Ÿ‚mZ>}Oìßï'½ÌbÞïå9Ó>±]¤}ZŠÊÇysû‰÷^Ú#ÊWIæ1òùŸýí{Äkdb/'=Ÿk›±_´AxoäqI÷¹ò™ž'åU*GJy¯’©ÀÓå{·Êé‘â‡òzIx±Tò½_;Åkľ~ ¼dÚ1;E*©öëÛ²{ŸkÇNáùI«[¿Cìã[÷.Q¦ŸÔ1a¯IóÅ•¿¿[´ÝO¾wÈ8FmãW?Ÿá9Ò/J^ýMíÓsÝT~ä½  Ð\7åˆÊgx—Ôº'Ý;¥Õ£òÑÿ´2ýµO®uOÜŽØ5Gjm•cí’¾Ë,G[û¬ÇÒÄQà5ò×E©õQÖ«1UNÔ¿ÃJÁº*9­NMŸ‹›f§ïצÜùÇ-U_P6çw”o¬qòöëkœÌcòëÔ×6yµ2õNªþ˜:ãÖHémr®·òë1÷q4@?©sÔÖo¶F*DHéáÄÕûS,’dÊ $#ŸC 8ã;DQX™n¡”+‘ë®t¡$ÚÍ"O&¿~C(iyõíI߇=sÊÞ.μs2ês¬‡2 ìw…AÛ¦?óÉ[‡¤¯Rž(s]”.¤¼÷Fxs×”+Q”@A íS(ySòŒ`N1”›º— *Á%¼äœè ")Šü|Þ«ŒÂ%®’Ÿà€1Á&td˜Á(‚tX¼÷Ó~ñ$/X… 4Á¯F0 =H…ÿÞ%ªâ„–!”T° ­l§Ð M*¯.~Jq¼&{…xRÉ5®.¡å `"ÈTà‰0Q¤¶›BÉ]* z^(t?<º¾ß•W™®íWbIRQÛÂÅ•Khå“0L8„–€Â!ÔL¡•;^¥ÐƒU¨ºŒc´2ƒctñ¥‹$‡(szœÇä“)Õv]\Å -_(™ùÒ1?¯CÔ˜Á$òƒ@èçaˆ_ ¨a¯aÁ(”ðÑ ¶i"Ð9MO€¾@r•6Å/S!%€â¼G†( "ayŠâÉ ¡SÂ#¤RNä¹½fnc=O*Ä£dŠžü¨¦èñ‰)t4‘<Ä7—Ç)št1¥‰g° •×CÑ£?ÇIË%štax§A”5æ~ï9R¦8Ë=g*(WkSîyS®ìò“îQÒD—+Â^àÙÒ…Ž}/ð=2ŸåÒ#ø)¤{¤bDQNHå‡èqXx¡TŠó>¹ö;ѽCN”L!¢)Ì‹åˆÄçy£ÌrXlÉå•ryšÒx¯”ÀÒÑD—C†xsìO%š´h{AÔ==ªŸ/š<¯”ŸÑ"†ÔñV”½œò£ëéû•¸2VNˆ^,;2ŸÿY SHå‹&Ãcå -Ó ¥ã 9]<±˜ RÝ/'ÄœJ[†hrEð‹‰¨¢î¹ÄUXT¾@\™á£Ê ‹ú#´B§÷±H ó4¹ÖW¹¼S1Ûb…–ãxgÔ>]ô8Å“æqrЦÜ~%€Â¢ü%Ýo°ðÊwŠ+ì"? kÊáq {&U ¶|/V˜÷(ö9SJTå %#*Ÿ¡/'°4Ï–Ë㥷)F©è€á^¨¸èzÊc—Oßïê½Ê÷H9£þiÇæÅJ&´ T¬Ðòžóö¬']t9…’ð˜mç¤G÷Ó=Z®çG9òº#jmsf!Aœ ØnÛ¶3ÔÙqœ:Ú»iÃúCE§µ«vRÚ¤×ËíØ¸áˆl·Ín/„nœ™Þ8ñ‹Fa¿h€¸Á`°Ø@=Ù ñQØFl›gÁÒµe?Ý9ø]úÜ¡†ó›*š~ï›÷QïÇ^£íÛÑ–ÍBHÕÓM ç‚/]Øl6€ ÀªÑ ¤ê[D=3pMš4›ZZ8ÍÊKµmêýĉ³èɧš©eÊl Ç%¤ØûÃ"êû÷ޤ9«¶Vb{×~Ù–»ƒGªo4h¾a°Øl6¨7€ª¸(Y??’6lØ\Pù|%¤ØÕºrKAå—â Ö•›¥wlÇŽüûT\}Ž©˜ú€ À`°Øl • @HÅ ±kw¿B…žA”â5I<ÿÎ;W‰ÛÂmÚ³B*ÕM Þ~ÂùàOØl6€ ÀÊaR…ÊŒê?N ¡3gÏRÚÄg瑪V!µwo5©m4´¡?õ—iMYö†v˜¦\ÈǶЂB~JQÿèÞ ÔpasaõÒ6ýÕÎÞ›‹¿ÂÛÛJ½Êx® šéB®¯wkâsKuÓ/uùÅö-Ž/M¿ƒ+¸Â`%·ÿû’¿ÃŒÔHRgÒŽâòwÑ€ ¹]M4ºäâÚâ­)þ¯•𩹫ø’PBv”:uêCJ›J#¤fÑmÖ5yÙKÛ2óf)Ô¾}Õ$¤Rs]¤òo. zÇ 9ï†tá€.÷ ‚Ï!þ´=µŠioäM5!Åâ%©0*µÐ)uùUð•JX¢½\Â`°ŒlÀÿ¾4¾ï2øͤB*Íws&mÈo²RÙ þQRv”êî>NiS!uúì9ŠM[_¢K…ˆºT§\ÞmôÂeâG…ÛfÅŸ Ž:R DH‚›A¼ònŒ½G;êK-r hsê:"ÚË£È/´Â%mþØö[|K]~Úö F˜®#°{Øl Sp )"9#DxN*ï•Òî“þ.„ÊN¸T[I.!uáßýSTÙÛÓ©>Š=HªÍJôèÞ°¤BJ;Æh“í©‹\×Þüà©„JÜÍØÞŸ6¿-Œ2;Þ÷À%]»•ø‹¿8×÷/Îè_ô/l6f!Á&t¯Œã<ï;7¡À‰ùTâ&?`“õ#h¡ßÍú¹e¤[ë‘N(ïÏ(¡¶ßûÞ«<…€ ¥M®r•Z³v;éIOê½'ÚµçE¦¶gÅLªém-_Û“½´@0½è‰6.£…®iPïý2džÕU×B*'´rÓãä4À/N´ŠYoæ‘ÒÅNVB*˜Þ¥0®½ùBÊü•)ÿæh-ÿFª{uŒãCnÖ‰ó ã*/îø¸ý¹ˆJÕÁ Na°Ø@­Û€é‘r‰š`[Xd?‡PJõ˜7mÏšj&ØÒ~—ÓèÅ÷¨ãø({Î^H)Ñ•›ž%§J‡”å­*§ªèu)!µ|Åf²“.¦\ûÓ©m]ÝÆÓ¯.¦Ç¸òiû ¤ n 7Œò¢Ž5Ë«s!¥Ý”}ñ$=E1Á"…TÔz#Çú%g]Y )öXùe…y׶Ƕ·‡z¤øWµ”7~ rj}ƒöÆa°RÚ€=µ/·)øq/nJy±³.ôÙ~]žw*#”=‹CÖÑH½{ó”Âä5J#¤4Õ"Å“š¾T9õœRË–o"Wb1¶/êÜ´OLŒN#¯n óû-säKW7\Dýæðñú{¿¼9OÑù ×ÑÈuÔµ2‚0‚ýæH‰‘(!¹ÞHÞÀüé‡öÔ>cú5E1o•=…QxeôìÎ`îçnŶ7v>vŒG*nMÒöü9ã­‘Ò¦¤šz¨êWÇ[_RUM Ó±&6€ Ô  8ÖHß±jöCÈ:dû;ªÀ5RF%ãÇB{}sßÍyÓáµéŒF`‰bÖHéë—Níã5QÍb5”ÿgx¡°Fªš„T˜ˆâíY ©N)ˆl1ʼn=—JHí#\ W —ý9J°ÕµÊ›ÚWtÔ¾dÏcRN>Tx´ÿa{“òñô¼ ED?ãAÂé„T¸‡-A{‚ôénñB*.Šžº±ç~­JU/,Š`\Ä¢tQûrQ ½(~H`€ À`…Ù@TÔ>ýAóÖZ*çº#ý;О‚¹6IÔ>¯ ûnÎÿ®T嘛Ê,¤ä >/ІJfd>_Lùûµ¯tÒ*Î#••Úб’¥±t•5•öªW—ÑÃç_DÏTeðg-jßùOÑÌ„åW·ªÐÀ6.j_a7Ø*ýbªð³$êŠe…ì «ôÚ‚=à‡ Øl ­ Ôàwr!Ás¤J'd*]r¹„ÔÚ5»©„”ã&!…)Ä l6€ ÀÊn5'¤”w-]à&©JËÒÕ_.!µbyUCRBjÿþüûeåÞ½ï£ÑoÍ 3gωu†÷QÿÇx#¿ûË»Ak…8Êž{ÍOIoj•®?í/WY䯹›6¾ØKz daS(¿ÂÃ`°Âl –¾“U0‹j!7B®Ýý,¤¶nÛN 1¶{¼³qÃQú½oÞG­‹—Ò¢…U‘&Ïš%ÛtèP5)Üx »ñ‚¸Á`°Øl6P#6!U»B)®å݇OÑO¾HÃG¼“:=þÄ‹Ô2e¶¬Â%¤6m¿îÙ!…g‰oœøõ°v=Dß¡ï`°Øl6€ $·8¿böÁ&0‡˜ƒ À`°Øl6€ ÀjÜ ¤j¼ñ«Aò_ À ¬`°Øl6€ À²²))ü€ À`°Øl6€ ¤´©”À²R°(¿†À`°Øl6€ Àj×J.¤â*À~¨7E›¨7 8ˆ#!GûA@@@@@À"!“” ¤RCv€‚ €€€€€€@JR)!;€€€€€@HÁ@@@@@@ %©”À@@@@@ ¤`      ’„TJ`È     R°HIB*%0d)Ø€€€€€¤$!•²ƒ€€€€€„l@@@@@R€J ÙA@@@@@B 6     ) @H¥†ì      !” ¤RCv€‚ €€€€€€@JÙ©®iÔØÐ›dêGÍ])[QpöýÔܨêíMÍû .©jl}Ùçš;φ¦µ‰›ÛÚä×8ìnéjîGM­‰‹ªxFn¯gc"9Î'®Q,âŽÍÛ_1›Ïµ$óþ3líeªÓ(Ö&R÷;Ÿ„T›‚1H „jÁ¸–šüã ‘©‰€ž#¤BXÚÿ•R¥ê?©B­Ç€€ô@RUÛ馷-*8%ç€;'(سÓc„TÖâ£bBª„ý!Uµw4 @@@ ú@HU_Ÿø-*~ÀœóhéÓ´Š/·RÈŠ™ÆåfQÄ™@H/»C‹±‰ìZ’@@@z"Ò )}À©Öµ8ÖýD{F,ï OW“ë„’¬‘2…Cèúc`œ¿'X_Sè:0»üØrŠòÑ™ ©tö:h¶„y¾x Ye©~ZÀKdº¦@ÚBªÙ$Ê®YÇ©@ Úú7ãš°ú:Iÿ©Ë&mÙùBJðˆ¸VÃbAõú.äØÂm"»›(Jè™Ê+¤ÂŒÁvû×n‡7)¢ CยÝåkGt¯K2x‰…ŒgP±ƒCÕ&-_&B*èçÎ1É@I ‰Š˜µGª!•ÆÜ¡¸›ÁrÉ¢"BÊš.˜±]esiyhB»˜z‹9Öž=Î&ÊqcE   õO Š…TÄúÕ/‰ÖHÙAâ:ÕñÌ*1,åT¡bÖ~Ä žð³ˆZU!yÞqë«"„ž;<}T„¶™­‘Jçù+¥]Uv<Š©7»cU? j_Ü]ûA@@Š'PÕBÊœF¤«N:EÔ>õü#yhôšŒ|Qƒ²"¢‘.¤¢ÖG•BHYý’·>-*lx)]Ô&˜éŒèó`èäQûÒ )û¹d i"ÜÅx‹*»N¢öÉË<‰w­øû'JèÁª[HÙ¢'æ¹CE?G*$ÒšÛ’Ìj y^Wrk¤"×Gå—<+É̇O5ÔÏ7Íó½Ê ¤,; ì ŽE1AI ^#åqÌÊ.ìþ+ªìbxuN…ó·×„k¯’]ÆÈ   yª\HE ü¥¨êG¹(]¶×!2Ê›8ÞÌõ쩨}qV1U0rÊ`aB*z}TÄ >•ŠYäßô25…=d9«©}ö4K‡"–…uLSSØCy§"…TžgÔõ#AÈTR§3òhs:£m.!añ“üð:=¶°6G x¥ânNØ   PR!ƒ@û@Vgàˆü)e±^'ë×ú°‡¿†÷…Ë‹6 ÍÉ]Ä­R­s ZS )Y’ƒ­×Ný\Ëá‘rØHÓ4jV";,â¢k m{e \˜Ø£˜Ö.ø\ÃûÏ´¿”eBj­,*O´%Z'˜²^£ÑéŽó & dQÄý‡‚€€ô`Ù© §^Ã౨áÎCÓA@@@ ² ¤*˵W’„T%é£n¨iR5Ý}h|Q ¤ŠÂ‡ƒA@@@ '€êɽßÓÏBª§[Î@@@ &!U0:Xó ¤j¾ q    P)R•"zA@@@@j–„TÍv     P)R•"zA@@@@j–„TÍv     P)R•"zA@@@@j–„TÍv     P)R•"zA@@@@j–„TÍv     P)R•"zA@@@@j–„TÍv     P)R•"zA@@@@j–„TÍv     P)R•"zA@@@@j–„TÍv     P)R•"zA@@@@j–„TÍv     P)R•"zA@@@@j–„TÍv     P)R•"zA@@@@j–„TÍv     P)R•"zA@@@@j–„TÍv     P)R•"zA@@@@j–„TÍv     P)Ù ©®fjlh #5Rs—8=¹¯‰ZùLõ÷¥>órÖUês)¶üÖ&Ù7²Cð     P()_,Eµ¨œâ¦œuÚ e9®‹š=‘ !Uà¨@@@@ Ž ”OHEz¤Z©)ðb™B¬«¹QópùÞ-G‡„æóëmjÊyÊšZõ\u{¢#È'ËÐêfÏNc3uñvñÚì*»ÊœäÓØ(=†Ru|EãÔ@@@@ÊB  „”'Z‚Á½)|ú¶Ñ÷éx¢òùÓ å‹2Z…rícá¡¶+‘¦„U°/¦ì²ô`’J”lõ¦^BH%†<     N BÊ\# ÚÃ_ly5ñUBÊó¦é?à‘ª±kͨBU"¤¨DU‚¼Që±’z¤¤'J¬“jÓátOTà™R"OkO•­‹ò¼g¶Hõ?‡*Ò*´R4 @@@@ªŒ@å…”?ØwN´×D… •¨|1^£°5R²Ÿ¬páöZ©¼5\U'¤,kÃÔ¾*»üÐZ%PBJ*-jŸ-Ï÷ åžK¾^Š‘3_ìô»ðˆžP²¢õékªRM¬ª‚N@@@@@ê@¶Bªˆà@@@@@@ †„L@@@@@R€J ÙA@@@@@B 6     ) @H¥†ì      !” ¤RCv€‚ €€€€€€@JR)!;€€€€€@HÁ@@@@@@ %l…TW3564Pƒ•šZµVÉÊ(†@y„T¤GJ¾È V¡ !WÐ(a&BÈivì- BBĕ˄µ-¢>ëxç´¼4B2­ ¬„½PáÓùœk¦ Ï`K×¾0óLשb.s     5ò©¨5RúÚ—(q¹GеQ#55iSütº±)#sn]TÈ”=£ãBÅ_ OOÁBŠ(*À„s_–) Dšv@He}é£<b”^HÙkb=HÚ'C,Xk¤Ô¾TkŒ\C‚"8½CamóD™ò[?>Ê£Shû“ )9ÞØF‡]©¼}<µÏþ<ƒ5RE¶k¤Š¹Ìq,€€€€@ÖJ ¤ìçHYB%RHñééÑà´àòÌí¨}¼M=·(&êŸSxð±!œù#Ú&ó‡Gãc!PtÔ¾0ï]Œ¨2ŸeNë‹Ú§[²¨}ÜáÏ +¼ˆÚ—õ…ò@@@@Š#­*®-å?šý%{ÀUùO§¤5æMï+imFá˜ÖW>Ö¨ @@@@ +¤¼©x1Ï:JưÇ䪈 © €ë1‹HM Ç ©Ô¤p€€€€€ø ¤`      ’„TJ`È     R°HIB*%0d©:·×ÇL ž˜ôn={ö ½ò88ÖMÇÊ$Ú¿“ IzýÜž“Çȶqñ    P; ¤j§¯ j)‹¨žögŸ³QG÷Ó½/¾KŸûá#Ôp~SU¤ßûæ}tÝã¯Ñ©‡èã>ìiÝ„óš&!UÓÝßx)’^Qß¿w$ÍYµ5Z™r´wí—mºïÅqðH•‰9ª¬”@HµRSCƒxØ­JMÔªZËWmð?ëïÃÎ&I×±ò8½ ޶¤%˜´-Ió¥­¿ÀüRžbOTëÊ-R,Ýa­+7˶á@@@@ ¶d,¤<Õ('¢®æFjhl¦.æ’Vd¤ÍïlZm©­~*¸µR$×Fñt>þ;wî\U%n“j[ÁŒA@@@ÊN [!å>,®©¹K÷T ¯T^^}?ç·…W57 Ï’eÍMšXãr”P‹RùBÎj“{Fy\¯hO«æM“M1ðzùíuŠEGZ=|.ªœ@€* æ!åy¤ ¤Ê~oA…    PײRÂï¤Äë ¼¿Ð©}ÞqhÁXÌù½ýR]‰?±ßù>FH™©ü2=Ï™/œÔ (¡5-Qµ×!¤Z…PÒÛjzçBöehrR… ©Y·™SCo›UOw5µïƒÏQtšE7 4¤ÓÊ7í6j¸ô%Ú{<R†ãX©ÒÙJRÈVH±`Ñ#Mø-çémrs˜*Ì£m×½DžÆj¤¦&mŠ_žxÓ¢úùtïP¤H[•$PF¨w ©RoXÙQBêÔûç(:Í¢›„÷é’!Ûbòm£ç/Ѽž7ÍòóëÛ/£ç;¢ëƒª„… N(ž@¶BJNe3£öÉixq)mU^°Cì8Ö.Ù^&ÅÃ)’üõ[¹JÌuWÖ1*˜D°¾I߯¯‰òT]hX÷è5R!¡á‹ï× Lí3§ö;~–bÓú—¨—>¿ï̼cZú6P¯ç¶Û×=w5ùfÒ«õìúøº ¤24v   e$±â–ÛϑҧÔùb†=C©£öù‚%˜Èe™^§€[¤‡K ð`µÕp¦éSm±Äg©EÛ3¦ FžW„pJ²f¬Ã€2…Ô®=Ç(UzçÆ`½ß…O®óm¡kzÑmZYmÏÒ… 7ÒÛ²|Çþz!¤ 0j   U@ BªLgÅÂÃ1°Lµ×L5R¦ÚÖÕM¥7o‚êzƒ_0.p>ðùbzl—?ž~Õ ÞG×!U3—    šRÞ´;G´>tn)SHunÚG‘éµëD½§h¶oÎSt~ÃEÔo?–®޻ʋ۟;B -€€€Ô&šRµ‰º2­†J)¤¤Hj óû-3×ì~QÃÕcƒm#¯&´Ï,À®£‘R€AHUÆÚQ+€€€”„TùXW¤&)SHmèØGñi=|¾õ²«ÆZÇÙy®£W‚²sû®z5º>x¤*rY R(š„TÑ«»)SH­]³›ª)AHU÷õƒÖ€€€@©:· )SH­XÞEÕ” ¤êüÄ退Ô-©ºíZïÄ ¤ˆN8J¿÷Íû¨uñRZ´°£ªÒäY³dÛð    P[ ¤j«¿R·BŠèÃ÷OÒµ^£oÜòM˜5¿j„ÔœE ©±ï@êóÔ©û€€€€T–„Teù—¼v)¢3g>¦NŸ¤šß£ÏýðÞ¼©*{¢ú>ý&}üñû%·T    -©lyV]i,¤zbªºŽ@ƒ@@@@ꊄT]u'N@@@@@  ¤ÊAu€€€€€Ô©ºêNœ €€€€€@9@H•ƒ2ê¨+RuÕ8r€*eÔ     PW ¤êª;q2     å P!Õ*xÚФ¦Örœ êˆ$ÐÚBýú©¥û¤«F4´Ð¦¸®Nš/®µ?ª¼¤u%Í—´MÈ    PQÙ ))¢š(7No¥&!ª ¦*ØÏRDédµQU“bª³AY”Q‰sG    N ©.jnl¤æ.«®®fjll&¹™ßëBËþ,$ /Ï£¥ 2YF55zÛ›š4q¦ÊÏ+ ½Nt˜ÚGP[^ŸïNc›ØË}byzòýžðò"…‹˜þ4¢YJ."önLObŸž/x¯ )ÃV¶%þ”°ÒÛi·9¯]9Á·I ¨ö;÷é]*ëŠ*ï+Ã6‚.Ži›š   õL {!¥„AMóTE ©·ZÿbFߤXÔzŸ—ªý°%ØR©l åVζ ¤0ˆ®‰A4n•»IT ûz„Ôó¹U‹ýÚô î=…ÚŽ}l ¶”•-¡œê±%)))Ø@MØ@=BêùÜjý }S=Øú¢Ömí¯?†Â º&ѸùÔßÍ'mŸÚD65XéµÉÉ9XWçx!Ñܧ*vŸÊ´o¸O{û6:Ú³ßÞâ5íµ€üµÉ̶¥Þ|ÿRöÕ5®Û˜Væqod{ öWÜ~BÚXñveÕ(§ò6VÆ>(¿ò¿<ô‘èyU]ðeì ÜDªàþ®Ú KHé©–ï®Áº<!¢X0VÅে^Yö1p†ªÚ{M©¾‹Ë!¤ÂÄY5ÝK" {èý©T¶Šr“G+&¤¢~…«¦ ƔܘÀ ¬JiqBj»=0µ¼Vêž3@ˆýW`ûs)Ï!¬l×`ÛÅBÑÕ>uä¤.äóáWå 9ïJœW=Ô™ªolô?ý¨<¨ìeTy…G"øaQ÷N„õ£¿=[Ê+ ¡]÷Þ4BJ¿Æë[{ŸqoÓm,Ê#qŸˆ¼¿è‚%¬ eÛâžÌð=ëò~¦·ÑaÏr¿æ‰gáU«³ êáˆsˆ¾¿TŠôH10hW$Œ 6¾dÐOè§(ˆR£ý©Tª/g_\¸î/£ýIWzzUÞ`Ýo¿î­WíU‚Q ,ÔyËÏç뫰뫾 ìIRÌßå‘R¢^Ù¨<6ª1å³f¿Ó )¿•@6~\¶løïÙ6&¤¢ì˲YãþâQê>d´QÍ<ò0lÛn£ÃžU~yσç¶fí½§|çT¿*b`À"J¥žÒ¡8ÏÂKàVýÜ’¬‘RØ<¯¶~Ñ)þ—t¥Ñ·XÆ@BUl§öÀIV‘ç©/ HÒôMÞ /²E—=Õ]Ý›f}'âÞUý÷.ÕG‰…”ºV­¥úDìµ ~\Ñ®í$Sû\³~ÔqQ÷ÝÖ"ï5žÙ¼\öõCîcÝÇpŸ(Ý}¢bBÊpíj®j¾Q$ñNŹyu1U:ÂÅ ¶å²(”h {ê‹=uJM›Së*ÌÁ`Éé/ÖÔå¹P`¥ Dâλ\}UOõ¤é›,„·åõ±þ+>„TÍ$ )m_çy¢Å±Ö\÷°»XèeDÙWÔýÅ%¤ì€?²îb…”8g5¥Y¶'ë€c5{ UãwKÅ„”=•&L<20p‰(ˆ) ø«ñD›’ÛeäÔ¾©$až&y__Îü%÷£L9ú(‰·M ÅT) Š0¤é›TȈÁf”ÇÀžöWûDÉïS‰§''†çRŽ){jLU1”~¯É@H©k€×€VÃ=×@6×@=r¬!•têM”ˆ‚˜Â…PqO9§¸5Rj³1ý%,Ô´ö‹opo©à\|ýÜìõܿƶ¨5 ê¼b»h嚎UTß„¬‘r®§J°F ƒËÚû>Kê‘r®)wy¯²®[¼'R‘kðR®‘ ¼Eúý3!¥Î«jB¶ãG©Ìî©õ6f©z!¥.xçŠÆaÃzŒ Ä ©à‹W‰+H=ð´§ÏUrQ³~nJë,¤{ê{ûA6‚yÌy×Û—X©Ï'mß(¡|F”ß7v`é]Œlß}þ´Õ@ðcj_ÍÞóœBÊï_5ENÙŠÝNEh´E·}Œ¾†Ò^C¶¦I•¡ÿXyqœÈkGŒmë×Èè{–¢P›â\êkå×ÞÕÐgÕ/¤ø¢ÅÀ f¿8ªÁÈцú¸9æEO«#]Ô¹UIÀŒz½ÎŠê›:²ÑzíßržWMÚRï/xÞT}|w—ó«D]åRøb(‚ À °š„$<ÏTçfý°d?c¦_$õ\gª¾IØßõÌ ç>ø­ [ª‚û‹îsE&„A`U“ @HዃzØ@MØ@M B ´¥z>·júÂ+¤-è Ú ±×1°%ØRV¶„rªÇ– ¤ øÀˆ«ÇˆÑ=£/êyRÏçVë×'ú¦gÜ_Êa§°%ØR9ì u”×Î ¤ ¤jÂCyo ÕÈ»ž!õ|nÕhKiÚ„¾Á½'½Då…-Á–²²%”S=¶!!!¨ ¨çAH=Ÿ[­á£oªgÀ[B_Ôº  ýõgÃeRI*B¨½{ßG£ßšAgΞŸø?þÀù}’¿#GŽÀ`…Úßg =¶Ú«çs«vöqíCßàžg#I÷Ö`KImùjÇV’h )ˆÀºÀâfU[7«zí/ °ª×Ñ7ÕÛ7µv?€-Á–jÍfÑÞx›…‚H‚H‚ Ô„ Ôó ¤žÏ­Ö¿ˆÑ7ñ‰Zïãrµ¶[*—­¡žòÙ„Ñ51ˆÆM¡|7…je]σz>·jµ§¤íBßàÞ“ÔVâòÁ–`Kq6‚ýµg#RRR°š°z„Ôó¹ÕúÀ}S{›jµ9Øl©Zmí*Ü6!¤0ˆ®‰A4.òÂ/òzaWσz>·Z·?ô î=YÙ0l ¶”•-¡œê±%)))Ø@MØ@=BêùÜjý }S=Øú¢Ömí¯?®:!õ…{ÈN0¼ú3<ô)ú4­ Ôó€¶žÏ-m?W[~ô îUYÙ$l ¶”•-¡œê±¥ªR, þzX?ÝKê=o‡ÑTÑ /Е°z„Ôó¹UÂV²¬}ƒû]Vö[‚-eeK(§zl©¢BJHü¾5Iy¥ËšÙ4¬¡?õ7Ò0š½¦Œ°eÆÒ*{ª˜³mýiìD­maÇ:í,ëò mŽÃÙ@=BêùÜßÃ3²“¬ëCß”ñ;´Jm +›‚-Á–²²%”S=¶TBŠEÓôY)¢øµ ”K8LKý{ͦ®rÝœ#…”%°|qeˆ©,Û !“¥=UAYõ<©çs«õ/|ôMõ X`Kè‹Z·´¿þl¸âBJMßcÅÓù Q<Ès ‡U4V÷Yž¡@Äðö^cil/öh)ÁÃÇ*—&‚dÞÙ4»OÎûå•’?´mâz}Vy~½ýiÚ#¸zÝì…sµ%ê|rç>Vœ—É¥ŒB´ ë¸ÉUïM®ž´õ|nµ~M¡oª÷žPk¶[‚-՚͢½ñ6[q!Åâ©h&V  mªŸ-\„hÊy‡ºh¶UÃuy"G/Çcáû¦öÅl/´=G¼¶mWmµ„å*!¢ÎG?^ϼ‡È‡«Â6Pσz>·Zÿ"FßÄ$j½ËÕ~Øl©\¶†zÊgkU!¤Tp‰‚½QJ³FJóVÙÞ¬<ï‹_„EåM3µ/ð$ù+OHY^0cíUD{Ô`×h‹å™“¬óá}þtÈU}ʼƬÂuÜxÊwã)„u=BêùÜ éëj:}SÝ÷…j²•¸¶À–`Kq6‚ýµg#Rz”¾â…”Ãëc ÎÙ3“ Há1q¢,K!&žœu„ˆDªaÞ:°(a&½XaBJíË *\صwa×cŸÕó ¤žÏ­Öm}ƒû_V6 [‚-eeK(§zl©*„‹)• S±Á¼5BÆúåå‰õHED׋,þqam‹Z#•·¶+D$&ò‚¥ñH¡®AÃhlŸ±¹©€ðaZ_Ø@=BêùÜjý }S=Øú¢Ömí¯?®¸r=€· 1'¤œb)Ê+£­)J:í.‘¨ÑÅUBa篃 Ö+õXk¤Ô¾Tk¤\1­7¼êºáÕ󀶞ϭ֯#ôMuÝjÙž`K°¥Z¶_´Ým¿R™vJœ¿¨³§%˜ÖçGé“*ç±z”»Á“äÂ5¹È¦hŠ|ÆU¬g+¤=Ψ}\¯Ý–ˆ¨}yϾâcã§IfÚUàñÀùT÷—\=BêùÜjýºBßT÷}¡–ì ¶[ª%{E[“Ùký) ij›~Æ¢N…e×츂eQ,ëyRÏçVë_Æè›dƒ‰Zïçr´¶[*‡¡ŽòÚ„·ÆàÖóÚaZnDå½%á]σz>·$}[ÍyÐ7Õw/¨f{‰jl ¶T«¶‹v‡Û.„„TQ^\\øb(— Ôó ¤žÏ­\öQªzÐ7¸Çee[°%ØRV¶„rªÇ– ¤ ¤ ¤`5aõ<©çs«õ/|ôMõ X`Kè‹Z·´¿þlB ƒèšDãæS7Ÿ´}ZÏÚz>·´ý\mùÑ7¸÷de“°%ØRV¶„rªÇ–Ê&¤’T„<      P/z÷¾F¿5ƒÎœ=Gü¾ÿ㼑ßã@@@@@@À$!‹” ¤RCv€‚ €€€€€€@J™ ©=+^ MS¯£MSzÓ¶Ù¿¦ƒ[[S6 ÙA@@@@@ º d*¤ö­yƒö,Bg>ÚOgÏ¡Žn “n¢ ˧U7´@ ª 455ø©±¹ª›Z`㺨¹±šÊý»SW3564Q¹«-Rk¥¦†FjîJÑ'[.§AزH½â¸ïµ²³è“,ÊHqªÈ  =•@¦Bªcü-ôÁñõtæÌž ß;ŸÚß½žÖ¾uUÚÇ^O»×Ïè©ÌqÞ )47 ÕäÐ*^YP¥Ц¨«RY»ÄIòàBªR=WoJ!% UŠ%C¤šbYö¹ßá­â—‚LûB*®C±@2!©Z5òôñÇ›Ýé£Mô±ŸNv/¦%#~A+V¬Èä$P€@è"á1¡l™Õ†Š½MÔäôHYž*9@Ö¼<`<š·#Ï˲Ïp{u©ò”¸óAJoLµ1.¨=³&Ñ?ƒ(6,Š$¯VÛÛÇåiÞ?=÷£RRúviÍÔ,Ë´…™üi!çá ÊNºM®Ùž}è÷‚Œ%ü cw¿©þÐûÛØÒ6”-hû|;iõQ?Êx¶è%S¨GÝ+2>uN S!µ|ØÏé£ÖP÷Ö7Å©ë¨sÒ¯"Ò/©sòu´gÃÔ:GŒÓ(˜@«ç3Ÿ‚©}™þr_pò:PMë ŸÚÇÛFßg{®rû¼ãU>vÝ™SÇBöƒvëxÛ£a”—Õ¹Ws9ï‘RIÙØùüÁ®ømÀÿÓ¼\ú>[H‰ ÀècJ`\¿rU¶á̃k*é¹Us×U[ÛlÆ úÃ}[^Ñ;‘׿f7‰ïÕÆí ©j{þgtúƒb*_oúèÔAíô?ïÏŠô¾H‡EÚCçεˆ<Óhå›WÑÒ¥K£QuµÑˆ†þÔßH#¨-÷T~Ô²M-´I~Oiï‹jÉajk¬ðyÕ~ ÙcOHùë¢êmjŸ-„œ"Qd·6 OG³ò^hkkò¦r¥Û×$~¹o² SNe˶Û+_ZOGB6±¢D/'¤¿œíð½ZIûô{iìÎ%¨ó¦|úߨ¼z5gÜqy¢K_i­Ñ+ ” uK S!µðéŸÐGGÆÒªW!€}BçÎl£s-i;=•Î~0Μz‡Îœx¬éGy–V¼üszíµ×)_´¨œ­-Ô¿±MʲŠüe&ž´Ös™•<§Š€D¥ A ÕRÀ°?×2<ÃClB ˜ü×`Pd¤rÓxŒéxÖt ×¾Ææf1µP?ð¡†M#ªeæIÛžç=r„›6¥ó³¦òŒMA¬“ŠôZ^,mšW~¿ê‚+Ê6¬`#=¹ß“ÚGš|N‘“°?òD0¯Õ ÕQÂ*ê~æ¼@@ÈTHÍ}ìGôñžçhÉ —‹¢OÐ'L§sL¤s§ÆÐÙoЙc/ÓLJ_¤=Kû–ÝE§÷=@‹Ÿÿ)5*½¾ Ý#ÔØB-ìµR‚‹÷+/–&Â|±ÒÖäØ'[uœªC÷މ²óDUH¢ôÃÍ#4Ïšå}bqØ$}\ø`]Ö©:RæZ”ÜàÈô yf ÙBì4JEéêÏoŠˆ¾—pŸ>…0Ïø¢Ê¨GKôHY'Æ&Ê›$mÛê3µN*VH…y¤´vE–’ÏÕ=­ßKaËqÞ"½Îȼ޺&ç4̸:’ÚC)Îe‚@ÈTHÍzàtfëݴ੟léœOg½Bg¼Hg=GgÚóí^Ø—>Üy3Í{âÇ… )Ý#åOýká1†ÿ·I¥;¿Ê‘Wí“Â&ðñÔºèã‚:B§öÅ•¡‰:Ë«ÆmÖÏ¡Îm§‰¸¢öi—z¢2ª?SLøskÍC~”¿¨5NûŒ˜æu±×Dõ¸µc]‘Z#•”sJµÞIŸË©¼`yƒÞ°cÒô«6u3jPôܪÿ‚ªžÆØ!¨c=Iv°’„Só®ó{¨jh Ô L…Ôô»¿OgÖ]I­ÿH˜Cg ¦³B<Ù÷˜ðT=@ﺛ>ÚqÞv#íšs½¿ù4ýþï'Rk¤œ!}* {ˆ|ïOTÞ¼}Úš%{_˜Š*Cz»ÂÖ@a}TÍ\5hhÙ èÏ‘ª·ÐçÌ!%BV´¾ÈÈ|QQºB~Õ–Íð¢Äu‰·æ"DíÓ£ö%b㟮çH©Ëɲ­š§Ê/ƒ×°ÅGíÓûÜÀèŒäÑÿè÷ìïmqvæeR×£v^l=Òg”ÞÑDoëKm¹†Noº’Noü íšùÚ=û;4òÆoÑ{ï½ )n=R”È‘%G"]ÜDµH%¤âD_n¿ááÂú¨ª¾XÐ8(9LÃ*9bT YÈTHm˜ò½w÷iBŸdiÌõߢ¾?ý.mß¾=[!¥¯Ÿ’%ê‘Òš•JHY1ÂÎN/룲²i” 5I@y‚ur=nJeMv  Ѓ d*¤˜ãìÙ³éÕW_¥W^y%6q¾Õ«WÇãOë‘bé”fTàÁZßâ<é)[,é^'mÖGÅ›r€€€€€@µÈ\H•äÄ R‘Ñ÷Dd¿–DQû´àεO~”ÀȨ}f L¹ça©õRXU»A¡     P"µ!¤²<ù8Q–e]( @@@@@ . @HÕe·â¤@@@@@JI ç ©RÒDÙ     =‚„Tèfœ$€€€€€@– ¤²¤‰²@@@@@z©ÑÍ8I, d"¤Ž9BH`€ j|S+ôØj?®žÏ­ÚÙǵ}ƒ{Vœ$Ý[‚-%µä«[I"¸ ¤ ëv‹›Umݬ굿0Àª^;DßToßÔÚý¶[ª5›E{ãm¶bBêµeg¨yáGôÒÜhè¬S4dúq<ù(=;¡›¾{€F-·Zÿ"FßÄ$j½ËÕ~Øl©\¶†zÊgkRÃ…ˆRŸ|Btæчg‰Nž!:ú1Ñ {©¥ã4­Û1… ¢|XW/ëz„Ôó¹Õú5…¾©Þ{B­Ùl ¶Tk6‹öÆÛlÅ„Ô0á‰â?—ˆê>MôØðmôÔt¢''¦• ¦`ÌñÆ Fõͨž!õ|nµ~]¢oêû¾RNû„-Á–Êio¨«<öV1!ÅÓùøÏöD±ˆ: 4Öý;èÎí2ÝÑ=-]ºS¼0Å 6Ѓm jÒ5hõoèï§a4{v]3›†…í«ž‘¬‰}¨¡¡ÁO}hb¹Û¼fõj¨@½å>Ïú’~'RŸ†^4H·½¸sp²årüþî5ˆÖÈ2ÖР^ZÙYôIeÄößY±¶$ûD]ëâ5èaaýåÚn—Ógbqßq¶·vPð«j~R¼&JÌè3¦ó)µWh¬•[NÒÔûeºoàz9rdUƒ„ò/òçžË9t¢„RŸUâÑE³{± K«ä—Ï*Ë"Jî;B«úˆ÷½fSW•}1…Ÿ›)bÖ êE ÅŠÒž{$Å~ž)…T u‘Ê‚©úLô®s½¿'öÉmÏä>ØÃû5†)¯¥øLÌ,dÿ+1•THÉ|º ÷lª× 5…¡âl%nJN•èÔÙsÇÅö}Å„–`!¥ÖDé"j×I¢lj¶%š²àÝóäZ5jTá7\Ä`¨yH6 õ…”KÇJ/ÕX`zD~¶šh{û¸ŠK„‰¨Í"P×ä…éî« ¤jdÐ ¾º/øZîŸx±á{Ÿt“SHiªJ®«øsË÷Pè}É õ‹³ é^ oŸõË´!°"öƒ$+=x2Ê«Ÿë zðky”€MÊÆÎ§x¥mj^®°Á°?ËècJ æÐûÈ!¯E^>Í–ôܪäÚª¦{^Rϳ³Í‰„TJ¯¨ÞGQýj zÝNäû¤¶›¨ù4«ézª–¶TLHqˆsRQ"jc7Ѥ…‡èÎ~+â…”±"d­.b\İšµ¤bCNßó§ö©µS¦Gª…T°æ!Äë£ ²'öy)ï…öKwÞ@,Ý>þ%ÚœTÄ ­†®Ãäƒ_GB6±¢D/'¤¿œÞß+•´Ï“æ³Å] õc5 º"mI_åâÜ´5Tyk'-fªþ‰°YÃ>D>¿­ò^Ãkm©á ó¶ÕÏ-Õ`[hCåì©bBêéwöI!Å%xM”>=Q,¢ÚM\ØM·?¸4¡Rë"| ükt®‡€ÁWÎàÁ¾vÙ'RÞô=_,Õ™GÊðPƒ$5€ò_ƒ6°rÄô=@3÷õ4(@בU¦ò‚ÕÓu–|ðë ‘cêd“Æ#%ú;˜¶9pµ¼Xzðù>lðÕÿÖtÁÐ殺áä¢ÜqŸ.Ä#eôS‚©·aýšçqâ²r‚*_H…ØR*aW»ßU¥°”Y½öP1!õä›»¥ŠQk÷Mæmº·­0!%šûâŠ=V½ÆÒX×BtÑËaj‘º\¨>[,`÷×U¸Ê±¼aÁ¯ß2ïlZ¥Eã}Þ/æ^ òæÝ`üiJ2_nMGdt2ܤjÖÛ‚›düM2;!UÃk¤ôÁ´u½ËA¶;½ä:Px¦¢×FD­›Ð÷éSól6l Wã÷¥äƒß¤¿èë%S¬‘bŽjT¬ óH…ÔÕwqý·¿Æû?Ë{sÒi¢¹:3Z#¥OMÚ6Æ÷>bM`ࡎ´Çø{{–ŒQx—Û*&¤ŒÚ)…T˜'ŠEÔÊ=Dã¥[ï^X˜Ò=R¾ØÑ… šaƒº¼Á·á½ò¬«¼žxÑ„”!€Xðh3Y&ÞD^£û³ÓcfÖ´M/;¯Í¸xÊ}ñ ¾òÚ\è Ä÷:©ëLŸÚWóQûòÖE Öý(pak¥òÖHE­ki^»mu: Žü는·')町Üʼ(j g^ÿ„±®­ þ÷Å\’°ÜIì=(_|Ô>Ók$ƒ’µÏﳸÐüQ6ëòœêå%µ¥Ô×”w|PIÞR½Ü%…”ŠÎ§OçS"ªmѸEÇèæÛç'R9Oçñ 8òbÖ¼UÁg],é*M,ÙbÆéIÒ…”VŽ}lXY¡uX¢ 7%x z D BLO­å]®ñçH%Ž&3V´¾Ènv8m=ÒVDp5˜÷½cˆÚçOc²¢öy‘ùTŠXÛÕGyëf|!«Gû󱼆->jŸÞç~Æ þ„ýßCú½³X¯ºñ̸"ž#%=Pºý…D‚txµÝ6kÛ Ž*3Ä–zÐ÷U)ìeV§8«˜ê×¼U )q&¢æm!zwÑ ºñÖ9 …”==OŸÆ yŠøbÎ+ìÒ§ïéÏš±öåM4§ëÓã„S” [Û67(ˆª:·ØAH Ÿ=Ÿ[­ùWeß`*UMÞï«Ò– ¹o²ý•ûyv…´ÇÔäuRkßRÝ$…T”ˆšÙA4fñIºá¦ÙÙ ©Ì·Z¿^Ñ7=ó~S »…-Á–JaW(³²vU1!USï‡1ï‚X6P1¨çAH=Ÿ[MÝë1í²b×w­ÛI’öã:¯ì€7I!ú(­ @H9ƺ§ÊŠþ‡4¾ha±z„Ôó¹¥ýRª¶üè ¬²²IØl)+[B9ÕcKRWdPŒ›@õÜj¥/êyRÏçV+ö…õk¸'•ÚVqÃÆJmc(¿ü6!!!¨ ¨çAH=Ÿ[­±£oÊ?0©u›(‡ÍÔ« ã¼òmB ƒèšDãâÅS=hëùÜjýÚEßàÞ“• Ö`KYÙÊ©[*›JRò€€€€€€@½èÝû>ýÖ :söñûþ?ðF~?“„,@@@@@R(‰zmÙj^ø½4÷:ë ™~œO>JÏNè¦ï Q ЦíûS6ÙA@@@@@ :”DH "Jý}ò Ñ™sDž%:y†èèÇDC&쥖ŽÓ´n ÄTu˜Z     †@I„Ô0á‰â?—ˆê>MôØðmôÔt¢''¦• ¦Òtò‚€€€€Tž@I„Oçã?ÛÅ"ê€ÐX÷ì ;´ËtGÿõ´téÒÊ“@ @ª–@SQƒŸ›«¶™E4¬‹š¨©µˆ" 9´«™š¨ÜÕÒÔÊÓJM ÔÜ•¢N¶\Nƒ°e‘„!{ÅqßkegÑ'Y”‘âT‘@z*’)^%fôÓù”ˆÚ+4ÖÊ-'iê‚ý2Ý7p=9²§òÇyƒÄhn"ªÉËÔ*^YP¥ÐÖá.q’<¸†ªÖÎJ)¤„¡J±dˆTS,Ë>÷;¼UüRißCHU«!¡] uF $BŠK°Rk¢tµë$ÑŽãDÛŽMYp€îyr-5ªÎ°ât@2!ÐEÂcBÙ23iX†…ð ·±‰šœ)ËS%Èš÷‚ìgCóväy™Bön¯.Užwž HéÉOåŠÒ˜5‰þÑD±aQ$yµÚÞ>.Oóþé칕’Ò·KÛh¦fY¦-ÌäO 9WPvÒmâpÍö”èC¿glqÆî~Sý¡÷·%°¥m([ÐöùvÒêÿ £~”ñlÑK¦PºWd|þ(êœ@I„GçãõQX"LDm>B4yáAº{À*©:72œL Õó@‰™OÁÔ¾L¹/¸aY¨¦u…Oíãm£ï‚³=W¹}Þñ*»îÌ©c!û‚A»u¼íÑ0ÊËêÜ«¹‡÷H ©¤lì|þ`Wü6àÿi^.}Ÿ-¤Ä`ô¿1%0®_¹ªÛðæÁ5•ôܪ¹ëª­m6ãýᾎ-¯h„Èë_³›Ä÷ƒjc‡ö€@ (‰âç,¤¢DÔÆn¢I ÑýV'¤Ä·ÿb-Æ[éþRÇë32J”¢ît'•Qîjo_F§‰bªŸ€+xBÊ_UoSûl!ä‰Ú »µIx:š•÷B[[“7•+ݾ&q“ oÒ,RNe«~SJ×Â<ž:„lbE‰^NH9Ûá{µ’öyÒ|è÷t6’4·KPçMùô=¾±yõJ5gÜqy¢K_i­ÑKz^È ”DH=ýÎ>)¤8°¯‰Ò§ó±'ŠETûA¢‰ »éö—BHÁAÜÄ/$,¤a®en†‡"*Ø„0ù¯Á ÈHå¦ñÓñ¬é@®}ÍÍbj¡ ~àC ›FTËÌ“¶=Ï{ä 6mJçgMå3›‚,X'9èµ¼XÚ4¯ü~ÕW”mXÁFzr¿'µ4ùœ"'aä‰`^'ª"ª£„UÔý Íy!/€@é„Ô“oî–B*JD­QÏ'ˆó6ÝÛ/¤º¼©=â‡Ø`zOy¼ZÚ>ûW]=â—:.é6®‚óêåÛÞ)WYªyÎ}~›U[äbz‘Äæ¼?ýxU¯ú•^E1 ÚÇIÔ!~Ж £8(¦c\, P)êúVÆ(^ aU©veP¯¹%782=C^Er-ÄN£¼¹ùƒúÀ3Å›"¢ï%ܧO!Ì;½¨22`QuEDz¤¬Ö†±‰ò&q.÷o¬ óHiíŠ,#$Ÿ«zZ¿—Âã¼Ez‘y½uMÎi˜qu$µ‡Rœ?Ê:'PÔ€Q;¥ óD±ˆZ¹‡hü¢£tëÝ “ )mzJxø­ $2´4QÂSœûüã‚ñYX>aRÌøQÃX$*_~âëfΙ÷¿ÄóyYí6Úc±pqÒEf’öÕ¹ýãôj€€+jŸºnk ù ›þÜZóå/jSÄ>c¦y]ì5Q=n@íXW¤ÖH%e“ÇÌ,SÚ'DyÁò½Ö©ÜHÚ\:XNÚÿÞ—VnmCì%´fd‹«)ú#JÉ} §æ]çÚTÞwmÃ:A [%R½Ü%…”ŠÎ§OçS"ªmѸEÇèæÛç'Rúà‰Åül ësÏç¬wJšÏRºJ_3Z¾œÝfYŽ_7{¹BŸ‰cŸ—Öï¡k¶Ç„rJоú¬f{ñ ´òpyfËW{9jŠRú ‰›#…•ýü'=WT”®_µåýÔ‹Ç·)s ¢öéQû±qP]Ï‘Röå ÙVÍ»è—ÁkØâ£öé}îG`tFò‹èô{ö{œ„y™Ôõ¨]ç†[ô™Æ#%Ï0ê^‘=”õL $Bª_óV)¤8Äy˜ˆš·…èÝE'èÆ[ç,¤tQ úu1áüëvÞqQù‚ÃY†o%iö±ˆâ©vl÷îþ«èÎGVЯEt¾ÛD`‰[îZH7‰é|쉺¡ïlê}ÍÄÄBJ ¹öG­YŠRI×…æàXŒu'Yƒ%ŽKZwà ²:ÉX#¥ÎS¨/#0…CH%┢}¸z@@@@@L%R›7o¦‘#G&Nœ?òÏ zä<¡'¼¿!ÅY¢êÉEìqìüãEÀ¬üˆ~ŠÚ'ÏÁHÂj»Z§¥ž¥ãŒÚgE åÕv›)®ƒ@I„TæŒ1°O†4+NY•“¬ÕÈ    5GBªæº,¢Á 9mR›ÊXOxp.     ÚRY-Ê @HeE€€€€€ô,R=«¿q¶     €Ê"ŠèY2RGŽ!$0€ À µ¾ízlµWÏçVíìãÚ‡¾Á=+ÎF’î‡-Á–’Ú òÕŽ­$‘„Ru;€Åͪ¶nVõÚ_`U¯¢oª·ojí~[‚-՚͢½ñ6[1!õÚ²3Ô¼ð#ziî4tÖ)2ý8 ž|”žÐMß=@£¡MÛ÷c€ H¨çAH=Ÿ[­£oâµÞÇåj?l ¶T.[C=峵Р©áBD©¿O>!:sŽèóD'Ïý˜hÈ„½ÔÒqšÖm˜ÂQ¾ ¬«—u=BêùÜjýšBßTï=¡Öl ¶[ª5›E{ãm¶bBj˜ðDñŸKDuŸ&zlø6zj:Ñ““NÓÊ S0æxc£úfTσz>·Z¿.Ñ7õ}_)§}–`Kå´7ÔU{«˜âé|üg{¢XDëþtç€v™î连–.]Š)^˜âèÁ69™ø54ŸÎÒ 5å¹fõE•l€µ†õj >Ë|nkQ¯†>4±‡Ú^²¾á>™H}z¥³='[.§AزH½ÑÉû^+;‹>É¢Œj…^÷ñ¶ä°!ÙO¾=h¯Æ} IžžÔWÅÚvèñÞ=˜¯Í^}úäî‹ÅÖדú¦ϵbBŠ×D‰}Æt>%¢ö µrËIšº`¿L÷ \O#GŽÄ º °Ð/$WæÁtØ^ø ä”x Õ甼GLìÂ꣚øÇ°ŽÐšA½ä8„Tym?Ißx÷£”BJªK†H5Ųìó>}»Î¸ï1ø+û˜"Ö–„Môáú 5¹¶¹úÉNÁ½ Iž*¸‡×Ì÷vص‘v;˜—ý«„ULHq` RjM”.¢v$ÚqœhÛQ¢) Ð=O®¥Q£Fõˆ©„ ÎòÌÀ»0Þ¡ƒ5Š_I >ŽË{ÄšAgÅà´¶¼R±,þï%YN”å©’_öš÷‚ìgCóväy™BöƒÿY¿<%î$¹g…]ê{H/ƒïaa÷H+Ÿ~íæõQÔ}¶°ï¿RØ#Ê̦/*&¤8:¯âÀa"j³Ô5yáAº{À*©tàÂÌæÂÇ|Ž=×#¥XáSûx`«~Ŷ=W¹}ÞñÁ¯Ý†ÀŠØ ¬<öàÁ(¯~®ƒp»sx”˜LÊÆÎç$½é|–—Kßg )žjä{1dÿSCúÜ!¶çý¨Ó~/Å=8RHiýË"&ÒÛĶ‘gŽé·I¼Ž~ÿ}¯ÙÓ«¹mQv⾿ðL°{v¯°Úbزm‡röA‘¶-§Ìj¬Õ¹&ñ<…òºÏÖÏ}±×G­–Y1!Å!ÎYHE‰¨ÝD“¢;û­(NH­™MÃÆÒª´b$ñq«hlÃ0šåºŒÄuza– Íiù"?¼¬)l ú×\zŸ\'U[ÓúøË#êÜl!äœÚg Âįªƒ”÷B[[“78H·¯>h)d*[/ÔpO¨Ë›¤¼r §ùÅ .½œþrþöyÒ|=¨ßKaƒÉ®sñžÄ“$§’†õ³.ÄcÖ7ºÄ|˜WT¯Ó%ô 9ÎöHžò¨s´¼·úÔÚH‘ußó¹#¤¢®¥½–âZ¨§2+&¤ž~gŸRX‚×DéÓùØÅ"ªý ÑÄ…ÝtûƒK!¤2¿!¤êéBî çÒ#§öЍ`jPá¿_æö@Ä^´îüý_{ÕTcª^ð+ñ sÊQð«xØ Bä©®ã"…”>Ë"†M”ÿ ¼Ò‘EM|Åö«>² k0n•[öu{™–Çæâ¼›æõ#’2õH…L'õ|Eˆ’¤b&VHé÷¨ˆvêןÃczo3®á¬„TȵT£vÛÆÅœcńԓoî–B*JD­ÝO4A<˜·éÞ¶x!Åœ^³ivŸþÔ¿“æò½;cƒ}ýi¬ùŠE…}\Òm|áy¢dlŸa~ݶwÊU–ºq‡ì3c /Z—VFÿ¬½k¸yÀó” „ B¼5Q”› ãGð«¥Á]ø¹y&ìd,F÷ÙÊé@ƒ„—D®£ñ8g*b­…=X²ûJ”èSó¾x’L#ÊÀŠùÂ+äØÂ»]Êc_7{Ñœ"J~©Zk¿¢>ç­¹Ò§öaš_¥/DÔÿ¹FÊxŽ”æªHlÔ>y1BJ~ù[Ñú"#óEEÕŠXì®M7Ô#{!jŸµ/ç€Íõ)u}øžFGÄ0^õOïsõL5àÔ#E;Htn5rí•óÞë¾Î¹\?Цì똩bQS8 ñ• Eœ°’k‘¶VèqŽ©}N[Ž»VB×g© -I¢ªû§}m8¦üÅz…ë{Ês9¯›j¯«bBª_óV)¤8Äy˜ˆš·…èÝE'èÆ[ç(¤B<.º¨Èì¹q•/O,…”!oiö±ˆšMcí)}Á1¥2ÊÑÚˆ25½Ñžú?È­vCGûj¿“‰Ú<Ïz>·Z¿öª²o’¬©º)FUiKÕd'Yyµ³*§šØ -Uw=«ï¶Š ©‡‡n’B*JDÍì ³ø$ÝpÓì…Tí{¤8Ò Oà ŸÚ—¿,ˆN)#¼P%X›ƒÝZÖzûëyRÏç»Ëö~§ÂÛSš0h¬Ú–mû¸Îc®… l9ïú€©™ë£V¿+*&¤{±“î¸^>l÷îþ«èÎGVЯEt¾ÛD`‰[îZH7‰é|쉺¡ïlê}ÍÄ„BÊZ#¥Ö,ÅL‹_§ä]üqk¤œë³"K²FÊòbå­‘J(¤¤'ÌZ ¦ÖHÙk¢ ¤pã©Â/Ÿz„Ôó¹Õê—£þkc­ŸÚŸ­˜-”'®óêè‡BûÇ¡ÿ\6P1!µyóf9rdâÄù#8/2_R¡ÖE9¢ýùâ#0,úž·ÖivùÎ~fU±QûüõP<5/ÊË·fJÅ1̨}z¤?DíÃÍ¢¿0êyRÏçV¶”¦MèÜÓØKT^Øl)+[B9ÕcKR™¼(ð¢T¡%s;ïÁçXσz>·Z¿Ð7Õ3`-¡/jÝÐþú³a©<0Å]t=÷i=hëùÜjÝ&Ñ7¸Ofeð%ØRV¶„rªÇ–êGHAÁ#¨k¨çAH=Ÿ[­á£oªgÀ[B_Ôº  ýõgÃR|×õà7­ú¹iÕ󀶞ϭ֯AôMýÜC*m‹°%ØR¥mõgoƒRRR°š°z„Ôó¹Õú7ú&ûG­ÛD¡í‡-Á– µW½¶S6!•¤"äz!л÷}4ú­tæì9â÷ üàü      &)X€€€€€¤$P!õÚ²3Ô¼ð#ziî4tÖ)2ý8 ž|”žÐMß=@£¡MÛ÷§l*²ƒ€€€€€@u(‰.D”úûä¢3çˆ>×Ô_k“hwƒŸšÌs+ljt5 ~¨·ç–i­ÔÔИξœl¹¿¿›É3×.jnÔÊ΢O²(#S~(Œï\y6$ûI]ÿ¹×&ý&—$qOÄ5 kr(‰â5QbFŸ1O‰¨½Bc­Ür’¦.Ø/Ó}×ÓÈ‘#ËyΨ @ Æ4ibIcÈíMÞÉMBb¬ëNkà­Án—PŠ Æª ç€wBÈ)…T u‘Ê‚©TëýÝ* 9Ó®G¿&ì×2f6ÑÔÔDú¯=®~ò…S`Iò”ñ4P€€I $BŠK°Rk¢tµë$ÑŽãDÛŽMYp€îyr-5 ý Nºˆ2jµ=\ž&¬töÜ9…•órѿͲLN¶÷ÐÕ¯I·‰N’å‹sã~÷ËF¿—Âx•×QôMp½úü]á0{Лe3F™º [RÞÏ<Û²Å|Ä=òÏÛæ¶QØS)ì eV’)ŽÎÇë£8°D˜ˆÚ|„hòƒt÷€URÕdh T&ßË”7µ/DHeúË~Y„y¤x»ú[ Jt¯†·ÏAÁ¯Ý†ÀŠØ æ­<¶¨3Ê+#”ŠUåð)›”ϸroyš—Kß§ç{'Œþ7¦†ô¹C ;m#ÎûÑãú½D§õ¯á} óæÙ€cúm’cýû‚á5ì8ìžÁþ(ÛÒEW’û‹2yý‡Ÿ±F± Pf%R✅T”ˆÚØM4iá!º³ßŠâ„”øVâ5bL•î/Åqü‹x¦¿r§¨;ÝI¥ÌE;²(#e³‘½g°…”G8=R5'¤‚5!^c&ò4+ï…¶¶&oP•n_“>h²ù=ÑÜòxêSûNó‹\z9!ýål‡?¨NÚçIó¡ßKféú!<Ùé}4j×¼—a‚I ñeÇ.¡”g—m‰þ0Ϫ<}Í_Âk¥däQ0”ž@I„ÔÓïì“BŠKðš(}:{¢XDµ$š¸°›np)„TéûÙ]C"(‹2*uþ¨·¦Ô»Gʹ=Üò€ÉÜ {°e/Z÷…Y Ôôýæ¾ÆæfsÊ‘™ ákN cÝyÞ#W@ˆàªÞ4)qLੈôFØÚ¨>×W‚|F»cέ¶=îXʬT"L ûp²òHEÙq”J#¾£î/=ý>Òãl½gžpI„Ô“oî–B*JD­QÏ'ˆó6ÝÛ/¤x°.¦÷ˆbƒè\­ÁMßóH‰)ìÁ>û ßµP=é6®†óêåÛÞ©°…ðêXQÌn³ú,ˆö"y[ XŸÕ¯ñaÑÊœmò9òT)uœÑ6¿µMçZŸÕ.ý\âÚØ3/9œu¡’ ©L½Ç…6¶Àã‚ÈíBì4Ê‹ÒÔž)%zB¢ï…þ¢mgüjn·?ªŒϵª‹ôHY-O4ÅŠ‰X#%wûžŠX!3ç²’ Äí|®éiý^ £Ì3!¢Y¯;‰×JÏ£k?ú(!köò¼]iÔ@,$Íç×¥ÿÊ­ ŽÐòõ6†ÔÍ^.§ˆJx^Î_Þm¢,€úëÑÍõd޼ºíéçºNL‰3ǹd¾¶¬Œªª>õø©Ä­äàÄŠÖÁ-*—¶/bú™}N%DíÓ£ö%bãŸZt3=z›¼Ü|Oc«íÏ/ƒ×°ÅGíË‹ò§GU‹èÑŠDçV}·‡*m÷©ËSìÿh"û:dÚ¥%¬r×¢R|÷±^(¿=ú¤ëÚ‹ö¹&Š[b_°§*µK4+K%Rýš·J!Å!ÎÃDÔ¼-Dï.:A7Þ:§`!+’ŠŠ¨|A*L¢D‹c‹(‘,¯Sí6…|Λ†çç³§îŲò ŸÅãzfU®íÕržKØ1YZ0Êz"tZ^=3Î¥<Ó ËS1jú%P!õðÐMRHE‰¨™DcŸ¤nš]°jå~‰Õì‘âöó4ÅÔSûl{Ô8Da=«°iF•!õ…ž‹«Ÿê÷šÂ™€@A”‡Ò9Mª qPÏ&`zŠäóÍÄ÷7þ@²#P!õØ‹tßÀõòa»w÷_Ew>²‚~-¢óÝ&KÜr×BºILçcOÔ }gSïk&&RÆ)á9‘1â É:%.&4ŸØÇb̹>+渤u.«_õ©qr]˜šÎØj§°„®UŠb²OÞt£êsÔ{Lvö‹’@@@@@ "J"¤6oÞL#GŽLœ8äŸ?X×#牱½÷%ü,aQõì5aù¤ ñ…L f´µOžϰÎE‰'®SNÓž—eOÁ‹Š$åeÒèõqÀžv¨Ž ­ÏÅßׯŠX<* ”DHeÐ.³{°žy(@@@@@’€JÎ 9A@@@@@@¨ !…Î*"!UE¦€€€€€Ô©Úè'´@@@@@ Š@HUQg )     µABª6ú ­¨"RUÔh €€€€€@m€ª~B+A@@@@ªˆ„Tuš     P ¤j£ŸÐJ*"!UE¦€€€€€Ô¢…Ô‘#G `°Øl6€ À`°z²89W´Š«ûA@@@@@ Þ@HÕ[â|@@@@@JNBªäˆQ€€€€€@½€ª·Åù€€€€€”œ„TÉ£z#!Uo=Šó(9©’#F     õFBªÞzç     PrR%GŒ @@@@@ê„T½õ(Î@@@@@ ä ¤JŽ€€€€€Ô©zëQœ€€€€dDàæ6JšYMæm¥оîSôñ™sµ¢:‹)­j%jh /‘­MDMq™%475wU'X´ @@@@j@”ˆzîÕ4iaµo;L'N}D»œ ÅëöÒØÖÍ4dìzxÄ’T§ÿï|‡¾úÕ¯Ò¥—^JwÞygÞ± B[t¦*³”™K*¤š„ˆjL v8Q)»eƒ€€€€@zºba4jJ-mßOGOœ¦ãB<ñû×§m¤~¯,uz®’ÖÈâ©OŸ>ô¤8@¼ÐW\!E•úkh˜ž{~î}~ùŸÐ'Ÿp"ÿÓñÚýþÇÔ¹ÿ­ÙyœÖîåµêò„W«8.Ø/¶±çI}Ù½??¯x‘M"O—Ø©—ò‚€€€€˜XH½2i­ì#…dž®Ãôêä ‰¦ü%áÉž¨gŸ}–n")¤øuáÂ…r‹©9sæPÃÛÇè_ý«%S”Wê¡}Bä=?kýË vúì¯WÓ¿»¦þõ• éÿ½|6ý?šDÿæ‡oÑo8}áêôã‡ÇÒˆ–6ºå¹ñÔ{À[Išä)™b/‹%¨Tü¹E@,¨Ôgq )`ü'óŠýjÚŸQçõóqER@ ¦Ž ö¥ÂÌ     ŠÀáã0ÞšÙi¨Ç^]F<ÅÅÖ»s¶ÐŒ¥;¨mý>Z·¥›¶ì> ò|áazùå—徆ç:©ÏJ቉”zÏž©† /—*9­Oˆ©áâ×°¿7ì¤ÿvý,zpôZúÒ­tÑ‹èß_½˜~çævú·¿\D¿sõLzðåytþo 5޾|Õ ôG—ÞKÿö«}ééQSSu|I„ ž`Í‹_àpËìi|Ò;åï×ßs^þ¬ "ý³îé’‚K«ÃþœŠ2ƒ€€€€8 ¼ÿáÇtðètôä颂I44\ ÄÑÒãÔ0l˜Oü:œE“VòõºaRHyk£.ð½Q—GöÌg~9–þõÆÑÀ·Ðg¾õ"Ý=l>ÝñòRzæÿoïN££*4ŽçÓœ9¶ŽsúÜù晞µEGÄeFÏØsºO»+aÙ#û.k¢²Ù‚ Y‘=„‚@ÈJÂŽ€È3ï{+·rS©JÕíVHèμ“TåMî­ß}ZóøÞ{kqº¦oR‹N³5ù³µºçÙá>u¹îjôž~Õ¬‡îjøŠþçåú/R¤œÓìÜÝ0_ØU&ç±-UÞï…”¥Ð›GÔöØû=[ª¼×Xýµ7¯ð%Çd@@þÎì©}¶LÙ»òeœsîзõÀIg%jmZ®sã‰X?ì)}q˜BeËÒ AN‘r‡½FÊyÞ)RËÇ„ÔݺûSuߟ?Ò¿¾”¬foMÑ‹ïÎÒ}JÑ£oNÒúèþÿ©|²¯Zw¨Ÿîª»¼¤»î}V~¶(–MçüìEÊ{ý“{½RðÎ}aŠ”-Dîê•{: »wµ=ö~/Üyv#¸zd¿ï¹CŸ[ºœÒú³¡½%Ìû½h?狃É € € à ØÒdW j» º-Yöº¨+æfvîî#¥æ‡Ÿ©í£¡)QvØ"5{ö<ÝsÏ=æk³2e;§üUžþçÿh8·ã3wã»®¼â2Íøb†œª±3SͪÓ2­\·Q{d¨´¬Ì¹³Ÿý¿ë7n¨äìE_›úY‹”wuÉ»5®gª¼cßóö(ïižëœœÓC®{òÞ\¢Ú&jù9_LF@@ @¸"•øù-ù6Ë9­ÏÞ½äÌem;Xä”§áŸTßhEʹy„)NkÖnô૪"Uû5Q·òPý¬EêV¾¶ € €?¯À¤/ö9ïµ~W¾2NœvÞ?ÊÞÉÏ–(ûÆ»¶TÕ¶Z[‘œæ)§\Ù•){C ŠÔÏ{Dùm € € ð‹ Ø»óÙ›JlÚW¨ù¦PŸ·;¦÷rË•ßô)¿?{³ç³"u³ÅÙ € €Ô{ŠT½?„¼@@¸Ù©›-Îö@@@ Þ P¤êý!ä € € €ÀÍ HÝlq¶‡ € €õ^€"Uï!/@@n¶Eêf‹³=@@¨÷©zy € € p³(R7[œí!€ € €@½ HÕûCÈ @@@+pír©ŠVŒÕÉ> •? ‘ Ü«“ÃQÞàûU6¬©Ê7ÔùÑOªhLNxB§&<¬¢±¨tÅ(]ûá¬/DŠ”/.&#€ € €@]8µt²J†>§ÒÞ Tد©Îl ¢QOèÂèÆº8æqÜR—RéêÄfúifK]™ÞLšÕR§?§Ó¦€ùù HùÑb. € €ÔYÂ÷ª¸çoU<þY]üvº.“¢3ó;èÒø'TšÔTçRîUù”GU>Ý”©é-U1í1iz3žRÁ¨G|½.Š”/.&#€ € €@]8>ð½ÿŸ*ûì-]3;YnÆu»³Š¥éìœ?êúÇ鯬F:?£‰4·…4ͬXMnªS£)Ruõ¸²_ € € ð ” xXeCÔÕ”ÖººéCULUùé"§T©ü†ôÕ¯í¯ë³WÅgæT¿›é⤧t.Ñ\3e®›òóÁŠ”-æ"€ € €@(ø„JF6×™ÁëJbS]™ØH?|NåKÞÐç $Ó¥®›VuqëhݘÙT?Nz\—Ì)çG<®Âá¬HÕÙËŽ!€ € €À/'P2¬‰.j s‹º›¨R]ÎÛ©‹›'©üÓ–ú铯Ò÷ËÌê”iSÒÕUuqôƒºôAc•{Hv5ËÏ+R~´˜‹ € €uV dxc6Åè‡%íMYúQ¦09§õ]-ÕÕ¥oëò§æ.}ß/qŠ”ÎQé‡/«dXån¢Óýòõº(R¾¸˜Œ € €uU tè3ºØP?Lý4ãEiÃhUœÍsŠÓµò3ºôe[]žÔÜÜ|"G×*.éʦUÜ¿…N÷zTù ©ºz\Ù/@@ø.Œ2%jZ#]H¹O—g=­ Ÿ4Ó•­u#k¹{ßOæâ¨|ý˜ÜLå ;êªÝó&¼…=›éL·u¬Ïƒ¾öŒ)_\LF@@º*P<Ω”¦úaÖ3ºT°I*J“Ö÷Õ•Ù/˜SùJœÛ¡Ÿ]5^§G<«ŠKWõ£©WùIÒñ®+»—yO))XLE@@º+pj|c]žfNÝ[ÚN×íHÙk¡ ÷éÊ„æªØ˜hÜÐÙ[UÚë7æÖèkM‘’./ªœwþCyÝïõõÂ(R¾¸˜Œ € €uUàü؇uá/U±äÏúÉÜeâšYqº¾{žÎk¡³óß0Oü¨kE‡TÚ¯¡®®žaÊV…J—}¬ÜŽ uìÝG}½,Š”/.&#€ € €@]¸d®‘*7o¶{eº)EsÍé|©ï˜÷‰zBç†=¬Â‘ U6³ƒJftVn§uêýç•?±²û=«ì×£‚®æöè>>(R>°˜Š € €uWàTê$]ùËsæÍvŸÖµä'U<Á¼GÔ¨Çt}Ä*Jx@½S^Ïût¢Ûo•Ûå!~ó~å½ýoÊJxZe_&ûza)_\LF@@º*pã‡s*X‘¤3ÃïS騿*ÑDÙ£~§¼ œÓù ú=¦œÞ ”enwž×ñ^å÷~R™ÝPÙÒáº~唯—E‘òÅÅd@@@@¢H‘@@@À§EÊ'Ó@@@Š@@@| P¤|‚1@@ H‘@@@À§EÊ'Ó@@@Š@@@| P¤|‚1@@ H‘@@@À§EÊ'Ó@@@Š@@@| P¤|‚1@@ H‘@@@À§EÊ'Ó@@@Š@@@| P¤|‚1@@ H‘@@@À§EÊ'Ó@@@Š@@@| P¤|‚1@@ H‘@@@À§EÊ'Ó@@@Š@@@| P¤|‚1@@ H‘@@@À§EÊ'Ó@@@Š@@@| P¤|‚1@@ H‘@@@À§EÊ'Ó@@@Š@@@| P¤|‚1@@ H‘@@@À§EÊ'Ó@@@Š@@@| P¤|‚1@@ H‘@@@À§EÊ'Ó@@@Š@@@| P¤|‚1@@ H‘@@@À§EÊ'Ó@@@Š@@@| P¤|‚1@@ˆZ¤fΚ­)S§ibÊÔàHJž¬ ‰)?a"2@È d€ 2@nË ØÎc»· MýpºlGŠZ¤2e(=}—¶mÛ›7oÑwßmÒÆßE6lTÕøÖ|ý­Ö¯¯>Ö­Û w¬]»^±Œ5f^p¬Y§5f|ýõ7Z½ºö±Ê|Ÿ d€ 2@È nV®Z«Ð±bå¹Ã~ÏÎýz͆àX¿a³¶lݽHåææ++ë¸23³‚ãèÑc:rä¨3>bÆ÷ÊȨ>:¬pãàÁÃòŽ2:öïÏwìÝ{Pûö™±ÿPµ±ÇÇÔ=–±ÛÍ{vhÂ’$uŸßMí¶Q›ÏÞP‡ñ>{”¾Ý±%j6ÜídfåÊ;Žeæ(Ü8rô„¼ãè1;/7z‘:}ú¬BGYÙ:uÚ÷())3¥¬LÅÅáGQÑ)SÖN™‚Vv–˜ÂVsä+täå˹yEb`@È d€ úŸôí)ŽeÝ>–›·¤iáK”šºT©K—™¯k“y®¶ãíØfÈSò—)êµî]M<;RSn| ÄKC5º¤¿†åôQŸô.zeÆ5dÖ0efçFÜVÕvNš9vÔ´<‘sR¡#ûxì8~¢ÐQ¯‘ŠV˜ÜrTÛçââS¦¿†Û}ßW¬\§«VkúŒYjßY¯¿ÑF3f|b®+Úªuë7G(ê§¡Ç{+~ãkJØÝEí¿|CíÆÇ눹<)œu¤í¸)Òg·Hee˜U¯ŠÔíd^ÿ²$d€ 2@n§ ìØ¶âo+R[Õ¢e¢¶ÞêÿÀþKîÇÏù»}þ.{ÊÛGÓf™â4S½ûPbÒd9VïõLдi34ë“9Îiqá2YÛ±µ+Q¶DÍÑ$ÍP¢¦VŒVòÕáwfF¼¯ÎÛÞÔ‹>£îK;ªçÖj³ð%Léïk;7§HíIQ«¸8ÅyFç¥Óö V¤øøíôp^ y&d€ 2pë2°cÛW7¯HÕ(‹üû·“¸eÌÎóü]Üb\Zô³˜ ŠÙfµâa¼Å0æßÃqôù»f|ú…æÎ›§þ‡›»ám:Ìÿb¹¥¹sçiö¼Ô'ü±Ý¾?Ý9Ï®DÙõQÅX¥üX²§õ Ïé«ßM{TŽ~ãdcQæ<½»î½0òY­Ûú]mEÊÐÍ+R­R´Ç\'å§Ýõ” O¤2E‘Š!¤·ú¿Š°ýèÿÀÃ#2@È ·<·²H-è§ø…¿ë¶Ž{Jq<v >Ÿ“k‹ÎS»5Êß1g¶ yŠTÄ}¨#Eª}·AZššª?¿Ó«FV^‹ï­Ù³ç¨sŠTâÒ‰J97Ò9Ï®DÙe¯S6À¬F%¨ÛÎwôü”ºQqCå×Tÿ­=Ôem½óÅ+ê•Ô·Ž)·L=5Q»=Å*°bÕU©öú¨]¦l™ï'u®ZÉ꼤òæi"˜ÿ”&¤U¿NŠk¤(bü—?2@È d€ Ø ìØ²¼ö)oAqVŠÜ•£4mi¾ö~ß û·§-=ÁÒ,.v~Õß­-Æ%š‚² öAûsŠTÈÊ•[jnÛs¬«.[Òb؇îëðü¬Søª^Ÿ-ˆÕöÃ-ƒ1>÷›Œï\Ë1zRÍ‚áØöXØÝ¹±„½&jô¹þÎJ”S¢ ßw®j5óq-Þ3ßÉÅæÂoõîú¶ê¼úmuXþ†Z÷móvnÍŠ”sJß2u6¥i©ó¹jujwr+Åu^(RöùÔ@y*ZÒEq¦X¥-S§¸.ZìÜ©ÏÜl"-Y-[&+Ísw>Šÿàä_žd€ 2@ȰضiY”SûªÊ†-ñÜÕ¢ÊSäÜbà-YÕ ×I-0?㜶W[‰qŠˆ·˜TŸˆÅ#P\Ü­àmïiƒÁºÍhûP}›U«h’Ü—p+­¶dÚ7EjðÀÑ_«çõѾM j÷ÚKjüÀZ“§ Éqú¯ÿµZ7¿[k§Ä)åýûkœHÇÖÞâÜ®@}PÖOMf> 7WýŸ†œè唨÷v¶U³ñ¨üZ¹³"5|G?g5ªãŠ7Õ~éëjö^Ó˜·K‘Z³r®Ö¬œó7ÜlÂ^#å=µÏ[¤ìi~îÊTpuª‹R+W¤v™Õ)§H¥æ¥ÛÏ!×[ÅÅuÖ"ŠÔ-_:ç_Xü ‹ 2@ȨkØôíâ¨×H-èXr ‘-n!p?;{†[ )F¾‹T-§õ…’àcwe(L)û[Ë\¸mÚש…[1óQ¤Î뮞½ûªm|_mÿ®»6¯¾Ó8ß¡;ÿù´wsœúwÿµy|—ž|2N‹æü¶Æßº‘Žm›¹o8×Bõ8Ø^C×õÓåò˲.Á”¨vzffMÛ4ÅÉÄÞÒtçڨΫÌj”½sß’WõD—Æ1o'æ"µj޳½¿îöçኔ[ þš"e Uš»"ÅíÏ)PœNÈ d€ XÿMà®Ú ž³µÐ½ICå)}îuL•Å`÷§à¶BJ¯Óê¢\±H¹e=B¡òµ!«b~Š”S¢<§$zWîb¼Ëáìä.Ú4¯£:µ{˹dgˤ8}üVÕ郖ƩÉÝÇÞwcéØvšÕÑyŸ(»•~n›sü+**ô—ÓÔpÄýºpå¼óxüÎêüµ)Q_™Õ¨Ôלk¤žëöß1o'–"eoî~ÄT¤JKËäŽSe§UjÊR«Væz(ó|IÉ©ÊÓúìé|ž¯—”:ßsOí ®@—ªÈŽÊªôb{j_œ:-ªº.*ôT¾|ó^Qvß7Ê„=7tµÓ™oIDAT˜÷’ÊÉ5ïfPì›g10 d€ 2@È@}ÎÀ®7ä­õ5l™ ø-ÕbLš3o󘎊ï¨1[̱7ßkÑb‚6›ç? ̱ó+ç?‘¦1ægƒsã:êóÊ¿!íüøùülÓRqñ‹ûag\åÏxÿÞtžw~‘9µ¯jŽóóv?"mÛý=žýµÛª}*·ü»Ípûl_cÕóÁ×Pé|}•ûçzEËÍà1S´ u¥,JUÊGŸDc“¦ê­®ýjÃHÇväg8o¶; £»ú|OË éúëNŠÎt>;sD]¾®:¥¯í¢Wõê¬ÿS§a]bÞŽ}}ÙÇó7Ýõެì|s*_¾2³ãXfž3¢©@Q •w髺ýy+%¥›rTö:§ªÓõ:›ëŸÂ\ûä\ •¤4óf»ùÛ“ÔÒ{zŸy~{˜âä-Röv½oÄë¼ñ.EвHY&d€ 2@në ¤o_½H¨^ZŽÏï(-né †@Ñh1Æ)ïí˃¥*P\ìß¼R˜ø¸ª`9¥(äR· U)S¸ì~TÎkaŠ]  x§»OYóÞÂ)qá÷!PæB‹”[òjîs`¾çfæw»¯Õy½îþ…¹ÚÊÔÀÑÉZœºL³çÌSÒ¤©JJ™¢Ä‰“« ûܰÑãôJû5réØnLߪWfþQ {ºªï®.ê“ÖYS%êbù…àêÐÔ]IÎuQñË^WÛůªÍ‚—Õ¼{­Ü°6æí¸%Ê-NösÕ5Qå>Ž©H¹å¨Æg³ªäÜ"Ê(4×:¹+L……ŲÃS’<Å©öÂä]}²_Fh³äqõ¦d€ 2@È@}È@úöÀŠT}Ø×¿·}ì3dŒfÏýܧ)ê‘0P½ú®1ú ¢NÝôüËmkÃÚŽí ™Co®{²o¶Û}s¼ÞÛØ^ƒ·öQÞù^(PG{:Ÿ¹¹„[¢þü‚Úê6'±dÈ]rŠSå5Q¶ŽZ¤rÍérv¸%È}ì=­Îy®²9Ÿ+&\!rËPmŸƒÅÈ-H!Ÿ³Ìc;2³rcGåˆ d€ 2@êvl[áü]˱¬{Dzõ[=åg„ÃhÇö€)F}&%è…QÏ97’°ïeW¡ìÝùìc{MTóMÔv`¼ö81#ÞíËÌ8ïÈÑU¦SñŒ¨E*+;×”–\³âS523s䌬êÃë~>–yÂ\ˆ}=z\áÆó|`dÆ‘ÊQùü÷æq,#ãp–2@È d þg`Ƕ¯œ"ű¬ÿÇ2ôÆzlW¬_£÷ÆöÔïûþÞyŸ¨¦]ë™wŸVüàNZ²ê«¨Ùp·søûlÙq(#38Ük ™þ`Ÿw<”);¼£©ŒÃÇÌÎTŽ óÙÞçC¾èÐQù‘Ÿ±ÿÀÕ6öíÿ^¡cï¾Ãb`@È d€ ú÷`Žcý>ŽáŽßÍ:¶ÞíìÙ{XÞá½Ê>ï}¼{O†¿k¤ö›e±XǾý¦ÀD{ö2;}ìÞsPÞ±k÷A…é»È÷{îãpŸÓvî2@È d€ 2@lv¦¨6Ü\¸×D…{¼#m_ìï#eW£ŸiN¡Ë2§×U û8Òpç»ß·Ãj«]aV¸ÜŸ9dVÁbÍJX¸ágµ‹¹þVñ‹ 2@È dàvË@¸³ßìktWÒöì͈þ>RöÜ?d€ 2@È d€ êXøÅ:]»~ÃéKqöÿÙöId€ 2@È d€ ȨQ¤ì È d€ 2@È µg ¸"Å’˶d€ 2@È d€ Ø3¼ _ € € €Ä$@‘Љ‰I € € €@•EŠ4 € € €>(R>Á˜Ž € €P¤È € € àS€"åŒé € € €EŠ € € €>(R>Á˜Ž € €P¤È € € àS€"åŒé € € €EŠ € € €>(R>Á˜Ž € €P¤È € € àSàÿ$Ÿ3Kº©ZIEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/screenshots/diff.png0000644000175000017500000017214410615044233024424 0ustar cmpilatocmpilato‰PNG  IHDRRXhÊ×ÕsRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.05Ãà\ó¾IDATx^ìý|×yçãÿy÷Íþwßõz7›d7yãÄ›ØÙ8»qìlÉ*!¥À޵)î–d5‹*¦:,[ª”ÕEJ$%Q%QU”D°‰ì ‚ @ ’`/’(‰”ž÷ç<óÿ#ñWãû»êª»ý›ð@@@@@ª’ÀsÏÝ›{Ü,¤Ô߈£ˆÓC¿yÆ›NSÞhDØl6€ À`°Øl *m€5k#¥“tíT£‹(θyóN:}ú´Løj% tk$ÖJ,¨ÔŸR¼wtlÛ%Ôñc'èàÃÔ·ë€Ü†°Øl6€ À`°Ø@5Ùk!ÖD¬X#ñ±ëbJ ©ßüæyGDíîßG›6õÒúõÛiÑ¢H`€ À`°Øl6€ T¥ °&bmÄI‰)ÖN2Î{£x-Ôž=‡hË–^ èí·VVåH¬ Ú8Øl6€ À`°Øl@·ÖF¬‘X+±fâ}¬¡¤Zºl³ØÑCsÖÑ«¯,AØl6€ À`°Ø@•ÛÀM¦_üòÉ‚×ã×¥®ûõo §B“¿ß¬•X3±vò©5kÚhÆô•H`€ À`°Øl6¨rPB§iÃ:u*¿ t\ŽË³ãú”Ö(eÝcŸ&½ø8}Úø*}Ü=>> Òà\úÈNï9ŸÈÏå¸<×Ãõé‰5“GH­^ÛAóçm¤Yo® MSÞXF¿¸á ú‡ FÒ?üÓ-tш{é®»ï£;núÝtíõ4}ÚòÈ:¢ÚÀþð1ð À`°Øl6(µ °øÙ²¹_Z8Ypâz¸>ÕçRÖÍâç½Gî¢÷ÇŒ–éøüI‰“*Ëõp}:kÖL¬x{öÚœ´þÖaÔûÈ4mÆ2ºðg7Ñ%_ýµÝüûô¹Ÿ¾@_ºz qÃsôƒ«FÒÑ?!꺘žýõOhÆ›+Œu™êǶ\æ`&°Øl6€ À`å¶;‡œ Á}Ç N\×§Ž©”u³ð9vÏ/éèwÒû=@'_x‚ŽOŸ@ÇÞyž5sç¯iÍ]°ŽVýø‹´õª?¥çþŠ^üÚ3V©'ÿ >z½výßÑY¿~‘~ôúfúÑmô—£æÐïÿÓ]´çµ¯Ðñ¥µÔùüÙ²Žyv]óÄû¹“}ö³ôÿÓ¢Ï|ö,ºi²µ?IzäGŸe/¡G–sÛx–~Õ­ŸÜWN_»ùmO?Mýxùæ³rŽÍ´-ÉñÆ-/ûó£g±LÒc^›“Ÿ?oü¾Yc3$dzï‰Ŭ×eâÚ£²µÏ|ã.z9oûNv>%>¾öK^w NÌÓº¶¸u™¶å×ÖÃöõçaÙOÛ^Åx½düœ¤\;Я“|,Þ¶“ÔíÍû’ï:U8óüû‚¶Á.Ž ¤é:…¾d÷;c—ߨ±ðÙºu7mÛ6 Òû•߫ϼMOjŸ¾ßÚÆõp}j,üu³ˆ¹þº'c%[zy5N+›ÆÐÛ·ðq¶ò~æ¢Iò8âÚ„Ê—è¸cõ=¾]:}U¯Î¤Þ=fyNL¢Ÿ1ƒÏ^NOä3&.Ãל‚“ÆŸ¯-²>û<ºèië\/¤Ç=×Û^ϸf-ã—û?'iK³}iz²úím;IÝZÞ€kJ!LP6ϱ(Ы‹»º/Á«u†Cg,vXµ¶öœ¸®OŸ¿î¤BJï“·î&)|z¯¹šú®I»o‚ŠÅÔ×çŠ)±-GD‰mœ—Ëqy®‡ë“×][×°fbíä©õMÛhÉÒ´X¤%â&–ÓšΧž{¿H{Ä”¾} õàŸÐ?žùuúÏÿë;ôŸ¾v1ýÑ¥ÒoÝ·”þøÒGè÷¾{+]uãOéTó·éÐ;_§½3ΰëQõ‰º§ÞCß´…Ô*qã¡ÚIúºLÄmg¶ÌîgÒòœÿ_¹Bj…¿C?üÓÜ~ëýxòBëæw‚¸Zi×gÚ–¤¯±Ë½L¶=N¶]O?åqûæniŒ"ûg<=}³ì*8Ù}g!•Ø>wœ¾çÃ.N½¦ñÒyìåR”ÿLÜøbã‘ã—Ïñ•°ÌbQwÁI°•žÁ“¯1\Ÿ»¼Àás³Ð68Ü©üÈw®5æ|NÒ–×öƒú趤n7¯ÿšR( ”ÏoÀ-·¬]ËÐߨïzìÏ’(¯Qkk¯R…%å5RÇﯛU{šâüq>½?nÝÖ}ºR#®¤¾k¯¥þ‘#=Éï™Ò?ûóry®‡ë³4’J-ÄÚÉRM-ÛiùÊ-´låf‘øu 5\ùmê¹óO¥7jï“_¤¾{ÿŒn<ïÒù« è¹ónùîßнà×ôo.{þóß_Oÿnù6½¿ìÚþìWz¸®å\ïŒû!µz·ÃÛîÛΦ_ß~…¼ÙáôÍÛçÓ´Û­›õ™ó>u±šx=%û9Ÿ~}¦µÍš£¶öyM“e¬Äõ[)!>´ínŸ,Ï™·ŸÖ¶q¾~¨~©~\4Îí«¾Mµíy•ÇïÃ…¢¬~¬‘åm.Ÿ¹Xˆåóé6‡d"¶/[9‰.dquñ}/ïvÎãåòëüÙ*wáÅîØ03kŸ;—“âÀã§êãmVß6Ó ß±Zc¦Õg·§Ž9w¿Û¦ÊcñòÚ{ÜAùs·çÚOHYíØ½ýȃ‰Rêœgñf[[µ"hìÂû§Ÿ?Þ>×§Ûqî8Ç;G6n:WóÑt[ן|Ò´ÛÏq®Vpný,®VËm"Ù×"—µ]•×mP]ƒ¼×Îo³ä-–ó5Îÿy‹sͲìëqî—]VÚAnžÀ¶/¾ßwNç›u>zûÂý¹h\8 yîê}¶íRÕ—Ïø L~v]íÜøÞ `å±;­­»hã¦'®‡ëScé¯[ ©¨¶Lùôºù^„…O×ÏN}×\I½#¯ÉI&±fÊÇå¹®ÏÑ3¶Vbíä©·ÓªµmžtÿÏFЯýWý'BHýÚÿðŸÐá»>G;~õtàž¿ #Ïÿ }훵ôïÿõIúÿÿÃ=ôç_þ2 .¬¥O[Σ•wþ¥U—x`••ÚiÕ¬ÑrŠœ(+Ûh¥Ø¶ÒÞö™3î§7W¶Ò,1åNÞDÞ¾Pœ-ò³<38ÿ")¬©}­4ãÎsäýíâÆžoÔ'ÜÆ74­"ßËt ‡3 7ÅÍ©Üw×ÁûD›"©²wLßB«V[ÛœÒO«ï =ýàr,®¬~Yí­^eÞflçŽwiµ(£ŽýâñV_LuzÊË>[ÇÊâmà·rÍKt±òH©éA+_´xHŽ­´jÆ$š äµoÞdY?›#ß(Šý,Ôî8Ã:ÎñÌLã4˾•ýàã—ãëë›–_M]šp‘u³vû,÷X¤7Mö= =#/ÃqÏxÀ²7Sþœ¾¨¶~Nã4» -Qo,&ÊVµ1XùÌÏms|ÉgÀ6P`±³is/mØÐ]pâz¸>u>ûëV)ª-S>U7k®_ ©ž+N½×^“üÓùø³)—WBjåZqc÷±®aíä©M[ºhCó6ZßÜIZøuM|b"=~î_QÏ5¿K{îÿ<í{ü ´ÿ¡ÏÓ3µBçþÏÿMÿî_Bÿè?üå¿Ðÿ?Gïþ² þIÓ0ºð'?“õmõ9u6™ú¹víbo?D™ç.Óúe×cÚæ´¡õáN!j¬v^¥Kì5Bk×™ëÔËË÷‚Ág>{%=#nn¸ŒS‡?êØÝzס)4m¯ÚöÕ[ \´ýv]7öä‰zÔØa‰ß3ÅÍßdYý0ô-düϸí]o í½m™™—ÝWí¸Õó¿m°Å篴m¯•ÂÚšu—e;¡õÆe¢ù9öqœõ€“k×™Çîí»ƒûð ‹ qNðÍ¿ íZ»v²cc^[pó¹^ß8óÚÆÈsÔ{u.Ùv`MUdñî;'Zç»s·Øç°xÝ8Ù×q³?AyyVœg¦YOÿÜ9ßsƶE;ïÖ»íZçï¹t—ü¡FØ{‹ïú£Ê)›Ëù,ꪷŽ/‡iαé㬭b^2o@Û,Àµs:Ç.|Çæ¹&‰zsΑV¿¬1²®3ÒNM>ã‰2àlÛ€¼—@Ø@9l€ÅΆ¦´V|?F&qÏaÌcoçzd}öùì¯[ ¤°:xŸ'Ÿ¿n[ðê¸ä)‚z¯^)1=oçUWÉäQÊ;ÅÛUÎÏå¸<×Ãõ­çºùûϾÇaíä©-mÂe·¥›6É´C¾._ÑDOÿô{´ì¬ÿL=×ZbjÏ£_ }·ÿ> ûÚßÑgþû×è¿ýñèœ?ümúpÖ0:½ù§Dí—ÑÝ?úKšòÚt».«NY÷ü‡è,%¤Öo·öû·MºÊRë›Ä~Ïçetï9J°´S³(?ÿnk*ü¢?ç!šÏÛîfß4móÕ1y_Ý<­Ô´Ñ—ÇÔO­k×/ÍéÇ WxûÅÇfÚfñµ’µŸoxU^§ËU°…µÛ"Ë;u\ö²¸™²xlÚb×!oî¶Úcê­×íCîv/—ˆýs­ñtøs›j\Õñé}‹ÿW=ÇÏýÔû3ñò0^¯XeµãåûÖƒ¶È^ÇgåM´yl&8BÅ0nv½±™Øãå¹Ù>çAz‡oXåø™Ç.ìØfÝy®}Ùçˆç|°۾Ϻ{‘ÃŽ'Xæ&{œ¥0áñ =GsÏ·¨óÑ{?LsÅõg£?MºZ#WS½)Ï–7è2¾¾ˆ©lkžJL¼óÒÏ’<¦£åxÿìéVzN³£ ¹-«œ r²¶S¶=W0ã1¹ôKDYùVÐ=Úõ§I•“6×!Î?»õÙ©UŸžrûàÝжÖgÎ_/®)þi‰òÚhçãý–缚ìüÊ΃Xlœäþ¸Å×.—3>Ø&%´ý{ïÍ÷4à.¥²;k×uÑ qORhâz¤ʾö×­RT;¦|ªnÖœ”ê¼ìrê¾òJ'ùEïó¯™Òósy%¤”>Rº†µ“#¤Ú·î öm½´u[Ÿ|åÔÚÖM¯<ø0=ù•?§E_ù÷ÔþÓߦ{…˜bA%ÒÞÇÿŒ‰°èœNmø}¸¸–Ö\ÿ—tíe×Ò¦ÖNQ×%R×+Ò’Çè[H­kÞI[yûâG½Û^¹ÆRM›Ä~ÏçU4ú\u#°•6sy‘Úšµ)w4R›S¦•6Š…q²_Z4z¸ó uNŸDÙÉWZ¿N󚪦M¹ý°öó ŠÛ/Ó6½VøWUfý\ÝÐ c‹*¿µÃÊù³âfOä·ê¶ëà›¹ÕÔ¦oã›)'Ÿ–WÛîåâí×ïÙÿ®5vò&ÍžBtó×ÚôôÍÀÕ;þS<Çïooîa¼^³ÊjÇÊ÷í‡rmѶ›K„çäí{XŒ›ÇfÖ]ÁûÖØõÆf¢ÆKz4ODÄØ…ÛÎRz@xnõê+žç©™|>䎩ËÙ²o>7-Û³<¶íqf!%ÏÛÐsÔp¾Å8sÎay-ò¥Í[5Fâœñï_bÙãY²Ÿ¶-‰söž·ù˜¬Ï| ÷iv$ëð1±ûû™Ë^”í5‰©V_|ç½*§lÎ÷yáƒÜŽÆt„Æ´Í|¾>ä@Ûêܵó/’mZk?½‘ÿ¬kÄdÙû:%ʸùO_?l¦j½âÝó ãb+l˵_0É›‰s/¡î)ðêÞ_X”ØXì¬XÕI‹o–I‰˜ g=©|¦W®G>—ÊîsPÝauè}Ðó©ºùž¦½£O ©ö‹/•iÛ¥— "¿ˆâm*ùÅoçrª®ï”®‘¯B;9BªcÛNê޹ǗhEãjzôç#éÎÿô;4óþµüŸß¦®+~—vÞúG4ðШÿžÏËÔ9âs4ûçFÃÏþzkælêÜ.Þ%ë°“x¿ò :×¾™Ù°i—µÝ¿mÊ/!ÕÒ.ö{>¯£‡†©mÔ¾s&]uõL§¾‰bo†MÓè*û¦z×±Ó*wÕÕ—ZöÈyöbmét·‡õéì»9l;ç÷÷c€^»Zï—UŸi›ËÃ:v«Ï e¬> £{ø&H¸?#Ë 6ŸùìÕô¼¸Ù“¬$k»o|s·¦‹¶Ém‚“<¢^·övÎ+·«ãâh>ÎÜrnîxÊþþÜZ‹3j¾[SßrÆ_µw='ŽÁ7¾9´ûéo/˜×JË6ôããÛðˆÏU_FÈ)q-‹BÆF” ì‡Vo,&c£Æ3`ìÂŽm¢å½9ûNõ˜ë¦Ú²s}ÌÙš~çÚ·óYgyÞ†ž£–ÝsY÷|sí,÷| :‡íëƒsýð£¦ív¿Xs?åù#×Ûi笰­ ó­tŒç=~Ÿ^A¹NÏs¾®?J¼ËsM;ŸÄç%aLýלH;h[;WÔµËúqã%ëºÆçܰ'h™Í+çšz ²Ï©Ïòùi‡å?㚟Ó÷c“d‘W»Fƒ­ç;¶Û€ ¹ °Øi\¶•æ/h‘),D9‹+•ÏôÊõp}ê¼6ÕU‡êƒ?Ÿ[·¥=Xø4‹eF›ú3j»ø2úÞ?ïñB+ŒK’1DÞd6_5¼ø `e±;‹–´Ñ;ﬗ)JH©|¦W®‡ëS糩î O—i»Þ†S7ë‘”b´éÂK©í¢KÄÊÄû¹—WBÊÒH®Vbíä©î®^Úਕ¥ÁGÄ{NGi·(Ô´a#M~áuºöÇ×ÑY_®¥?ýý3èsÿí›ô•?NçžýSºÿÞ'hîÜEÔÓ»Û*wÐ*k%ûýÆñtž-¤š;¸ûw÷Q³)ζÝR´´î°Ë{> ÎuüÅÞM=\·]֙Ʋ®v;튈iÓ¥ô6[ÆÖ:Bªs·ê§öê¯Wï¬ß×±­g“Ö/»¦m.ïñ©cpŽ; N½¼¬Ó€ÆÚP§db³ˆÊëðÚJ—Ür»;¬›N¹ß?vŠ›h³Óß7mü½ÓŽì±”Ì"Úã<¶=˜xyö)[Êoã5ûIЖnI˜³×Fâe;­4æ‰iòœ²ÙÞ„‹&Ž©Õž²i«\«p;çe¢sT Ú9¬§Ý/ç|4ÃâÄס¤i÷6Û6ì²=ÝÔ¼M\k캜ó‘?‹}º¶öXe¬:r§ÆYõhç½§Nq¬vy« õÙÇTLLµ>¹Ç¨³ñØým‡ä÷›8WQ¯‡jÇÈBµ§•Wùø<Éc|’Ž'ò'?*’™s?¢îKðjÝW!Aém€ÅÎÜ›i曫eŠúã<3켪ŒÚÆõp}jÜ’Öío[ÕÏí¹u[÷ÁJHmøþO¨ù‡RËO„0úiüÄù¹œ,o{¤=cŸ{¬!µsG;öžLG9ï9ñ¶ãïÓñÚÛÛOMM›…`z—^{m&½0i½úú,j˜·Tlh§=ƒtä(—yß){T”µêu¶?Cµ¶jÙ~D´ó~ÙRë„oÛBªƒº÷—¯åd0äm·=Cç§dü‡ü؇ÐÖÙ¶åÔºW­óËæ.×mµÚÛ†°?•Ìdžk'l`(lÀ¾/Q÷'xµîÓÀ`l€ÅÎ;s6ÑS–œ¸®OÙniê¶4 ©åÿz¡LJ )ïRÜW.§êàúT¿¥Vm°vr„Too}pò#:)¿ZéCùù¤xå÷Ö~{Û곕×Ê'’ÜîÝçÔ×ù}Û ¥ûmz¶S+§×QÒ÷‹è§¼|õ.G?ª°ÍΉrü9ÀÆ'\›)éxW!gÁ³õa+rŸ3eîÆ™´‘½,mSËݽîè× ¼Ø@µÛ€sïŠÔ^¿1F•{¯ÁbgôÃoQý¤%'®‡ëSöRŠº-íò¡RÓÎüB«¿ÿSZýƒ‹’'QŽËs=\Ÿ£l-ÄÚÉR»úvѩӧéÔ)‘NB§O}"Þ‹$ÞóöÓ¼_Å6ë•·q^õÊe¬|V9k»•¬2¼ýô‰}´É™6ÓI»ŽÙuqÙ!K‡©³ì}ÊãMQ[ÇöÊñßÔóþŽwŠŽÈlü4è±Öõ9iëáT3—×$0€ Àrl@Ý‹àÕº'Cƒ¡³úWHñÃ"¨ÐÄõp}jüJS·¥e¸n%¦X’¸«ß¶®±õk'GH±»       Àv÷÷KÁ"HO#o#?ó«zïÏãÿÌõp}ꯔuÜ¿ßS,„Tzã›Ö{~Uïõý¦÷Üo®ÏÿÇÚÉRìfãBØl6€ À`°Øl6l¬|P´c¥È2‡D½!é hS&‘ÇŸû’ÊçÉkÈÇåúT²ë–e´¼Î~=¯óþ (¯’V—1¯¾?¸L¯(«Rß.QF%U§¾-â}¯Ø¯’ª§w×A±ÍJÎ6­M½Œé½Þ'g‚ò¦:Y+±fbíäL탲…¢GäxE„„T  ‚rŘSR^‘!%Å„”-l ¤’ «(ÑdWR–˜‚’BBŠ…„”FRJôÀ#b B B BÊ:`6`v LíÃÔ>L틞Æg &©}˜Úç (á»ÀÔ¾tMísD“X#eRqEU"!eЦÕoëX>|,mõGÙÓ¦¹9åµ(~z;cý¦Hw¦H~j›ÝvõÙýÏX"**_¡ûC"ú1'ª_PÔ>Ið¸Œ&èFþ‹ŒÚgŠôç‰úgy¤œHy2ºŸë¥Ò·«÷a‘üd4?™ÏP‹Æ§¢òù"æDûsòiuz¢þEø³#ûµ¥aÃÆR›,cmózŸLõj‘Eä»cÏuÏŽØ6FÔ=†ZyŸ¥Ï‰ ¨E ˜ÆgŽôçØÇyLQûŽý²ÓñÊIïGŽÇh ÷w,m±óz¢þÙ"³'ÊNq#ýEy®ôýz‡7[c³ÙP‹ÄU¤J°v*¬.Çó¤¢ôù_õ)y†H~zä?óÔ=ޏg'µvI¯SEêÓ‘£óå "O;†ýVD>;©h~üTd?­Œò89÷|þÄö)0jŸŒò§Gé xï‰ÔçFúãè|Vr£ú9ï=‘üìw¢õiQùôºì÷Þè{VT>=’Ÿ9ŸŠÞ'òЍ{*)ï•ùÏõhÙýd”¾Ü¨|žmbÊÛ>;y"óùE“ɯ˜Qûöˆz9yꌈúçNÓ; ¦íRܨ}z¤@UÏ>QŸJž`v;Bí1%;¯'²Ÿ)RŸGTiÑúœ¨}¹úŒu:QüÌúþ6]DÉðé¶bÑá”-³â¾øììúƒèñ-=Tz¥©MôÔ0ÿùf}öÔæô))ÒÆxE‘a„”=и BJ†@‡’b BŠÃCH…:‡Ê a!e )ßT¿!ÅbJ‰£¡R¶ÇÉôì(%`Jæ‘Rbˆ…JRT1…Ô [8µÛÂKïWès¢‚QÅ#&¤üÊy¾TJ…{’”Pòx–tWy¤Z-N«òn¥É#!å ”õì)x¤à‘‚GÊ}†{¥4O“ñÙS†gK™ž!!”ÆÜà1ò¬•ÒB+±d|¶”^¸GÊ$¤L)÷JxklÑQÐÔ¾0!å÷–±÷IõK ÷—ýáã7ûŠ›Ð#%DHû¸a4|œ]ÏÜ‘Ö{½ï™}ü¶Ðq­±u¦þåN×Ó¸ë}€oÀÔ>“Ú¢{î 5Åój,fóÔ=mjß–§¼}ÜRèÔ¾(!Åûþˆ1Ø"¦ª©}²ìH©yÞFÎvCšcjŸÅSû´‡öjàå)~ÚÃ}1µÏ7ÕSû¬ˆ~˜Úçyˆ/¦öÅ{ø.¦öÙÓû2>µÏ$ LSýÞ)} ¢.Î R±¦öñ µ Êk¤ Œ‚…”¿}9]Û7ôólOU©§ö±øâ‰oîÙ“4WÜ\_?W›>xÂ>~åRÇ/…†%žXˆ]ýH«ççiuÅR9ã#D>µO 9BJö[ ^Ï$ß³8ý³~ü^®§ò­‘âí¢.)¤D׉6äš)%¤äv[ÄHÃíXëª[Ùf1ÖHå©Ùt÷g¶µ>JŠ£ëf‹¶Ôv+ß–§‹ëÞv…”_#i¶¡oöuâ}1„”Éîät¿·i$·§Ô«D—.¤„ÀËâŠ×KÍfQ*òG…B·×Va¯Â)%¦ ¤ ¤d8t=$:„”ô„y¼X¼6J%ç½ú(¬‘’ñÅ)k­V†…”imTGŠÅRœh~… ©8© µT`Â~ÞÔ{ù›òHùƒK …rÄ’åM’ž%Õ?ãZ.‘Gˆ¹×[Þ©¹â&{.çc1f¿l"–GÊÒ£´FJ­«RA(”ñˆ-µ¾ŠE‹±Ã4‡¥Y›ð{¤Ôgÿ?½_â¦ÝAJÜ8Þ+KÐÈ`v­Ê»¥‚Q8u³¨Ô=(‚{–t”R6GÇd‹f+”J¥R,ˆxªŸ a ¤[p)/“ôD±0q鉚+ćì O"Dx·Ø3åDà ˆàgÚï‰Ú§„”¾J‰¢‘¥ *ö&iâé¸?¯_H p\KR@5Ø‚Š‹!*ŸžßÙï‰ÀÇå줛³ßòä©ét®`²=@2?OQTA&l/‘-~¤‡Êšèx’4Q%£ú)!ÕÊeÏ¥§[µºd×|BEíÓ_UD?ýճߚx@OZT>ÕÏí¯?úž!*Ÿ˜2w€“ÝOêç¬ÒŸ%¥EêSåT=Ú<XÂInÔ=ïvïCx-ÁÑODÊÛïKnä<Ž¢g%ø2DàÓÛQeô|î6·NO„=ÑGíó$-žª+hx°‰ðH}Æè|ZT>ÅO¾Ú}2FêÓ#ùEõ‹ˆÀ­Ï‰ÎÇu’“ì:ÏïÓÚ)ôỪ^O´§O¾È}v?'dº½OFðeüI䧯—r„¢^^‹œç‰’WÂíNÔ>-Ÿ'ŸŠú§­GÒ=EªŸÞ}Epx–”) ~'* /:_œ‡ïêÑüLSy¤òRÂ[sƒøÕû1ÕnH„”œÚçFïk/¦9ñ”¿œ5Kì9²ÖCCHÉé}<¥LNÏbÄPöT3{»GL‰<×?W¬©Ú$ûÑ6Nˆ(áb/Õ)!®빬¶ì bj›µ&Êšæg­u²¼KœÏ3µ§ ú„”ZO5üi±¶ªBJN»c“=åЙÂgO9<¦úëŸÚg‰îoƒ{Þ‘îtÏ’îáÒ…—"Ä™&"¤8üyÃuv=á ºŽ£éÉéqO¬âõUTÁBê¨%„xª¡g Ÿ¶ÛmUSû”溧6YâëˆÝG1­Î/˜ÔÚ¤‚…”F5$×=)ůABЧöɰèRŽøaÁ!e‹©¨çRöÔ>OxóÜmRöT>G”AHéÓùô°çºàò¾‡ fiRJ E='Ê´¿ŒBJ(Cð=šŸ1êŸæÁ ôHå¬Å°ÔBh±W* ØDŽ¢aœ ã„h(šò…ØWSórƒJØS Á&\!uÌ[ÄhužÿdRB|°0R¶ääw˜×#e=GÊò@ {J)­ßİ”ÓûJ"¤”·I›.§¦êI/”o»ã‘Rž(F]‰*¯˜*žÊ?½…’.¤|c=[\ðHYψ‚‚Šzf”ò.y’ìíðHY¢ B B )ͳSdïTÚ„TÔÚ§ ýÅR†©z9^+µö) oASûœç=ù¦ö™B¡ëè|EòHùÚPkœô×Ü©‚ús«ø»îƒx öH„˜ûp]Íã¥?gÊyï>;Êô_Ï{ý)ÇcT$!¥¦êSÿô Î{Ãz,c>ö.Yy½×µD‘³¦‹ß³'J%ç!¾Úz'ç»Z]ži}öÔ¼ˆ5R‰=RJ$©©}š8s¼FZ]¼9ß5Lá ò8%Rö:­Ã‘ÒÕ¦ö©éƒ¦ˆRÎvMSòrÜ+…¦öÉéx˜ÚGðHÁ#ešˆ©}ÖT:Lí+xÒ½SYR¡äU 6;‚ ¢öIñç™Qa‘üJ¿FÊ´ʰÍÞÜ´®)(Ø„qšžæ RuEå+ÁþÀõPBpœà¤·iÚfèSÐ4»"¢'Ï&}•ó@_+ŸÌ«ö×E¹Ó÷މ|*™Û7¬¡£ã¦d·å™¨µoœšgš²§?Ð×´.J+£Ö>éSÿxý“Jj»g”-p¼ÓÝ©ƒNÚº)o˜óx롼Á&rÛ›Øëæó®§A!TÈs¿'J[uT¼W)r”ó<)ëᾜÜéxöƒreÝáûõ2jº¡©}›VÜÙ®y¡<ûMÞ©ˆðçjÝVÐó¦œýZHó PçnÈswM’“W/oxožÒ'že‘8$^UòDëS¢ÊÎç o®B–‹þȇòºõq½z^ÕŽÊÜ Ýþ\…:wœshs-Ô¹½¿hW¥zxtÃûýb'½žàPèÞ0åzÈòý"d¹“´èƒâ='Ó³¥ô2*Ÿ|5…?×<[¦ý¦êz>ÖÜ&]kKˆ"IûDÛNò%:}C«»eœPåz(óÀòvhsí9RaÑþöжœ¤…W)öîû¤‡#WÛÝmž°æ*”¹.€ì÷A¡Ñõò¡ï=ž§"€„?¹k¥D›œôÑ•TÞ ï’ …®ï7mó>3J=OŠŸ)e¥Rx¯<áÏí6¼!Ñùá¿*Y"È#r”§KÛîY—åÏïûlENýš-*DzÁk¤<^*)O°ˆHÑ!%E „”_L™"ûAH±Èb‘äF탲Å„”œÂ,Ä ¤<Ï‚ÓýLÏ Ò¶AHAH•Ø‹!ešÖ!!”7 a€€€€€€€‡@¤J©Sû0µSû0µÏyÁ&Äú.-è‚MXÁ,lÂ`2Ÿ)!Ø„w-‚MXÏyB° +à‚Mx§Ýe?ØDœpè¾<'Nˆhr" 6%δWõï=}[ "ð™žë‹¼ç´kÇq-%zø®!šžb#£ý9)¬vd[2bŸ‘Ï•OÞ§GèsÚÑ¢îå5Fö³ÊӒЍGåÓ÷«`zô?o^pÂt‚#噮떔]ŽËæ_(uç¾ZT>ÓC~Ëh~*™Ö=…?ÿ)*‚_dÔ>S¨ô€AQúbm7Eð ˆÊçF¾s#ùE=tWõ!*Ÿ)êžiÏ[ÞZk剚'ú¬êPQýô¨}z^)г_EèÓ£ï™"øyö»ùœh|ZT?'Ÿiï€V§7‚_غ)ýRáì5EØóFýS,Ä«­oP»“Ä6+úž9Â_èv޼g'=0„Ú¦¿zGXQû¼É ¡EêÓ£í…EåÓ÷™ÊpàŠØÉ$€ôò†ý¦H~QBJßïW":ß>N¡Ðµý*_pÔ>7BŸ¤Âÿð^O qÏÑ/h›¥O0 %o4?7ÚŸ9`„þ\Œ0–ð<„W™®Þ›#ºïåùªä>Ð×ÁO +’Ÿþà_o?SÔ>-úŸŠ®§¢÷y^ݲžuQN„=7jž`9É];eŠ–ÕÏÁÏáÏÁOEøÓó©€Z}}â½J*jž1’ŸÌ§"ø¹e<øìºœzdt<}=”õÞ»?f”>_8qOðgد¯=2…"×·÷kõûŸíä/ëYçdˆðg*¡Ï´¿¸)))%j ¡Ê!¤”è‚’aÆÏŒ2l !å„K·•.Þ ¤¬ç¹é…²˜^BÊ “êBŠ•Aœi!ѽ)XAH±`ñ…T‡ò„5‡‚‚‚ržUey¦lO76Q{ûÚ±£ËNÛÅ«›º»;©«k›ÈÓ*ò6ÓÁƒ{1åÊgˆÚ§{šLQù ÛXDM{?=róõ4vÔ-2=Tw=½ôàmRL­^¶ˆ–¿;[¦ÕKDD>_T¾ˆz|{ÿƒ%H`€ À`Û§ôöô”¸sô>-H„1BŸ7¬ºª£_L%äÄ!Ö®Ý(<´Ä”7¿÷±ˆâWwý’tB™ðFôÓ¢ò)Ï•QOŸÆçlw#ìéuqñWÝä¹ä›âg}SN;ºŸî-cÅõ‰©’ ©}G÷ÐùËF‹ôý`åãÔ°cUê…Ôš5+‰ÓÊ•KÅÿ¾bªôBªsï~:WxŸX3HMƒÐÔ¶Côì†}ô\Ó “&Š÷õÍû…·j?½ ^ÿuæNúδí´y`?}gêvª[2@_y©³ !uäÈGD<8H{÷ÐÀ@?õ÷÷ÊÔ×·“z{w8i×®^[L59bªØBŠEÔ›ãFÓØÛo¦'u=½ûê34不iâwИÛn¤7žz€^ºø§2½(ÒŒ›F@HáF¨à!kü°€ äkJHõïÚCù$a¥Rº˜‚ªn¡’ö£/¥b±¥ÄÔ†?o=°Îm¼—þu壴h`]¹~B&„Ôúõk„(Ø%½RìÚ¿ ¦˜*­buÎ+Û©¡÷}ZÖÿžô<=ÛÄ"j±xRÉ/ª^Út€îj짯¾ÐA?ŸÓKÛD_©ß–·ò‹¨={ú©§g‡ºÅk·GDY‚j'Ybj‹`ì™*¦bõÖ³ÓÄ{o£±·Ý@s^~šV7¼JëæO¥·ž‚&=t=yÏmÔþ›_PÓƒ×ÑÆ~Aî»B B B 6€ ”Ͳ ¤XL™ÄZ”Gêß^±~8÷;T;÷ôO3@Ì>†ÍÿgúÁìoÐoŸKÌùW:sÎpúçùߢXðúnÃô—óΑÛ8y"óÁ#•vSöþ… ©å+7Ђw—Ò²•ëiÕºZÔ¸Š.ZAÝ;öäN×3x¤”xRmøÅTI×û´ÓSît>fÇûyšŸ.´XLµµm–k¦1•§ÜÛG ¥icFSý¨›„`Cë¼AÍB@íj[M}m+©uå<Ú°`:Íxözå¡;éåñOÐ[W_H ¿º†‹ºgÌ U®¯•:ê><7h‹(ÿß‘£Çå¦Ã‡—ð:Z·¾A<€v!Ä~ý† À`h+WÏ ž¾^Z³v-=ûÜD3v¬X[´N|¤›æÇ|¨„Ô7¦\C/6Í“ëžÜ‡ëF¯‘Rk›òñHýîEHÄé°Hâ+qÔ¤AúéÕ³è ß|›þà¯WÓ¿ù»·é«—Χf¡ÆGé=–Vî¡–?«Ê–Ž){oMBŠ=Qk›ÅÂîY@ùSÿàzWxª<¦r © ×>Mg OÔóÛçЛ}ËhÊÎÅôšRk[釫§¯/¨£¯¿{}}Ñíæ$RÈ€% 6±bE£EKh™– o“Jìyâ´ví*ñ0Ä=Žâà ­­›¤˜Z½zyHŠÒ©>vøï_í¢ÍOÊé{O­¤§×Y‰?_-¦ë-öõ%¹O¤³ÄûÞÝEÏ A5NìZˆªqÍâµE®6qA?ø!}å…äSû6mj–¢’EOÓc‘Äbóĉ£"ï½ÓúxßÞ½»éƒNaº[îW^*KLm¡M›Z,!“‡b5oÒ4ëÙÇè¥{ëhæsOPÓ‚)Ô´è-êm]!„ÔZÚÝÑD;6.¥-ËçPÓ»Óhú„‡èõGï¢ê z§þ ÚÖ²Rˆ©ýŽ˜*TH±hR©§¯ß¹°ð¶Ö¶­´hñ[" ÊbÜDUàM~µÿ«=XU¥ÙÿH¶|ÕjZ·n ½öÆzó­i4mútzé•WÅ}ÃjZ³A*ÃiC%¤þì…ŸÐÿyë×tÞÌ›iúæF[L•XH]±Qx¡>¤Cô-Þ¦óëÖÓùßïÒá¤bÏÿñ뺢?üúcô¥¯Ô‰ïO±Ah©ÍÿéßBH•]šd«&!õNƒ°7á‰ÒÔºí´¬}­ìè§;÷‰;š©£SÌBë d*—â ç6ÞCß^:š¦ö,¡½RD)!µf+ué¥ÇÄ|'Ížª7}‰·ýP¤õ´ðãï‰hsVÒ"üÙB˦\hˆÚÇyÙÛtHÜ8ó´4N‡´¤¶ñ+{J8d÷ºõ«iõšâ¦•R`­X±T¡ˆ¿n*^u=jžç˜„À¸§q'âéž÷è¡5{éQ;A&~(˜Ÿÿj½´|“\ÓÅi²xö+[é‡oî û„‡jLËAzRˆªÇDà‰gDT¿5ƒBHMl÷­‘r£òœ8é}:&¢65­“ÓQ;¤0bõþû'dRbЧðñ~öê½ÿþq'1S}º___57oípý2±¨²Óqñª’?Oç[ ¦ðÍé•n"êÚ0ÿ !¢Þ¤ž-Ë©wË*:¼{è¦][71µ„¶ÏTóâ™4uüCôÚ#·Ó[ã¦Ùb]UGó ;4ºÞ¾;ÅPWQ© !¥ŽÚ§¯›úüEëåt>öH]9zýéß>Gû?"¹íãOÈ»t9¥Oü5wŸ õ[Ú,ÔG'hËŸÖ©ÛªßJ»´ä´%¦i yÔ¾ciáüdÍ;Á_° p¤=Ý»4ý­ùr ©-}ƒ´hK-ؼ“–oí§u]{¨yÇ^êè^äu[C›ëõ•lTë`'1÷×tÑŠÇ-/”Roì\”“^èšC?YýýãŠQôÝ÷ÒÿÑÒ?.¿‹žÙþý`é#%R,†ØKÂ!¹9uk‰?³÷‰"¬^-Ä“ˆÜ·vÝ*7‰²¼~jåÊeÒsµGÜÌç÷¼©\q&¤j_ÜDÓw¼OOµ¢Q+öÒ="Ý·r/]4«‹Îû6lÞDûöív„OŸðþ|gòf™¯ƒþzf/}}V/ý`ánµá=±ùX¯&„ÔøM‰…‹Ð}ûlÏ“%¤ØÓÄ"ê½÷X0Ybª¥¥Ix®¶JO”JìµZºt±ø2iëÍ:é~įtI…‹¨E¯Œ§ÅSž§×ºUx™¦õs_£ …ˆÚ´Œz6¯ ƒ»:èÈžn:ºg袭+©eÁk"ðÄ£ôÊ£¿¦'o¹šž»ã*š!ž75s̽Ծ~©XÔ¿zõ5kj_˜âýœoӿɑ_ðB*‘“ð†ªcÂßÒð î¾ÿ³ñæ#ÉÍ`NÞ'醚ª‘é‡4_õ—ó9Ûk<} ¼ŠÝѦG¬ôAg»îÞC]¡œ‹ÙvB{ð³vúYÌ>EÔUt6qú'O–qŽíÆzlÝsÄi#Åyxì¬s;æ9çXŠ56ÜV‘êzø©ç¨kûVºûá§r®ï=U/¾7ÐÏÖ—UHMhžFou4JAuïêè[ÓFÒÍ‹ÇQóÎí^AU$!õ¹«÷Òû§ˆî}®‡þëOSkçGtRЧ=tT¼ûú,á$¼U§>¶nÅùó'ÂOÕü5 ©ã[gÐŒ)Ï9iáVK¼åBª(‹Y‰IHMž6×RJD­éÜMí}hǾ#Ô·ÿ(ulï¡…K×—OH=×:‡ÎxófúÕúè­]Öz(žÊ§§)=‹é¥îùôO+î§ï¯M?\ý ý@¤ïÛ¯üþŸWÞOOl›A?ZôpI…‹#RÛ·wä)Q|±cÅ‚‹EÓú kdÚдVz³TZµj956.ÓÓ6EL… ©¯>¹ÞÚ}’êÄ3£nlÜC7Šðå¿\¶—þf\½³f“ðàtDÑà!ñœ¨—6Ó_¿ØN3»Ÿþ¦A¤9»éoçöÓ7çôÓ5köÓ[ý'é+cš )fÇA8ø™Q,¢8uwo—aÏ•úàƒ÷DŽõžGD1+ ìÕb†¹ëáiI…Ô;§5s¦ÑÔG~MÓÅ=öDmX æ¦ ¯SÏ&á]b%Ä“J,¤íÚFÛÖŠµRs^¦Ù/Ž¡iOßKOÔ]Kõw^EóÄçéBL"¤ž™8Ù(¤,^AœÞ]"®ˆ(1·Þþ@ê…Ôüÿ–&lsÅ€ÿs±…Ôükè†ùV{ò¦ëÆ'­›ù?t¶¿ÿ o¿òí‡sc§ ©À> ¡ŠÅ9ÎMf‰ó8ý,Ò ¨LjºBÙäÓ8eâä‰Ã:αÍ÷y…í×”Ó+ç¸çásXû¡$qyíŠbM!}ÐÊΘ9]®Š“¦Í|Û#¦‚Iü€×úpê´ÓöέòæžÅ (ž"Ç7ý-7È´qS“Yœx;ïç¼K–,”Â+Žgêî»ï&rÈk?ä–E…jß¿¼ÔJÏïø€~27]òî€L?žÝKg=µVLAÜ+ós9)¢^h¡¯<ÝD3£G¤ôwoöÐYo÷Ríìz´‡.ZÐ/¦ú¦¯<¶6±bñÈQúX<©Äbˆ¶»]D(²Ä“žNˆõd¥àä²Ì·¹y½L’­ð\å#¤f¿4ŽÞš8†^yâÚµ£šÅt¾M …ZFDj¡=]å)žâ׳e:±†ºÅþö•³…˜z‘Þ|î!êØ°”½éçôÔ7Rýƒ·$¤zb¢QHõîÚM»úÅóAöÒÀž}t÷ƒcc © B`(û¶„Æ4a¸»mø„'½ŸùWíáB|hy<EywÔ šôøøÅ‰ÿfÇþ¬ß¸Èrꆈû$Þëûù&ЮWÿ5Úº1ôC÷æ*ð‰Ë)ŸçJÝ€æ¶t#æ;æ >ä00xѤàóŽ›§Š·Ñ+gsôÜlñWmØL¢ÆÇÃ(ó›lÅe7¹V±ò£´½:²lÖgºP²û™cÛöc‰e¡ÏAuªvs¼~ÞW Ù¶ÃH³?Sþ öb›>×;î×Àc²Å¨f{ò<𰱎+Ø]¶7ˆ§ÔµÆµ¿§5ÂÖs„`¡Ç¤_sôë`ît9Ï,¾qÌ96m|Ì× ë8æΚù*ÍŸýõo~…>:ø*=óä}tãUÐîÆóhß’óéÚËþ™î«û'X>Œ§\ ¤„@rS_ß.1â*ºãŽ;E§Ýž|±…ÔÃÿâ©»–§³¦^A?n¸MÌry†îZ6~0õVúúøËèáw'ç-¤øFVŸÚ÷?¾·U™ø“ëÐþôméib!%UGèã?ùÞrG±zöËÈ};þÛo{„”éž/ljߺ)3¨ãXÀý9‹ ÇSµ˜T¶XÛ…‡‹š.¤<åBÚ-Š\@%ALBêÉç§Óâu­´j[¿œÒ·¡{/õˆ,¢>úè9z‚îór,ÅSüŠ>µï_fÝKg¿q3Mj/ÖBÍ¡IÛçÑ‹B4éé 颵Ð÷„'Š‹§k…pºe݃tëúé×"Ý.Ò¯Ö¦§Ú&Ñ…³GÇRŒÂ#¢xMUŒ5Rìáu:<}ÓV‘:¶¶R»ˆ"Ç‚ˆ½%ÖMþ9 mó–ÚÒºQ Ø$×Lq8tÞ΢€ß/Z´@……±„ ]Hé‚)Ì#õ Mtï¶èÌ·ûé̆Ýt–H÷r3Æp\þî†vú§çÖÒ÷&®¦ï=»Š¾÷œHϬ¤ïMXá¦ñâ½HÿçYËä¦è5Rlƒ#õ±hâÔ%B»r¿õë×JA´{÷.g½”µnê8-\8_ NÅŒ¹1[ShbÍšU‰=Rܧ…¯¡?þˆ¶¯‹ …7ê`O+íÙÞB…ÊJÝv²>÷lYIƒ=›iÛªÙ´lêxj&O}NxÅÚ21æ…x¤~uïØ!uôè1aÌá}:yRLcøðCâ|±¦ö‰/nçFÿuÚžs­ÝìIA“{s>_ÜôÊés&!Åe”GÈö YŸ]±Á77ˆ:¬þÙSäT?t‘å\KÈÓ®s“ ÜŒ7þ¦ucÖMŽÃÊÔQ—Ó¶>m0ï>xÛt½h~b˜â§øúÇQã®{ÇØ+'ù;ùƒD_Äø˜xê"ZÙŠgÌ-¯ Ù|^$[X䌃Ÿ·?Ÿ²í€qSžI‡©ÏAuÚ¢ÖèíôðOßù!ÇYÚLÐø‡Ô£Dß#`N[þc‹Ù¶c/9å}6©·¯Øj?„Èñ7zÆ ¶t¾Û6Àv펣_¬›¼ÒAœML¯¨sï ÔõϾ.šŽ-ôšãœÇ×¾e½òÚ«4á¹WéØIbÄôw÷]úçï}‡>J_öqr>H³z§Ò³ÛfÓóÛÞ’iRçÛ4ºõú'1mï"!¢®êf! îÂéÞ ÒƒMÒC"=Úü ýfÃhz¦m]<ëþØBJ‰)GÄR,‚=Q,Š8µŠÔ&"ò­³§òñ ¾I©”“ø³HVèî&2qž\+Õß¿3¶RbÊïÁ R_µˆ~Ýþ>}ãíÝô•Y"MÙI_‘ù¾ù@#ítù„>ýôSâ|C/¤Ô¯ÐQ‚Ê?}ÎûYz¢æÛ(9EOÜÀ©uLö Ç|}“sSãk7LÄÝ,kk¶r§íhž*cyà z¢>ønãÞH›¼~ºppnMÜY\*Ö!"N»q ŸDBJÝàûE­¯Ÿq~\!%xäÚÖf–…Tâc ,A¶ç÷íÌà…6å W£G7hÊŸú$È‹rŒ‰…T„GÊ$Lƒé?ÅR³æ‰ûŠí;¨­c+5®Z˜æ/^JO½ðºQHulÝ.fǘӔ)S÷éBJznºwIïJÊ»ô§#Φ—›'Òí‹o¦›æ~~úÜwéË7ü=ýø¾‘4é7hÙ ¾‰ìµÊq"uŠÄ‚%®GJ )öqú“K;åT½Û_ÝJ5_\Lw>)¢& á$#õ‰ì™ZÞò)}õkoÓïþÅ4úâÿžK>¹Inßò…ßwúÁ}Ùn'î““ºx»èŸÁ#•3µ/Ç“d‹(!Õ¿Ø»ÖÊ_ÖJ¥Fµ)!ÅÂgë¶>Ò\¥mâýÒÍôö¼åôÎ|^».ì]l{æ…)tçÃÄ4Ò½´tù*ZÜ(îµrz]ÅRvr}Ýààn𵬾õôÕô“FRÃÎ'èÙŽ—i´˜/ü¯«î—ž(Qo|ˆÆoy„^hFéµvoš²í1åÏ3¤¬çI‰Ðç2¹Ï—R‚Äô¼)î› •î.‡„i±Lb*—˜Â§§f!¢ØcÂ"+,ñZŸùóç!±Ek}RÜtœ×2ÉüêySb:xÏ鯲“Ê'_EHð›Ç7ÒÕovÓ+ÐW^Ûa ©×ºè¯§vÓßß³ˆî™,B˜ Ï”*7xðM_ÖA¯4¶Ó 1§s}¿û¸g?uÏo8@»Dó={…”_©Ï Ì“k£øIí,‚x${÷ØëÄ<*\»ÿ^é©ã`ÖörÔ‘#‡¤øÚ,ÂÃrâi},VÙ£ÇÇè„7WÏ“ò¿ÊˆzÞôúc·ZBJR,¢fÜ|¹Ü¶uÅ;Ô½n.íÜð.µ-™IÇ y¯Œ1 )Ñ÷c*ií…?ÿîUw;BêØ±ãÂVßw¿çÃe-è&ÞTŸgŒìu9Úš£GÊnß±Ÿ(Ï‘Z`ÎØFL sΣˆ)yv½îÎ<§öÙ(£g&¶ç'¬mß®0Þú1ùó…ypÚ®§nñY¤?ýùÙôZËs4âõKèo5œÎÿÕÅ4þåi±†ÔÜÜæ)Xìò",4—åøÊi{â•“ ƒîe!ÅÛ¶³ÀéÈ¡&ˆöŠçŸÿò*úìo¿H‹Ä3£N’åÇ⾎ÿ8b{£~÷ï¡W¦6Ëðè-¿óûÔ)¦g9IÔ×éKÛ:{Åþ>Ýþ<'Ø„g*^Ò©}Úº'ãÔ>·¾Ô¨Š*ìˆ#¤„ˆÚÚÑ+ÖˆŠ×‰#û~ê%GL-ÁÁ¶vºeÛE]ü™Ÿ/Uœ5R!ÅbæÈ‘AZ¾n)3ö*:wüzµõz¤õ!9¥ï&1otËÃtíWÒ׺”¾þèeÁéÉËK*¤æÎ}‡æÌy›æÎy''ÍÛ8q ö<ù¯÷™?¿A¬ b‘p ¶€rJä)¤Þ]½‰¾-ÓmïѹÓÄ´¾—…z½›¾2u'ýµ&qþ„ ôÏ÷-¢[ë×ÐMb]Ô5O¯ Çßí Û÷Ó¼®AZÖ;(æï§ûië¾ý´ºu‡˜ì‘ Rï¾;_xãÖI!ey£VËçmñs¹XHíÛ·GŠ(5§²âçr±âr76KÆïY¸rþ|…Ô üÂR[VÐ@Çž¦¶eoKÑôί¯–"jþÝ7Ðô/•ÛX<µ‹Ô±ì-j™÷:?²f>{_q…”ðF?Îaß? ?–ž(ý¯`!eߌ顄åÚ¾é ¡ìýµÖs#§ •è°ç¾)vk·ÔT$a^îôY <½®Àò.Š×Â'ûnÒå:^Ùz Ö±˜Â.ûëuÖz†`7UÙ‡èí\·:VgŒ|7®ÆÐÞþcôßú§ DeÎú½NOÿMSêrƒ{˜úé±=ßZOØk5.Ab=ÇCh‰qO›A}û hœüç‘)Ÿ¿=ƒÐ ÙoúÃæ–sl¦µH&tLÚy ûeRÚqÙcà”M“Mz«\gü7“SÜñ SaÂÐÃ(ì:lâõ·Ä ñœ¨…B,M~c½:mFNz}úLªùUºû±§Bª½}›¸ïHžt!Å"нJzbˆÑ_\s6}gô…tæÍß§Gë'Мy‹ÄwøFñ£§Z"qY'Ùå·‰°Ð\6©bqÃé÷/é’Þ%sÞ»ç´˜Úø(ý?¿÷*=üd§W§ÅÄ?±ZE®£šµåcªù/?¤is—K!µõ?#Ä©8•D}Û|©c[ØßkRü½þ$ £lÅd ýô´ÕçŒÙQIyúƒ„dŒÍœEdòÖüE"¸C-[¾œ,\Hï.Z”“-^DoÍ~‡›à}–” ÞÞÖ!‚^%OºbÕÑÉâBKB ± úÛë/ _M³Ъ5Í–àbŽø³“ìò"±(›TH±¸áô»¿Ø"Eû¥8íþ€èG·,¢?øâ“ô_þ×Kô_¾ºŠ~ïìFú±>÷÷ tùSE‘IümþãÏ {ì$êëð§ŽbO *É=‹*œi#à)>[wJ1Õ3qÞ—Þh Ñc&Ñ.=“Ÿù¹|ÅY¾MÔÕÞÑ34BJŠ1M­cëfzð­g…÷é2º}Ñ/iV×ôæ¶»„ºÆYÏ¡¦áå (5•¯xSûÔ´:§MÛÃÆŸ•èa!¥¼Q,XDñ´Þ¼E” Qž§b¡±zÃF^×@Ïn9B÷4‰ðå¯t‰õR"Äù[=ô9}txàî/Vì¡ûÖ Ò¸ƒ4Y©·:÷ !5H‹YHõí£•"êÈÜÕí4¸ŸODYSݤGósß=*\ö¶˜âh{*ú‹§¥K—Hqåñ°×˜©õf+W.—k«XHY"Š=W„GkOÞBêÉ_þ˜>þH„ISõv¶,¢í«h£I,š^êÝûn¦Å£I/ÿü_-!5÷Uj~ç%j"jìItd_=ÿÀÕERìmŠ“âLíËëF¢ MÿzSЇš-âÍI^ 2×~™8GØJîM~9ú±¶&sc­Ÿ?Åç©{>+é¾Y¸Ü'~ÉN’ôcRBª­µC¾Jžt!Å"ªc‹ - 1Ä‚hÖ[sÄbúÕRh©´=:vÒ·»åwȲI…‹N_»p«|P{˜¢þøÁ½òP‰çJu}ñÄrQJvªn~ÝÚ!ú'Ä”ij_T{Iö¬qê;CЗ¤ä-9%¤XDµµ‹èÜü3q^N/¾þ]Q7š&¾4Í)Û*êjvVò©}ŽBŠÅÔÀ@½¾x}óÑ+èŠ)#©q×=t}†„TGG›œºÆSýš›×‰ikýyy¡Šá‘bsäÈšôÎjþ«y4vÓaº·å}C<+ꯅ:Cúð$u®šC]kæÐ–ÅS©¹á)š^ºâ_dä¾%ÞJ/ú¶Ü¶qö+´þÍzÚðæ ´N¼Ú³ƒÆßyqQ„1²@Š›²ps€>B8Â`°ÒÚ€R­[¶Š(ÂÉ“GH µu›ÆäX"ï6-y˜UÇÖmÝò5¹²Ú:sÎð¼S÷Ô¡$ŽG%ylÞÔ¾U¬}bªÔBªä …€R,¢ZÛÄr~™8¯JoÏ]J­[ݲ[D]üyÈ… +Æ0kiƒxÞÑÕôã—GÒ5S®ÎŒGŠŸ7Å‘ù:;Û•ŒÌW€GŠEÌÀ@½ðö*é™úõ¬NznÇ{ôÓeû„ÚM?Bê—+é¡õƒÂku€Þè5åÏà§Gí;Áë±d9kŠ‹©å«×Ë5Sçݵˆn™ÒF¯;@´£Ç·¥'[Ñ‹ÛÑóvÑãsÛéöçWÐóï4‰È;]2@†%^LS÷LSû‚¦û‰ç.‰º¶oo—ëœØsÇBIOÊůœXdyE‹%wmÔq^³%Å’Ø®RÄþGoü6ýýð!O$÷¼ÝrmÔ3?9žýÉù4áGïÙ§òBêñ[.BJxÚìtLD䤇D×ßEí«´/wnXa°Ø@ém@ )Dù&ù°\ަgLÅöƒâÞÁM»E”;™Äv'‰ýN>Uȳv­uó¸F¦à0è2‰¼2ÙQü¬H~ÈŠÚçFõsòÉü"âŸLZyg›Úç™×þ<û²G”€%¤zDNŸ=œX“A9"I+³jµuðë )„bãÆõôƒú_eBH-^¼@ *á !óƒ‡VHI˜æ×*¨û–ˆƒÓ¸%ôwΧs~5GLû›KÿxÇ<ú—Qóéš1Kè…Ùë©yK;íêÛ¡‰¨â)dJLYÑ çF&k:Ÿv½P!õØÿ@…¤GEyNI…ŸLH`€ À`Õ`,¦!¤XL•#UÃ8àË{½ Q,¬Jê‘Ò=O[·dBHíñ•HŸRÏ›Ú/¢åmïÜ**¼A<«Y< ¸Eˆ•M›šD`‡-Ôß¿SŠ.ÎL"™GJÕÇbŠ#öôtÉ´SKj›zÕE ÂB…T³8n™šµ¤¶ÉíBHÊäËcn¯œ’)å-ó–qŸ9ånBSxØ;É®ËSÏQ¿ö‹W™´vô:ý*Ÿ|uÛ4íû9éùE9'íûEÚ¯'-?ï‹—Dû­´O”áTNíß'ž÷?‰z9éeÔ6ÿv+ÏÞ}nRåöòv;9Û´|ú~'ŸØ¿ÏNz…¼ß#ê3¦½b»H{µ–óºûˆ÷Võ«$óxòÙŸííâ54±—‡“žÏ´Í³_ôAxod¹¤{ŠLù¼ž'åU*GJy¯â)ÇÓe{·†Ò#Ååµ’ðb©d{¿v‰W'‰}»ìäxÉ´2»D*©þëÛŠ÷ÞíÇ.áùIk[ß'öqŠl»_Ôi'U&è5n¾¨z¢÷}·“íò”QÛøÕÎçñi#UÎäbO”Júþby¤LmêÛ†LH§ëéÙÕ§jÓøÜ@ÚƒvUÞLíÓ#øéÓðÒâ‘ÒÞô_%tœý‘QùòRþHúú¦ 0êj{¡BÊiË3P[_嬫Ҧ jyu1wj„”W\AH¹bÈ/Š ¤,±!!W\AHY B*á4@GDAHÅHqóE %Kä'©‚×HyÖB™Ä„”=Þ)!R¶0RA) ¤r½\ðHIÏTbo{¡à‘ ñJÁ#ÅÂ0ž7ŠóÁ#WDq>))w-U‚õTR–· ©’­‘‚GÊñ^¹Óô‚MÀ#¥‚Q›ðNt×]Á#¥W`jŸœb‡©}RÌajŸ5Sû0µ/LXAHAHAHåNáÃÔ>+J^åMíÓ=Ÿg¿µÏ¿îÉŠìgEéó®›Ê¼çDÔ"z×M¢òy¼KjÝ“îÒÚQy=Ñÿ´:íµO¦uOÜÈ5Gjm•aí’¾Ë[¶öÉY¥‰#Çkd¯‹Rë£|¯ž©r¢ý>_rÖUÉiujú\Ô4;}¿6åÎ.ïñh©öœº9¿¡~Ï'k¿¾ÆÉ[&·M}m““W«Ó³ÞIµÑfÔ)½OÆõVv;Þ} ÐNêµõ[ù­‘ Rz8qõþ=I2¹É“Ï PŒáÑ ¢(¨N³PrÅJèº+](‰~³È“Énß#”´¼úö¸ïƒž9åß,άcò´gXåy0°³ß=h›þÌ'k’¾Jy¢¼ë¢t!e½÷„77MÉÓ“fv ,'ëp…”÷½Z¿³nÊ#¤r½KævÌâËdZ=A'¼Á$r…”Yå–СWhÙy#D.”Œ(ŒBÊ$®ô Á$ô€Î{Û«•$…'ð„ÕV"q5µï°D2iõê#Øåf®Pr…IT¹"ILñ$…•½]8áˆ,Þg -¯hJ"”¬¼z@ ":(E€ÈòŸíÀz c°opg하>·Ÿ“O`¹Ÿ•(rDB+ãJÖ”G\y#:ÂGÕõ/BhNïc‘äi2­¯2y§"¶E -CycÔ>]ôÅ“æq2Š&w¿@AQþâî÷°°ê7Š+ì"7 iÊÇãôL*GlÙ^¬ ïQäs¦”¨ÊJž¨|Z„>W`iž-“ÇKïS„(Rу½PQÑõ”Ç**Ÿ¾ß.è½ÊõH£þiåóóbÅZT¤Ð²žóô¬']t…’ð˜õrÒ£ûé-Óó£ yÍ‘µ¾ž7¥ ­H!õ /À`°Øl6€ À`°×"…TÒ§ #?€€€€€¤™ÀáC´µ«“ž3‰æÌYB œç¤Ùö6çuöbzìñzYB*Í#Œ¾€€€€ ©qã'ÓÖ­]‰ëæ2\B*1:È2%¤ò=©|É¡€€€€€@f èBêÔéÓ”$ñACHevèÑq| èBê½÷NŠG5ÅORùRG9LÐ…ÔÁƒÇij½éìo}+g›Ê!•é¡GçA@@@@ò%  ©~ñÜ)=±ˆRÉ¿?CHåKå@@@@@2M@RÛw“JºˆRïõýü¾hBª·— `°Øl6€ À`°´Ø@”ÊÓ…Ôæ-½ÄIQ¦Ï*„ 0l6€ À`°Øl "m ‰jjîòˆ(þ¬’.®Ô6)œ4yÒ¤åWô¿ÈÁ`°Øl6(Ÿ $Rš¶'Mê½þêß!!!€ À`°Øl6¨HÈGH™D”i„NšŠùä“T$î ÷i` ?!uðÐ{ôècÏÑ “¦'J<úõï9!…/|¡À`°Øl6PM6!•¯ÌH9%„N>-#É’ŠDæ‘J«Ú³'?!õÉ'Ó˜±/Ò¼y9‰§Eš¶ó6.íâ{¤V®¥gk¢‡dšDóV&½8¢ygsÙZ™ÏâÒíOQC5g×ç×N>}Ó˨~ŽØ{iþýÝNSx<Î^K­…ö;Nù<Ž-ÑXõ³³Ù86[ˆí&µuäO4îqlyb_OÀç_åÙ@#¨ßë9©–N|/Rlûè¡‡Ïæ¾ÕÑ”\§Š#¤©®¦–ê{Ò/.ª©‡JH½÷ÞIJšJ#¤Ó-¾sòü‰;‹æÍR©½{óRùN…TÓ K'¤ïż@!e_ VŽˆrÖEíì‡{Ì7|³ž÷1ĸ'ý ½0AH%á‘øØbð*H„†±3±Qâ*ŸK ¾P+ïf c€1… T§ ØBjD£ö½no+צÎ5Þ ¤VÖÓÙž¾ÝGHU“<Éα*!uðà1Jš’©Å2 È´c"'DÔyB8¹ywÒ3ç‹nY]>FÅR\ÇÉ“9¼§þþýÔÕµG¦¶ö^j6šš»¨Ù^#Vš5R¥¾‰Žyã-¤¬‹ëˆ)† ×PCâ6Búɤ@!•´¯IóGöß7F‰ëcgfÓúð$éUb²¤ýE~xI`°ØÀØ€IHÉ ÂsR~¯”ö]Æ"Š¥‡ÊŽBÉHOMBêìo}Ë(ªüÛ“©N~BQiþ54ü™†|‹éÆšóiBgtQm”BHí"ŠSÿ®>!Õ]!åÜô®µ§ìYS§¦LñN©roXý)õÙžr噞曖¥y"…ÔûÞèb÷µÇS ch°¦Èy¶©©yþ~»7èó¤wÌJÏ>|Èú2ñ‰«¿!7îýÕëÒëÞÕ—IÚ´K[Aœ <ä AȸÇÜïÝ ׸õ[ì b×CH™Æ?ŠÜ¯Û§°ÝKT§gjªÏ«šø\2ýºtÎDÇÐýRZ¿¤ƒ/Æ6P¸ ÄR¾)€º˜qN½=Ï$”wÉžFèñvíÓ=R¾<νHH½N¿êì鋵t¶ª ¼OP?Ûuxâ‚=Röt½z«.Nµ-—«kÔ•çu§]z÷eD¡d¤›JHõ÷•ÄbI%µ-h{!õÞûŸPxZL7±4¾#*ïßIã‡kÓr‡O¤­‘õ[õ–DHí;BûDÚµ‹=R2IÔ¦rx¤œC{Í“óYÝX '+¿îðz ìrJXMið•(!µòáÚàõQ~‡ÿ BÈjm—z˜ï8õ6|}7}™Ä꯳&Í/"úâˆ5&¾ò—o@~´uo¡å“ô§PvF!¥xùmÖ»n/ü„µbb²)Õç ®qÏ%ÿYð9|žáæ®ð›;0CØÀÐØ@œ©}¾©~~Ïúìx°r§z=\Þýò»Ú3úûÞ^ßÔ>ƒG*¬Þ^§_î+oý~Ï[!BJÜÐ*¥ÓSOµ,€<Ÿë„„ò )™OmgeýXmåë¡úZX[U*]¦„”¶[S¼ÝÿYåM"¤Ž;M¡©m" «¹…¢ò‰ý × û¸~‘SŸÿsX;¥Rž©}ÝBH‰dMí+«GJynü7ùñ„ÔCþõJö¨ãáñM›R1ÖG±Iµ©nzuoXޏöH©ž>ù½ ¡ë±ö7Jˆø÷G ¥ aäLZ_Þý±=p…°óx\Ï¡'PJ¬ñWž1[ŒÆe` àÌ©3ê\2O‰43JHåœg˜Ž4Ó‘p“=47Ùà\ùœ‚Mè^Ûk£¯¶Œ-P¢NÄ~%nr§ìE©¸íêÇâ)“l-X¤GJ8¡¬?@ ý³ÿ½í½ÊQJQ*ÑdªW ©Í[zIOºxRïýy’©þ£šÖ>%¦¯^GÓ´|k¦ƒF®å:èªõÞ®ÓP6¨-)gŠ˜úµ?bjŸšþá‰RëbÿÔ¼@”v£lRN´¸°(… ûëóä9SÌ"ú¢ß`{¼¬òo:!,0Æ•f^”IÔ8Û‚"ûÇócï×…˜ÙóäNS²ÖbÇRõ×TiuúfßÅRJt¹Ç-§J‡”Ï[5”ª¢ ÛRBŠƒ"ø“.¦Lû“©=)<½MWÖœK¿Yiʧí[9†Îªù½î©/¬¬·>©H!¥]èíAù«yDÀP!¶ÞÈ4m/äFÚý?O‹›ïZoÒþV‹Gªì RºÇìqtÃÛGíïµíÛ]/5Å2?T¤×I?Ïà‘‚G 6ÈŒ ø§ö¹kŽbðHy8Ë á™rå9ÒYÙmYÞ©xBJ÷dEµ+û-Û¨ϲá)…ñj”FHiªEŠ'5}©¡ÔsJHmhÚN¦Äb*h_!Õ¹}/E¥ÉWÔЙ£7òͤ+jΡÑK¹ý½]çÒÇéÌškhrŒ6 ¤"„”'€Gp˜×)/F˜ ]o¤ ðOíóL!‹X¿b¸A»‘.¨¿9ëÎÖHy¤"…—]_`0˜k¤ÛÏ øk –ÝŸ‚Øå+¤"™¬‰ó¯‘ÒmÊ'`œévjmYâ©}áÓfÕ|Οg•ö‹5Ž^Ø@åÚ€aTÎÚ"ÿ¸€5Sy®‘²ÄðÁã%ò‡?Ïík¬5R9Qþ´éŒ¦ ù›H:µ×DÕÖ‹ÕPöŸÇ …5RiRA"Š·[HuJAäS,œøÄr )…ØäÅ {–K>B*bM3Ú†Þ„B*è†Î¢z߇HHùbݪ¯µ@m˜¦ù½T±BÀ'÷H¹'^R»a1ĸñS¿x„Èw I>Ç”ßy%\¼7ùyÚD‚þäV•/«Üs8÷ÜK.¤¤NÓ¸ØA]äiÕ±,BÊi~!ßABáÏ9²‚€DHŠì*2€@žâD©Ë³jòÈç˜òô­‚€€@¥€ªÔ‘Åq¹ ¤` Y#lB‹9²eÖýl€Êöø¡÷q@HÅ¡„ËùPYS°…,0ú  #$¤ö &N»ví§®î™ÚÚûhãÆÔÔÜEÍ-ÝòHÇŸL#FŒ¢)SÒ©ÓŸÈ÷5ü?ðF~?(+©²âGã…yAhàBÚDY8 ¤âPBÐ@HÁ@@@@@@ !©„À@@@@@ ¤`      „TB`È     R°HHB*!0d)Ø€€€€€$$!•²ƒ€€€€€„l@@@@@€J ÙA@@@@@B 6      @H%†ì      !„ ¤Cv€‚ €€€€€€@BR !;€€€€€@HÁ@@@@@@ !©„À@@@@@ ¤` aß黓®Øð`Îü톫èÿê½ ÃG†®ƒ€€€@º @H¥{|Ð;$À"ê{Ÿ>F|ú°GLÙô ú¿?ú¡L5íçƒ €€€€@ @H•j5VùÔSOµŒ$zA$ü•œ€.¢Îûô>âôÃwêˆEÔoÑO¤ˆú·'~@5û/€˜*ùh j$!U£^äc–"jûQkƒHüŠ¿RÐEÔw?Mç|z7ýõ®ëè/VýLŠ'åâ÷JLý›%_Ê.¡n¨:™R~g ùSÕTŠøž{”SðJ•t´®Úð°œÒÇ"Š=Qgïú%}½ñJšøéB:ãã;ˆE{¢Tÿ?Òß.¸¬¤}Bå    PmR/¤X@ýõó5tæ»VRïy;þÒA`äH{JOíbêÝQÁc³téÒtt:ã½àJD}kõµòýÿúôfúO/—A&j.ýcëU*ˆ¨Œ6º   J©Rº@â÷£©ÖIÊ+J²g-Mªyˆò¤I´¶gùË>4Ðv“ƾ=D ZÆ ²ùv¿ØõúqÉ%—œÞÇž(SÚïã´sçN™^zéÕܸɋ&Oy‰¨ ±âù•on¤s¼C|SþMª·oÓõzì¼Áû⛌_<)—¤?Â߯}unTU_}7ËÛÅMmØñèåõ|Î{ß`žwÞyÎÔKP*)!UFv:“!X¿oü,ágRj}{¡”€â5R—<޾Ùyƒ#¢Ø3ÅbŠÓ7žÿiîié½:W·0Û °·$äH­RÁ%y£!•d”æ­òßXæÜhj7íay“Lí“FЇBú£ŒÍS‡Ï3'ÛõzÜu]bŸ=r{]°gäûBø*ñt ¢Ò_œ•Ô#å3ñYŸŽ2,:”ÀqÞçwÀ1äË'ñûÆÙ÷þBŠ£ô±pb!Å^¨?úôd‚§ö)O”QÿwÃ9ô[3¾E¿õÄß©"œ+qYDzëRx…D—@@@@ €@*…”¥/?!eðù°gÀêeç7 —€Íb ©P”o:aP œ)nö‡yf¤$èF^ís•ɆXH±xâWNì‰:Kˆ¨ßþíß–ÙóòH±ÇÈ/"lïœ1ˆˆsìQB×0Å,_>qŃ-J˜{ÖñùøüÜ(Q`‚™à‡ñrp‰X"ÊùÑ¡çJÛKÐ®Ö    ©R,¦TJ$¦"õ¶ÖwxÖÿx¦Òù…KÀf1…TØ©œµ]1û£¬/_‹(ÏÞ†ºw* Á¢9¸Äïᤠ(N¿õ[¿ßþ cfMm‹9J6ZëÖäDÌ0;O _›ák€L‡ t¼q̬ãq‚p„»%úØ[edãD2V±ùľ3¼k¤ô*g×O¤'Êÿ÷Í ? QQbÓ´_³é\‰g{¡â¶ÜWB´    ¤NH%ìnö(!%Jx"¸ÙQú¤1–Õ#”x²rn:màRØô4u$‘‚, ?²¼?joó÷%$*]Îs›¸lôÔ¯’™ÍË trìúM¾Ax§ræµ/އÌ8ƺ ŒµODPŒðH•Œ»Vqà¹jW?LÄ87 >&T   CD ò„Ô«ŠføÆW…e¯ŠNÑA2û¡|öYŠ],€ÊÂ(•¡aá·ËÐ*hÒëm }^ pˆ    R™&t@@@@@ M ¤Ò4è €€€€€@&@Heb˜ÐI4€JÓh /     ™ !•‰aB'A@@@@ÒDB*M£¾€€€€€d‚„T&† H©4ú     R™&t@@@@@ M ¤Ò4è €€€€€@&@Heb˜ÐI4€JÓh /     ™ !•‰aB'A@@@@ÒDB*M£¾€€€€€d‚„T&† H©4ú     R™&t@@@@@ M ¤Ò4è €€€€€@&@Heb˜ÐIðè©§Úšªñ¥Úúž¼QõÔ׊új)ªŠ¸ùu¤±Îs,uZiϾèþ%j™A@ *ìšo}¨ï0ßuÝs¬¸ægÁ<«¹RÕ<ú8öì°¿htáT3„|Çâ=ŽFªã/[[Y5Öñoé:k(ºˆ6@@ ¬*ðšo]×{¨¾V¿®®ùµõ"þ@  ¤Ò9.è„0|©’í¹q¼9¾_0­íö—–›ÉúŸýBÌû‹a ©"9‚ÍØŽhJõ§^óžÅùBÔ#TdÁH@@ JTì5ßþNRß þï1ù^©*±òLf°:BûPÿ~êêÚ#S[{µlÚAMÍ]ÔÜÒ-wÜøÉ4bÄ(š2u!:ý‰|_Ãÿøoä÷ø(2ת孱¿pBˆ%„”WÇúõE’G ùÊëu‡åóìSSòl¦„™gÚž ‚€@p½/ Šƒ€@"É=RÝC°FÊ'¬„CÞ”DêgÆkÞèP@2E×ûL :  yÉ=RC ¤<)UÂU/¼R˜Ú—½‰/ÖÚzñ  PÑp½¯èáÅÁ¤Ž@ºƒ‡*‹à‡®Ï˜î1t¬Ñ€”“®÷夶Aª@:„”à.LhÏ‘ò?+Š…•Ú/tþÀsEÀBVÈ0\ï3Ê?Xt½Neˆ:Â!Ù_a'[ÏET+@ר©¶þÿBq#ÕÕ §Æ¢ÿä­·±þ"ê m#f?Ÿ:Á¤§þK$Ù4·^}Ü¿ºÆRÙGÌþ&äêpâã©»1bì‹Ý½¾àºë¾Dõ=^®9ã¡Æ%áñã‹£TöŠzSi[¸Þ‡_ãp½×ø”÷zk<®¡å¹†fAHõ¸q÷£ºxª è8 ®ZqÏ:$)n#•dÜ|¡ÔoR z18¥Ì®ØgÓxˆ6j‹!¤˜‡%úø¢.¹èB*²´ÛŠÆ)–*öñij… !årǪ<ßb³A}ÇbÛ®÷á?àáz_Ús.Ùõ>ç;×øÿÈ]ìëMVëË‚ò÷Ñ'¬ôÝü`^ÿÃzãÊÒå˪qäöÛùÅ'àW|ëavRÞ ¾á®Þµ]‰ !0ü^cyŸ2çQ}ÕoŽõ›K4²—Hô¥Fz¸Ü‹´S§ÝçÈ~“..Äñ©wù*˘Śäj÷«ÞÓÇ dꋞ_‡±^íK(d j…'®Î?fb|Míz8ÉcÇèWmD÷Á#z4F‘cà± KÐK{,ê )´žñÐÅn ÃÊ9wK{ÃN•È×{{&®÷Ö÷WÚ¯÷¸ÆC8•e†I…”œÎgðHm/@Š[såÜd8¿ø˜Œ•…‘6Õ‹o„sßðZ-ýº§Çšg,¯Ý0û¿ØMýñü*%ÚÒ½¼ÏmÃ7MÌ#ÁmøEEî19Þ'ãÔ>×{¥úåécïqÙW¼þZ¢ÒªÃ3fÎXjǧ y<ª¬žGïOÌ>Øm9cçñvùÚwÚtëöŒ™ìWîÔ>ãnY¼j•s=¨Dñ€crí×{]HázŸÉë=®ñW%WYR¶'Š×A™¼NéôF1à ºq2­+鹑…0ÊùÒÕ§´y–}“« ^CXÞä9 [£e_8{ê‡S}£µŽG‰6¯H2 )'ijÌ' Çzã呲ךé} bõK±esÑýÍ~ÚøJ±æpVÂ$xjŸi:]¬>8ýôNãlß ¤èB©ŠºîTÒ54KÇ‚ë½õ#“ïf\ÿ> œÖŽë½g ­ûý ®óCt½‡ª¬{ÐT^?³$¤´¾æˆ¦T®R® !%ozs×H©  )“R'„úÂÉÃ#e¾Y´<µuü ¢xÏÓä{Kì¹Á1ây¤Ü6ò&úIŸÇk—®˜BŠ§Â© JxëñH…k\1§¦Ø‰qRcÚ~€GÊëiŒ)¤|6ñQI×*KáöŒë}˜Šœ6•Ûðî÷|ÎÆ›ûzk<„TÉÅWF…T£˜Ú§GæãϵbÊ_:ÿ*ì =$ŠSà)à ·žWýª½.Æ·vGxL¬²ÞuBúÔ7®Ó|ƒîRöú~¯”Ó†º‰ø…ÒÿË¥çF& jŸòÆ™û²Fʉ–s”Iôð/Î,`ü=}Ó#Åš#—µ©'ˆAØ© á城Ú¾šNèýEÓ3Κábêzü‚Vع[ò/ð*\¼¤Œ!®÷)\ïý¶ãÇFåáÓ-ÚõÞ¼®ÛtPqç)®íeŒYRo“ß#U/"õ¥/ÈD%z¤Rö%Ÿº‹GÆ"ÁUup_`’ÌGTʼn›#ØÀÐÚ®÷CË;©}ãŸîñI:žiÍŸ!%úè^”_4¥w}T…­‘JpIá‰%~]ãçHeâVÍBªšçq6ÎOŒSúÇ ×ûôŽ®ñ雊º¶eDH¥sÊ^Ü^¥ðf¿¢Œ|3!Ú`søRƒ À`°Øl ¢lB*®* nôq£€ À`°Øl6¨,€*@ Å-Š“¦²NŒ'Æ6€ À`°ØlB*®B>ˆEàð¡7~²Ì{òäGtðà1™ö –©×êêÚ#S[{/µl즦æ.jné–e¸ìˆ£hÊÔ…têô'ò} ÿ㼑ßã@@@@@*‰„T%&Ž@@@@@`H¤GH5‰g†:I|tÿzÄÃxƒö &4â'ÐX_ObXð  Páp½¯ðÆáäM PHí;BûDÚµk¿˜Ö7 “œÚ·iG ¦öÙBI‰'ùL©:÷˜ôgHÉ}âἸ‰Ï{Ì‹P°‘êj!¤ŠU€€@Ê àzŸòB÷@ÊH BÊ@VüÞ'œXX)ÑUFvÙtO}­à]+<ƒu’q£€]#\…2ÕYÔmüY(ÛZ{»µ¯–øaÊžz8¨³VÕãˆ0|AW¤á @2A×ûL :  b©RðH•Ïbø‹U &ù%˪ÈþãÏ–fÒPˆRõH±åˆ§ª¢J«¶|‹–A@ Š àz_ŃC( t )qÏ®ÖBùo´Ù %×PaZ_Q>¨]º=Ö³¤TtϾ„\]ÙÅs;øÁ¹ÖStñ  P±p½¯Ø¡Å¤–@:„”-”Ôí¾|¦TˬNˆ(¥ä¾ZñÜ©Ô"M[ÇðÄ´ú ¥!€ë}i¸¢V0H‡ò÷MVüÞ'œXXyØ‹Ñ !€/V˜€T\ï«cœq” i!J!åñHAHh+=T_[GÖ‚â  i'€ë}ÚGý¨,éR¶'Š×Bé§œ©}ðH%´BüJ™²ƒ€@F àzŸÑC·A2H ]BJ虾§ ¬Z±Fª^LõÃÔ¾¸Ö†_(ã’B>È6\ï³=~è=€@Ö¤VH5Š`AÁæXd5ftÙúË‘œ0µ¯løÑ0€ \ï‡ 5A BJ¨"$>Ý#¥¿÷GôÃ(FÀk!ìÊ €ë}eŒ#Ž@ +Ò!¤-)´gEy¦î±ÐRûúÉŸzž”.¤ta…ò&¡‰¼     ¥ .!ÕCTËÞ(ŸGнQêè9þ¹TP'€€€€€„H•bÏS½Jž©}¶¸/Î +·?(Ô)ž²W+ÿy„Tcîº(©r˜ ÚPÒ!¤z¬)}â%WHùöq)0€€€€€@9 ¤BH©2*Ÿ–”‡ k¤Êi"h@@@@@ÀO BÊß)øsþ¬D¢öÁˆA@@@@ÊM BŠ!á9Rå6´      ¤RHax@@@@@@ Í ¤Ò<:耀€€€@* @H¥rXÐ)4H…:|ø0!l6€ À`°Øl6ˆqRq±°Øl6€ À`°Ø€Ï ¤pRऀ À`°Øl6€ ÀÚ@¦„Ôì‘5T3r¶äÙ4R<™wØØÍp«¿Vß½îØÍc‡‰‡ £±›Kà¦=RÔÀ)l_B#J‹{ý( Á2rÁاçüÇõ>=cóc€ ”Ö2$¤6ÓØa¶(Ø<–† 0rviáÏø‚¿X‹×†Ÿ…ÅË,¤Âöe…)úY:Û[°… äo¸ÞçÏvv°Ø@¶l BªÇ, SØ)˜Â`é´l)M<¹Ï’J'P³¡Û_¬¾¨ƒÂ3Ï/ŽüÏzòEí‹|Ž„ÄÄl60Ä6€ë=nø²t‚¾Â^a…Ø@6„n†øF'U!'ÊÂ~`°Øl6€ T¾ @HA¤A¤Á`°Øl6€ À` mB*!0üºPù¿.`Œ1ưØl6€ À`Q6 !ÕIì48|h€ÆŸ,»tòäGtðà1™ËÔß¿Ÿººdjk稜ÝÔÔÜEÍ-ݲ —1bM™ºNþD¾¯áü7ò{ü€€€€€@%€ª¤Ñı€€€€€ @!µOx¤Dêߥ{¤ú†Æ#U_KT×h>þ°}CB,S4R‡d‚™©cAgA@‚ àzë¡&:!ÕSO$îýB*lßPƒËN{â˵¶žz²Óaô@@ /¸Þç… …@@ OéRân¿–½Q&Tؾ<¾:Šá‹µ:ÆG  ¸ÞÃ@@`( ¤JHñ´½z!˜LÓ÷Âö %°ìµÕ#xÖQcö:Žƒ€$"€ë}"\È  Hâi{bšüó ©°}•ǯ”U2Ð8Lª'€ë}Õ›€ t©kJŸxÉRaû† S–Â/”Y=ô@âÀõ>>+ä ¤BH© dBOì¡ ÛWøáWC É Sûªa¤qŒ ÕN×ûj·?€ÀÐH…ò2ŸÓðÅZLš¨ @ÒK×ûôŽ z P‰ ¤*qTcÂsE*zxqp  €ë=l@ÊF •Bªl4Ð0€€€€€Ä !²€€€€€€€NB ö      @H%†ì      !„ ¤CvH¥ò?GÊôPÞFŒ]𼑢8€d„®÷(t@ cR'¤”hªÓ”Rc‘þ9cŒSÚ]|±¦t`Ð-(2\ï‹ Õ€€$.!ÕCT[+D'MH±‡ª^ìÃ_~ëj¨¦ÆNXí‹U(Uµ_í6—ɯ}”¸Þ g´ ©RJ0ù§ö  nôÝïT|ãí0k5ÊŸ-~®jÊUeééé¡à2ñÛEN¡%€ëýÐòFk  O髉ÿ/VüÖ@@ \p½/y´  P­R!¤LQùx*ŸòPùB*™¹ú¥ÌÚ管2®‘ƒé”ɘ#7€”ƒ®÷å Ž6Aª•@*„”¾gj_£X¥y«”ÇJlÆ€€€€€”…@ú…”Àâ÷X!‚_Yl‚€€€€ØR)¤0:      fRiô @@@@@ • ¤R9,耀€€€@š @H¥ytÐ7T€Jå° S     i&$¤ö &N»ví§®î™ÚÚûhãÆÔÔÜEÍ-Ýò°ÆŸL#FŒ¢)SÒ©ÓŸÈ÷5ü?ðF~?¨$©RžçHÙ´y?¤—Ÿ'3AëuÔ˜¬rƒ€dŒ®÷0t@ ÓR'¤Ô3£ê´»þÆ:"ç³Øî@o¦G äo¤:ˆ¨’SF  P~¸Þ— Ðj".!ÕCT+[\Õ ¯¦öåco⋵¶žFü€T4\ï+zxqp ©#!ÅSúÄý¾üËRÂ#åx¨laaÇ–zK¬ŠC y@@ Ûp½Ïöø¡÷ Y$!åG&”.œxªNÄ57üB—ò€@¶ àzŸíñCïA²F BJ˜PS÷Ô«òPñš)©|M sæó%‡r  -¸Þgk¼Ð[¬H…òCô‡?÷Díóy¯²>¥ï?¦{”ž1Z4Àõ> £€>€TL)žÎ§E9R´ÐëZñËgHHñ”>IúÔ>½ûÒ[Ué*C†ÊùBw~Ù1 )FžtÛSå|cky´ôåžk œ±¼vcìÿb7õÇóë“hK÷6ð>· ßt0[Œ·á¿ùW¢È=&ÇûdâcœÚçz¯T¿<} `â=Žû ƒ"]4H‘ëþâÇâŃNYí¸õ²ò8UY=ÞÏx,u&‘í;mºu{ÆRö+wjŸñ¢Z–_Y+ç:Qü/*°)S\ïu!…ë½úîö|Wdõzk<ÄUѦïgDH±Hª‰ÿ‚„T:½QÜã º 0­鹑…0ÊùÒÕ§´™nÜ5aÁkˆË›5cÏ(¤R¹6JÁ­ !%o€s×H©  )£Äæ¢F)³Hµ¼ µuü ¢xÏÓÌä{K\D ©`‘ãË ,PAB*ÈKGHzø<ýÈþ(nðXhÓ• ñ¶â‘ oyñŠÁR‰A1~jÌBÛðHy=1=R>[¬¨CŠöåQI×5‹ÙÆq½R‘3¦r~8Ãõ^Ì ªë=®ñREû.Ì€ ŠÌ§ö…Ø=®ñÙ·BƼ”e+DH¥w}T…­‘*«ê/å‰Pĺůhü©L_¨ªYHUó±ãüÎöy‹ñúñÃõ~è™jç¸ÆgoÌ ó’–Ï J§×)¬WE¼I/©1 Ÿ™?° |9À`°Øl6B€]€ À`°Øl6¨,€‚Â/CøË. •uÅxb¸Þ—=Z¨ ©RµCƒŽT$Ô)ž²W+ÿù×HåLíƒG*¡1âWÊ„À@2J×ûŒº  AéR>¯SX° [Ês•AÞeè2~¡,t4  e €ë} £I*& !¥L¨¨|êUy¨üãêÆ*´d‡Î‘œ0µ/3ä,Àõ>‹£†>ƒd—@*„”ŸijŸ .!EW]v}ÏñÅ:ôÌÑ"€”ƒ®÷å Ž6Aª—@&„»ŸoBŸ'°VÇ®ƒd”@*…TFY¢Û     UBBªJ‡      P<RÅc‰š@@@@@ª„„T• 4@@@@@ x‚…ÔÚ7x„úû÷SW×™ÚÚû¨eÓjjî¢æ–nÙ‰qã'Óˆ£hÊÔ…têô'ò} ÿ㼑ßã@@@@@*‰@ Úw˜ö‰´kׯÚ!UIãcȃ@¤êBª{@&é‘*•2=”·Q; ~¯ Žðçy 3Š€€€€€“€R;vöѦÍ=‰—)ÚÔ¾FñÝ:]9iG©? ä-æð£.|°:tô8=úØDzaÒ CšnÞþâ züÉç‰Ëe‹¥úó!°7ÊÑX"O­þ9Ÿ£F(€ÀÀžÃtðð :xä=:,Ò‘£ïÓÑcЉ'é½÷?¢“'?¢?þûä“O2Ÿø8ÔñäË*ªnR£a¹ ÀFË=h@@ m ¤Ò6"CØ%„N>MIw3Ì#!•l q“šŒr=ØèÐ3G‹  é&P™Bª§žjkj¨F¥ºÆ£ÐHu5µTß“ HAYƒÚãíÚ1¨÷µõäíš·|O}­uÜ"_£öÞt8J½÷ÞIJšŠ-¤ßâ=Ö[랬ÅtKÍù4qgy¼[|¬ðHdä(\ ¤*`q  E%PyBªQDˆó ¡Æ:q“[L¥IHå :K( =K|Y‡ª¿7ÛŒR£¤)®úðô'žvÒ3ç‹ñ¹e±–o1Ý$Žã¼‰;ímüù|zfGT]¥Ù_!% ¨£˜Æ¿¡>GÒÈ úú!U}cŽ#'PaBª‡êk•Ð<É_’¼Å0¯0”Y0I1e†z]ÑÇaRgë[FQåßWH}pò O‹éF!’&túò-¸…jΛHÛdù€<‘uGµo?„”ßΣm«gêH©tz  P~•%¤bÿŠo .gêŸgºœÿ&1:o]{‰”÷À—?Gðø÷×L% ¹Yõ§Ê0Pxvjæ)*!Õ߀8±XRIm ÚWH½÷þ'žÓ ¢Ã'ì ÉÇyΧñ„¸²§8zóï¤ñõñ>‘¶r»i8—ë°ûõ9 ¯éRúxû=WÚ¾ºzaëº (Ç6%΃zöÞÚŒ½¦«×©Ùlìs®ü;ô 0R…ñCiÊ#PYBJN닞åŸêçýì0Ñy½B…óëÂÅï=Ê™fh˜Šh™YدþAž§ü±MÿÌï‡=½Ãª¯Áb×5X}j}ú|·Î~¦OHYbܱ3¶!çǯWÖ;4¤œ½®0y•wQÄ™ @HÁ2@@@ÀK  …”A =<b&0o>ùÃÓÐ ©Í[zIOºxRïýyâ ©þ£”(M¿ÎñˆœýX«]¶®ªF®UuéŸýûDžµOÑÙ5×Ñ4n›ë»ºAÖ3íêºêjßçéÑýKÊñ±@²½N9û4;JRNÏV'®¨UCBªj†   “@e ©8ÓŒŒy<9Iò2p´@ÇËt“›§Š%ü¢×±¨©}MÍ]äOº˜2í+¤vö¤¼Ò¿‚êôº,ÿ6]Ys.ýf¥ªKû¼r åä ÛÏe¸>ýU¯3¸ŸéRþˆŽšy¦ªúEVH9Ý›ëRAuƼР[ö @He q  Å%PYBJ7›7…jÈ{¤<ãe7…›ˆ3ÍÏl4JHmhÚN¦Äb*h_\!Õ¹}¯˜6’^½F„œ–øó,}œÎ¬9‡F/å²3é ç½ÿ³ŸØ/Ë^C“ehô™¢žÑªþ\CWŒyLíúšN!0…5‘GJ³ 9x¤Š{¥­€Ú ¤*`q  E%PaBJ°‰þ‡j®˜io R{iòÂËâäÍý,ëÒÚðŽ{©RöÎz&J°F*L,êô¬ŸÒg­õš‡Êò$!•'8¨X•'¤x¨"È‰Ï t—$¯lÜ0'ø…wm}}HÔ¾¤äÍ/ØD˜`*Ô#µµc/E§ t¿ð9QyJäå3µr3érᑺ‘ªËÿÙW^xšéí.zœÎÐËû?Gô±|BÊ0þN(=o”ÆXöäe, \˜GÊ_®Qû*ö!äÀ ¤ªqÔqÌ  a*SHaÌcˆšÚW Ô–Í»)ë©,B*ÖÆÈd‡5Y@ Œ„ì@@¼ ¤ªØ"†BH57õPÖS¶„”ÿybæ‡:W±ÙãÐó$!•'8¨XR;´ÑÆBjÇÎ>çRúó¤¢Þs9%Ä^›ñާ±÷ޡ߻`5®YO«Wud>Í]¼XO)ÿp“ZJº¨»`£Å ˆ:@@*‰„T%fÂc9xè=zô±çè…IÓ§G}Žæ-‘-ú…ÔÉ÷OÐÕ¿Jß¹éizgñŠL ©¥«WQíõchäã¯'¤›,{ÎMêÚŸ! 9ƒµk×’Ë@·b©dç4rƒ€T>©Êã’¡_H:õ1}ôá º§þ-úâáÍë2›ØuýoÐÇ¿_RŽRéŽQ^! !UÒÓ•ƒ€dœ@f…ß¼#¥‡AÆÏƒ²wB B*"Bªì—t@@ Å2)¤RÌ]¼@HAHAHåuê €€”„TÙУap …Ô¬³¨–Ÿ©¥§ ÿqÈ× ÅY«Õxáç¨~V®j¼ûoÞ§ÞÂòüˆê¿¦ñûÚYÔc¯;³ú•Û·4Š™rö )\¥@@@ ˜„¬R@ PHy„ ƒr‰’p‘YHý£xØô—©±\A3&|™j4~,žê&ˆãàíJTéïE?Ë)ZÒØ6„T .耀@j @H¥vhбj"r½*³¢ƒµ·Ÿ%Œåq©› yeŒ·¬G ±7ÌÎolg­·ÞzƒGÊ-÷eª¿ýs¢¾Ï‰>Yª¾+/UPƒ¼X=¢RHiÇ&E•&¶Ò(fÊÙ'©jº áXA@’€JJ ùA ’NícQP{ûœi~ŽH`aàxØ#äz°á橱E…ª/¨Ïv9Ñä)s=Rœ_{ž¢¨¾ëÞ-í8œãò~ùŽY (›š¸dVNђƶ!¤Jp²£JŠ!!U1C‰É2h”%A%Ûû$…•ôH)å/c‰Ñá”QÂÇ@ϑ֎3MÎYs-¤T¿ÛÑw¯·)·¿&o”<ÍëäuRºÓÓ(fÊÙ'©,_UÐwR€*5aÔ1D ){mæ1Ò=RŽ€ˆ#F‚SÔ‰ @HAHAH%>mP@@ÊJBª¬øÑ8X ¤ ¤ ¤p5l€ÊÖx¡·J GHÑq¤H`PnpO¸\­Ð“‡  1 >4@[»:éÉ1“hΜ%ÔÐÀiqNšmos^g/¦Ç¯—eGŒES¦.¤S§?‘ïkøàü á ¤Ê-оY´BHáÚ  AXH?™¶níJ ‰ËpY©ÄèP¼ ¤ dÒé}ƒÂµ @@¢„T¾„ ¤ò%‡r  €‚‚Â%@@²E@y¤¸×§NŸN”¸ „T¶Æ½M)ªR=QmÝbÊ+šë¾Dõ=þ>ÜHu5éQöM_Þ¾–›•~º`TJ/耀@ÙèBê½÷NR’!U¶aCÕFBjèK´º¾”_(E«{¦AHUÚUÇ  P(]HÍÃd¬‡´ü¢õI©,Œ0ú˜ '¤|)â)o!ØrÓ®þ½äO}»ö’J½}{ `°Øl6€ dßÖ¯¶»a!Íž3—ž›XO?¿òºð¢KiâÄĺ¢•´pÑòÀ± Ûí;zé®iwј¾èECÏÒ#4öÔýôè±Q4zϯéî·Ð•?¥[›®¥Ÿ¿u]ñÈ•Ô!–'™Xµ£RЫR]ÝýÂëÕ!U醌ã×%l6€ À`•dkVÍ.LH­|ŒÎ:û1ZYîØKÙbÖ°.žò6áÙz!œž§[~y;=öøStÿýÑõ7ÝJÏ>;‘ê_xYN‹3ÙdØØ²'ŠEÔË4–&Òc4îÓ鉓÷ÒÇî¤ûûE׬º˜¾;~ÝðæUtÓÊté”Ó­Oß–¨¡RÍcèÜšªÑÒ5oZÓöL )\À+éŽc=Ã`°Øl |6°fÕ;C'¤rDÄtºÒ¹ÿ½šÞPbŒói÷Åg=¼6Úc[ ˆ6=Â/ º0Œ]wŒqLX×ħÒ+¯¾J·Ýq¯ˆ†·ØáðúÔ·éŽQÐ+¯¼J/½:3@à˜Çvõ¦õr:{¢XDMøô!ó±åâi}÷öü’¾õìWiñ¶w¥mLÛþ*\x}ûþá´påÒœ¶‚lhè„Ô¹cþ¿öÎõ7Šë ãû©**ªü ýBó¡j+ÒÙÐ(Q*µQ¥rIBÁxñ…€ Øæš’€Íem£4iåbÇÆX\…\0`¯×7|ÃW°âЊP)U­OÏ93gwvvfw&‘ N¤7ã3{fÏ™çü‚çá=ç ÚÄ:)eœZË1KÀãf¦h¤<@ú¨ÿU„í§þ Q#2@È œGi¤ª–¬6žë.n›…ÀÒZCêìèù‘Qitf¡ôbŠç?Eµ! šÅH¹öá11R™Ë×ãH]þ¸xU+ ‚«±wï>ä¬ÞäËHíÅÔ¾˜ÙÆ ¸Tg‹Ì)rÚXMVœáú Uâ5m/™‰QFÄjLÌñq5†qÑ­èw»µm65lö6Sõ!¾ÍXÍ0‰Ñ¾8eZ¥É”7Fjú"”n.Æ©è8_„% ~‡éONÁéŠ>* àg?ysfNÅ™Ê*Þ˜–`pÜÆVnq.3P[î¬ÁŒ=Oⵓ/cãµUÊD­hÎÀÓÛމ¯'TFêÏMkT6*ëøkÈ<ò ž^ñ”çv¼©Ó'öãô‰}ßb³ ¹FÊ:µÏj¤ä4?™Šf§rQgf¤ZDvJ©ˆQ/"¶õV@jh¤yꜿ°ø ‹ 2@ÈxÜ8ÿqmÊ5RUKŒ2DÒhC êÙÓ);c3F¾T’i}vC-ë̃)û¶fΩMyßnæÈ)cæÃHUÈÃÊÕ…È¢ñÓ<4œš"tþ!¦üøho 8ï Qþžy&€š}?MxÖuÛEû_Uk¡ò»2±éì|5ñ6ž-&j žÝ3ï¯TL´GÔÚ¨œ“"%wî;<¿Îî¹ÏFêä>ÕÞ7ÛþÜÉHiõMŒ”0Ta‘âöç4PœNÈ d€ ÎýؕÌà©LTµÞ¤ÁœÒ§×1™Æ ÊºÆ)Ú–ÍÔøšV—bm”«‘ÒfÝÅPùêƒ-+æÇH)e™’hÍÜyÜåpoY.ÎÈBö’…jÉÎ…]üealú`ç‘fL5Ê¿˜65a ÝÆ6ûý,õž(™ŠÜ½¤ÆÿáÇø[Ó{øåæi¸÷Ÿ/Uy{ófä|(LÔ1‘ª[ ÖH=·üyÏíx1Rr týÇ“‘¿_Üù'Æ…Yš=[¬‡ç?ÿü sZŸœÎgùùð¸úLOí‹f nã– 3C¹-§ö][eŸÊwC¼+JFô½QöQ{ˆwIŒŠwF9„!Š|yƒ2@È d`23ÐÒd¼7é=\Ø`Z:ÒJª^CI‚Á,”\c/>KKÛqþ`0`Ô‘õͺW¯…Q"®Ö dá ù )ëü4”¤#¬1ú!¿3`^c}ÞTçõõ5bj_¬Žº^öímý=–þʶ’÷Ál+zlÓ©Ïòcç£÷`ê½?³Z¯TÜl(©DUÝ TÕÔ¡âÝ¿ºFih7.[“0†ncûÖß·¨—í®íÉCq× ½Yƒûî+nÝýLþÕ‡ÜcSú2jæcþû/#ûÍ\ÏíÈû¾zC½t×CÃ7ÄT¾2b`𺊔FÊ0J¶0wé‹m>¡ˆ0G·ŒëœbÓõrÄú'‡µOj-Tañ²Ý!¤[§÷‰óÆÉj¤ä‹v­/âU/Þ¥‘¢Y¤Y&d€ 2@¾Ó DO¥6R×âMËÕCY†iѦ'j £‘V"Œ”uûò¨©2Œ‹|æ5L™QßxŽ,eŠlKU´A‹)a¸d?ÌziÂØÅúº‹Y³n¡Lœs 3g7RÚä%öÙ¨oÙlB|·¾Wu¿º6#—ÌL­ÛZ†ÚºzìÝw¡]»ª¨ÄÎòwâBž{së6ÌËÌOàÔml?‰\ļ=@QÛ2¶ä¢ œƒÝÝ;ñï‰{ÑìÐî–Z¬µó±¨j.fæÍÀ‰ÎxnG›(mœä1¶&Ê0QºìÉHis”pY%µ9DŠkt†ilì6døÍ8)“d1NÉ “5û$6Âî,YŽwÚÔƒz2@È “H£‘‘š }ý¾õ±`c öî?(ŒS%ò‹ÖaUñ†„(X»ÙyExanFÂ&Ûõ{6!(Ö=É—íæ5±â“Ll¸X€ë_Ž`ìÞMdÉé|bs m¢~_ö"2×g9râ…!RÆÉ\%Í“µœ2#e7JÚ Iƒ‹äIOͳOÑK˜ª'¦çÉ,“›Qº6r2ìNxxø:T¨TœsX%6Ò“ j@È d€ L6šO(#5Ùúý}èï¦ÒrÔ=†w*ßÅöeØ*OˆP¨7oÁÜŒå c˜ll{ú†±m UÏSJÈùäçËO-ÁÛŸ®Wk¢ät>™‰’&jNîoÑÕÓïȉW††Ä>ú½œÒHŠér2´ Òeë´:uÎ4Aêh^ãdˆ´JvŒ#mlÇ!Q–184ê)úFÀ d€ 2@Șü 4]:®žk9–ßXÎY¸~Â>†©Æ¶S£‚]ExñíçÔFò=Q2 %wç“e¹&jfþ d¬ ¢£³×•k;ƒ£®õúúGÂSôY"¥‘¦eTd|b188Cñ¡ëêãÀà5±+uô÷_…Sô‰óF Ñg†yþŠ({‰žÞ!0¨ d€ 2@&?M—Ž)#űœüciC¯c{üÜi¬(]‰— _Rï‰zjÙt<ûúoÜÃ'¥dC·Ó{e2º{£¡×@u ÿ ÏërW÷ dXË)TOï€èŒ=â¨ÃzÞöyww?üDgWüÄåÎ>$‹ŽËW`öŽ^0¨ d€ 2@&7ú!˜ã8¹ÇÑiüþ_ckm§­½Ö°®’ç­åÖ¶k¤.‹´˜×è¸Ü# Œ{´µw‹Ž¦ŽÖ¶.X£¥µ Nié„ ý™.;ÃÍ—Á d€ 2@È d€ Hš#q¡¹Ðk¢œÊMáïï‘’Ù¨Þ+ƒb ݘ^ Yv ]_.ËN—írÈpékºEÌKt‰L˜SøÉv±®¿ì õ¢^d€ 2@È ß5œf¿É{Ô™´¶öžÔï‘’sÿÔ€ 2@È d€ 2Ï@õgñõýÊ/ädAždP2@È d€ 2@È€; FJž`P2@È d€ 2@È@r¢)¦ì˜¶%d€ 2@È d€ xg ݆‚?P*@¨ T€ P*@¨€'h¤<ÉÄJT€ P*@¨ T€ P˜4R¤ P*@¨ T€ P*àS)Ÿ‚±: T€ P*@¨ T€FŠ P*@¨ T€ P*@|*@#åS0V§T€ P*@¨ T€ ÐH‘*@¨ T€ P*@¨€Oh¤| ÆêT€ P*@¨ T€ P)2@¨ T€ P*@¨ð©”OÁX P*@¨ T€ P*@#E¨ T€ P*@¨ > ‘ò)«S*@¨ T€ P*@h¤È T€ P*@¨ TÀ§ÿcé\¥¶–\IEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/screenshots/markup.png0000644000175000017500000021075210615044233025011 0ustar cmpilatocmpilato‰PNG  IHDRRXhÊ×ÕsRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.05Ãà\ÿjIDATx^ì½ ˜^ÅyçÛ÷¹÷ÉÜ;w<žq–™ä&&±3™Ì$q2I¼@ÀD@ÚNô&Ý8c=]úÈ4úÄÐÛè/½š½~¦S—Ð}w=!å¦ý}Càøx(Ã`°Øl6€ ä±)v:wÒ–Í{eÚª>õï"Ÿ\OLH9uâGy“yl·n>{¯¹Šöþì§tàÚkèÀu׆ è²XÚóu±ÄÛõ?ÎÏå¸<ד)¤-Ù …Ï´é‹EZBÓf,¡_ÿ­¿ìi÷ãI»ŸþÚ3R©æÿIûï=“^¸òïèì랦¯½ØA_{iýùÍSéwÿå&ÚóÂ'éøœFÚøä9²Ž7T]oˆïÓÆÞDŸúèGéßÿ‡ÿ@ùèÙô“±Áþ"鞯}T”½˜î)X.:ÆcôÕÔ6¼F?ùŒ8Ægn¢g­cØÛËoG±óö1zö§gçb)ÛúµÇ q.rM¼yÕµþÔO_K=nþ¶×­OÎ#gÛ 3ÊYoÄ$²U¾gä}³Ëòí¨ðy”|亮U>wªš˜gð܉ŽãÛVZ;îVϦ»å9({×ëïï"LjÛù ås±]¤n;ï3Î3¥º×£ôv¢]õÅ®VžQhÇàù{ƒk™ÿZ²ðY¿~uuíiúäïú7o3“Þgî¶q=\ŸæïÖÍ"æÊ7çJ,¶ÌöXu ÍÁÂgו?¦]MBLýä*ùiþcÑÄÛt2Eç3Ëq=\ë­‘X3±v çH-YÚ%EÔŒYËèM™–Óâ‹ÿuøo´ûáÿ)DÔŸÒî{>N»nü}Zzë§èw¿p }|ØÃôg-+é³c–ÑßO«ç§ã«¿C&žIÇVÔµœÞœ¹<¬sÆ ·ÐgDgøÌ_L§Es;DZMsDΗ7Ý÷MR—Ò蹫è­åìúWÒ+¿8GÔsÝðJ¼ ÏýLíkåňUÛTÛ¿õhwû*z "íÈÞ1>\";ÖßxpuË'èk‚ù7GŠvÏYI’ßgo¡çJf—³ÍƵžß¶"áúÚmK· ïG¾=FžG^{Ñù w®¶çä`rÎU¯É¤%:gy¿Œ¡ï0ƒ~¨öõë‡úù™SÕdðççŽ<–º¾ýpð(çø÷[Ïe¯gÞN“ÄóbÖ,÷w‘c¶/íÀLA»íc©ÛÈ›ðL)‡ Ê–x-Ê´ÅÁÇ]÷KðôÏÀ ïl€Å‹ µk·—¸®O_?·î¢BÊl“]÷r)|z®¸œ¶ÿx8íú‰T,¦®¾2.¦Ä¶˜ˆÛ8/—ãò\×'Ÿ­J×°fbí ©¥Ë»è­9«h¶Ho‰Ž*§EW]@Ý·}‚öˆ!}ûî"êÎ?¢:ëÓôÿçè?|ê"ú/ß½—~ãö9ô‡ß½‡~矯¥Ë®þ&}°âótøõOÓÞñgªzt}¢î—o¥Ï*!µ@t.ôqŠ~¶‹¸í,ÂÚU;‹–çü/ý<RóÜzŒvê}AþïÑ#¢3_å¯D;Ji;—iþVСm´Ç[×ÈKd¾Q2_ }“;âªs7§ ~™íÎs­­¶6—œTÛYH¶‚ç§í¥°ËS¯ïz™ç<ò{R@Gtü˱ÿÌëWÊù•Yf¶(_Õ$ØJOŽàÉÏ>ß×¼_îð}]îñ9ª|AäÜkm±ßEŽeÛ~R£c©;Êë>SÊeò¥]p‹s«ÅçÚ”õ7û‹h¯ÑÚµ=BH•—´×H³qëf•Þ¦<ÿ8ŸÙž¨î@_H!5ì´ý‡?¤Ã‡[ÉõL™¿Ý¼\žëáú¤ÓJbí ©å+7ÑÜùk¨}~‡Hü¹†¦üàóÔ}ãKoÔÞæOÐöÛþ„®>ÿÐoþåéñ ~‡®ùç¿¡ÿôÅëèÿºäEúÿp%wáéôÊÏÓÛퟣMýUX×5—ëÿËPH-œÇÇám·‹mçÐu×_*;4œ>{ýtj½>èàèßœ÷¡‹ô°ÀKé!ÙÎétÝYÁ¶`¨‹ÞîÙwÖíÔ*ˉëu<}Îñýñkð²m$:ï¤üñíqÛJ)kœ»n«¾núzæf¢¯ )}¿Œ xs æ%]»ôö™÷–ÝÆäúL›‹_ç|÷¯Ï>Íû ¼Ïú¥¸Wù仿ƒgS)©õúυωÑóìúY\-”ÛDRÏ©ˆM°]—7mP?ŸìgçW,ù2—Ÿîï5Ö}ü‘~NÜ;Iç¥ÊJ;ˆçI<öE¿tîéø¹÷£Ý>§oJg!ï]³ÍÊ.u}¥\”)Í®ë÷MÀ6Ð?6ÀbgíÚ´jõÖ²×ÃõékéÖ­…TÖ±|ù̺¹oÁÂgó÷¿OÛ¯øõ ¿"–|bÍ—Ës=\_¨g”Vbí ©U«6Ñ‚Åë¬ôËï £™ŸúO´{Ä !õßèÀÝD½7ýmýùŸÑÁ[ÿŒŽ<ù¯ô©Ï6Ò¿ýr3ýßÿx+ýé_üíŸÙH¿^y>Í¿ñσºÄ‚UAꤓFЙÊ#µpþ:š/¶ÍWÛ>ræ/iâüµ4I ¹“Åëg ³Fþ–‚g<çŸ%Å@0´o-¿ñsòùõ¢óÎñÑ¿àNËZ‘ïYú6‹ƒ³î ‰¢*÷}›ëà}â˜"é²7¼²†, ¶™)دʨ6^ô°¨K´Ñm‡>‡3ox“ò°›‡ƒÎîE£D›ý¾ò åDGqŸ¿YHm&]§ærÑ#A;GÉŽ sÎoá‚ø9h,´‚v?Ci”4ÿ逕:×ãÇÐh!.éíÜy“e]fŠ1wÅ~j7œ´éæi\ëIª+ÛÁm•×>(¶Íȯ‡.þvÐY»~Ÿ›q<Ùö„ã©k`óòœ÷ø;[ôåµEëû4ʰÙÔ²õæb¢ÏÙ¸‘=1Çgü×.íÜ”=rG9¸/‚kæ/„X ÇlAÙ¦xpDeìëœïþM¶Q߽꿿×Ð|ñ\)%º˜Ÿ Ÿ“×ý;‹z^eø]t?cĽÄv.· »¸1¸—C;­Ž¹ àfÚ߃ ¼i<›¸êÚÈ—1¢ÞØoUŸËT<+ãçÔe½D϶ñ!‡™þc»×QÛ…:·ð9õ·o]ü™2)…|ñs[þíËÇ嵚¿XôcÔß7Ö5¬B!µzÍfZ¶¢‹–®ØHËVòg=ñÀtÿ¹IÝWü6íùå´ïþӻΠGÿˆÎýÿ‹þŸÿu1ý?gÿ„þÝŸÿ+ý×ÿïhÕ-!C ¸|}ëß‘õ-õ…uN¹“ÎpÏoÓ‹u«xß2cÛ¢ù´ôÉaJxtyÂßkiÉò6ºùœHH-Qû>òÑïÓ£ËE]¢¾%â$Í:d|Þ·ë¾O¾õ\ÙI¿á•h›lNª]gþâMzíÎ+Ž!: ‹—p»¯©ºn$8¯çéb5ŸgñãûcÃD=çÒYB ~gT'½.Ë™õFÇ×íó×¹‘¿Äà`¶Ûü.ø|ä£?0Ú­Ú"Åæµo‘¡’ÏòõâÓØ¾p½‡™±_Õ´™½|¢}]Ï „ñY¢óƬ€ŸHnÛRlƒ¯ÕÏñr]ã¼Sù¾æ±Ó' â´cMº)°+ïuÓõæebØÑ"y ó³ï/ ˜¥ÿÚöêoã,&„Íqç_^ ãþZ¼xlh·¶-Dù"ï¯syÞcæýkÜ_ÚNû]–_ÉÏu¯>< Âû›·óý­òp¾üIݳ¢³?Zù âûñì4éáï‡Ï‚ص]iÜ¿K£ã÷ß¹t“|‰#ì}¥ólÒå´ÍÅ~‹ºZ‚ó‹1—yùQ!¯„c³7îé˜]8çf=SDb÷HŒEЮàÏi§…®K‘kˆ¼uËVö%À6Ð6ÀbgÙòm´Xü ÌL¢_áÍ£¶s=²>u?»uk”Vï³ò¹u+ ÃBjÃÅKÔs™ðJ‰áyÛ.»L&WDiïo×y8?—ãò\×·”ëæ¿qªÃÚ)RkÖ —Ýš-´Z¦­òsî¼åôð7¿BígÿGêþa ¦öÜûqÚwýïÒOý}ä¿~Šþó~œ>÷û£w' ¡Óß$꼄nùÚŸÓ¸^Quuʺ§ßEgk!µtS°ßÝ6æ²PH-].ö[¿Ûé¶ÏiÑI+Dùé·ÃuäóÏÝEÓyÛ/‡¨ŽQ‡¿¨ÏÉþŒò¬¥å«|yıX´y)}çlñùݧE§$8æê5v;ž¼Ô~[¶‡;2‹»è)±ŸßxGtbϾe–xË-¼]b(ÝM±z£ve¸Ã«Û÷"}O[0ê¶)~²ŽKž5Ú­ê»õêzÛõןS|»Í,cÿ´àZ‡,ø˜úš‹úcmË´ç­óç6šíyâ{i¼ž ÊçÊ÷Õ;픇Ô=&;Ñþk3:*žë¦êÍÍD]«³ý¹;éuî°Êëç¿viç6éÆsÕý¥îë^I°u²í²½ÄB0/W×Y ¾¾©÷oÜFÍ{uÉr~ÙišußMÓœý2ÿ˜Ë 1r9µøò¬y‰.ágʶèQ! ÄpÚ¿Ü‹Æ×û;¯¥Ç ;Z¶ŠÛ”“ANo”í›&˜ñ5ù òÍ£[gÓr]NÚÜñÜPõèߪÍ>¦A}fŠ·ÁÞŸpl£Íœ¿%õ9µQî¼Ü´\å×vžÄbÕ˜èÅ?¹œ{ ñLʵèï’ÿï9öƒ l z6Àbgñ’Í4Oô;ÊM\ôB©¾¦[·HYÇñåÓu³Þà¤…ÔÆK¾G[~ðƒ0¹"Š÷¹s¦Ìü\^ )­´®aí ©Îõ[©³«‡Öwm—ŸœÖ®ÛBÏÝy75òOiÖ'ÿ-u~óc´û6!¦XP‰´÷þ?¡Ã",:§– ¥wg7Ò¢+ÿœ~xÉiõÚ¢®K¤ \¯HoÝGŸSBjÉŠm´ž·Ï¾×ÞöÜ¡Z¾Zì·~/ çê?ö멃ˋ´n…1$ð†6Z–YK«ÄÄ8y'Íq^ø:)Ïúç‚¿y¾e’è4 ê±Û1]ÖÌ·²#j©2ª=²ž×:„·æ:G‰ŒKF›õFí ÚÇuêól¥ïëhÇØÄ9ØçäÿÞcfýªîÌ-ÜHë乨õFuÄ·ÛÌ2ö¿\WÙUCÏäkcÓj›k1Ûg?·ÓlÏ´;Òx½”5Î;•ïkwÅíT]ËÅðÎ×ne¡î¿6“nJÞ·HÕ››‰ymÌHm×.ÝvæÐâ)Î.}’‡fò½â·… ¾s¥}ó}Ø^àÍ]¥®3 )yO§Þ¿ž{ѼWשç„|E)v;ûeÞŽõƽ'î7Ï[=ž-Û©lIœÓ­|?ªß|Sn7ìHÖá0Ñ÷ò%üb¥ƒ–‹a A[g“.§mÎù=óN‡é0ƒiŒƒÓ†Øù'›ï=~f©ü³ä1“ŸSceXHü¢üš§ÓÅTÏW¼eºÿú™×ßÁ¨ûºOϨ`Qe`±3oÁFš5»C&-b’ÖzÒù|Ÿ\\—Jµ9©î´:Ì6˜ùtÝÜ?ìܰ] ©Î‹¾+S×w/!D®ˆâm:¹bŠ·s9]×ÇÏ/­kä§ÐN¡Úе¶lÛã¤Ý4¯m!Ýûýátãø-šð{ÿŽVþïÑæK›¶]û_h÷]§·ž!ÓÆa@“¿ÿ'tÞ9ÿJ¯N˜L7‰Å»d}»Ußç?@ç*!µlõŽ`»»mÜB!µ²Sì·~/¡»†è?ö]Ô¹m]vù„°î(±Ç`ÙêVºLuœ§rÛ‚r—ÓmÙMí÷œ¯:ƒëhÍÆh{Ô^Þ&ê×ð¹f>§âÑðLPŸÓ¨=<÷x=/„–’çæUßUgþb¦¬3hﺕŚp¾p¹ÉÁS^pûÈG/§'Eg/ª_µ›;w‹6S—<¦jwºD½Q;Œ6Ëíúœ¹Í,âå,¦mѵ–íý~0çæéâºûÚ³ }¼Ëèqqá55Úé/¸þ>^ó»1Ï;¯º¶ê¶ “CâVÎ ®·÷X¢lÚ>]o.&‰×F_ï„k—vnO/ιQ/AˆÿÀFüõ1g.ó‡ùº,Â߯u–÷têý»[Ú-—îÅÈÎâ÷jÒý­žî=“ö[µ‹…0·SÞ?2DtN,p—M^ö$][¾~^AZiß/žg“~vÈ{͸ŸÄï·Ò˜ÆžGI÷¨¶ƒ„c»÷tÆs*öLI}©{ê£|ª°ügÞAÓŸ¥IÏXl=û‹Ø5òÆÿv‚ ˜À*j,vÚÚ×Óô+eJ QÎâJçó}r=\Ÿ~îùêΪC·ÁÍÕh>+Ä4£Žo~‡Ö]t }ågXž§õ_Jn23p~.Çå¹®ÏÒ4â¬B!µqc7íÜ}€vîi÷Á ‰ïÛwì§93ÚèÆË®¥¯œñ—tó¿ùé¹ý[zå÷ÿÿ/ÿŽ&œñš$>?ÿñÿJç}á»4¾umÞº3ªk×¥ê]<’ÎSo…—¯Ûc‰³m•¡Z½Qì·~¯¢ûÏ×b#mReáRCî¡)ü–XÖÝ*‡sé}ç=°ŒÖ/î Y§8îâ.PoÙ×Óúnu¾ú¼ÏW~¼Å^´xu‡Ûvðöqæð¢ èþùÛh¹Q†ëù«ÐÛÄy}äžÃ ÎÃs\ÉÆ¨“Ïãògø­{Ð^®/x{ì//÷{>^ÿ„áÑ0¨óGÒâݯѵçhåŽèÚóñ'üÐò\è7Ïì%[ß/g1ï\Wq­d'ôÌ4â OÛœkk8×,`”q<¾†)¼vÆÎ;%¿j»5¤îüfš¶%ãXIí(Ê$íÚH›I¹vm°çæ{]’Vß*ºG [Óe~ø@³Óiu[‘û7°Û@„E÷bü^]Ü«ªîØý½vwðœ*"Ûä㊲㯤soU÷µ¨'¼ŸøpÙ=Üw³î¹ v?pÛÎ{`¥hËJûÙ¤ê”çú¨xvÄ~¯t˜jq.˜rû¬s{Õ¾Gcçí;%ÒsÊdÀ÷›>†—Å,!_hO#^U÷çøàYñ‘³î¡ù®K‘kˆ¼Ål~Pñâ>ÀúÅXìÌzk½þúR™²„”Îçûäz¸>}?ûêNòtù¶›ÇëfÝ!’R,‚Vë»´îÛ‡:‰¿'%‰÷s9.¯…T ‘"­ÄÚ)R[6÷ЃGƒtè(í?xD|çt”v‰BË—­¢±O½H?üúéì¿h¤?þÝ3éþógé“r{Î7é—·=@Ӧ͢îž]A¹CAÙ ©ï«¡ó•Z±á`´×vZ!P¸mëù–|íVUÞú}6.á?Þ[¨›ëVeÃ!uK¶Ó®ð¸b1cH”yÌ•#C!µq—n§ÿ³{µÑ¶°n§¼]µ3hËzrëåzÂsÒùWï68yŽoÕi—çúBa»¢:ä~_ýf’—┕7d©Ï-^n׆ Ó)Ïݽ®úZ‰cntÛf؆=u5½4nnÙ‰ëáú´íV§î@ƒ°šûåoɤÅö.åýärº®O·[j%q ÖN¡êéÙNïœzN‰ÄŸAzWþ>%>ù{°_m{GÿòùD’Ûí}a}§Ï«ávùèçé±F9³Žª~ŸEׄmà ô]ÔÝÛíÀ1C›áë½ñ i<ŒrÕÖ‘=UÕêó¬½;ˆÜ™»z­b/˺CýÊ=zî˜Ï |Ø@½Û€õ·úõ9kQý;#î~•ZƼUvâz¸>mGÕ¨;Ð.ïJ!ÕzÖP)„^øMZ8ôÛÅ“(Çå¹®/Ô@J ±v …ÔŽí;èƒÓ§éƒD:ý!þàCñ]$ñ·Ÿæmü)¶Ÿ¼óêO.ä ÊÛƒ”áí§Oì£ÕáИ´ã˜ª‹ËöYê¥ýÞ†¾<ßr¬c{¥m¬î~»ma€°©ð½q¢{£=Dm}o¿3—Ï$0€ Àb6 û"ø údH`Ðw6Ðòü )~X•›¸®O_¿êÔh®[‹)Bå$®'h·Ò5J±v …»«ð@@@@@˜À®;¥ˆ`d¦á×>(ó§þîæqs=\ŸþWͺ8Š)B:½ôÙà;êïæ~ßwn7×çþcí )v³ñ !l6€ À`°Øl6H¶ÖN–š5{…Xl `°Øl6€ À`°Ø@’ Ä„Ôc½D?ÿùCH`€ À`°Øl6€ À<6ÀšÉR<ñkÜË3¥‹êäÉÞX:!¶éäÛonóæ;!êLIfýáw‘ÿ„NÆñ£ý‡Å~•Ôþ¤cDõ‹ü'ƒdæõÓ©[æ1Ûäû~\äÑɳÿ¸ØÆ)±žã¢m2ùë9.öɤꑟ"ojÒyó©:uÝâ󘑢ºÍ|Á1­|ÇÄ6•Âíâ÷1•ô>þÔÛÌòá6Þ/ëæ|¢-aÒå¢mGE;uŠçë¡*Åþ0ño;é2ævß1Ýr±ßGÅqÂ$ŽqÔI¾vXe¢òGÄvNQ}ü»7Lz»Î|ªýGÄwŒ2á6kp™Â2Ѷ^±--yë4ëïu‰”UµŸó§•ñí×ÛÄçᜩ·÷8N2Ëõ¦¤Câ˜2‰³üþb?'£N«¼Þo}êúUY¹Ï8æ~ñÝIûÄï0‰üûdÛ|i¿ØÇÉØ§ë·ò›uê2Ö6ã˜jû^ñ©“n¹mï>±_'×ÜVÂ÷=¢LYi¯(¯“®+k›¹?ãûn±¿ä´G”ÕÉ[ÏAQ·JfÞð»Ø·G§ ®]â·NaÝ2o—Ì$s¸}·Ø§“Î+rߥ“Q>ª+:¦Y¬Qv§\>H¾cîÇvSXÜ•¿«…Ty1Õ»DN¾|Ö6ã8ªüQΗ¢ã¨ºõ1ÔçQžSxlYO°-©Îh¿ÎgçÝ.Êr²òí¿¤óYy=ù¸ÜvTݲŒ‘7Üoæ ¿åu2êòæ5÷'—éeuÚ¾C”ÑI×inËøÞ#öë¤ëéÙqHl R¸Í8¦YÆ÷ÝlS¸¿@y_¬•X3±v ‡öAH)¡h‰[¤AHAH% *©HŒi1!e‹4))¦ ¤”°*&¬²D“O\AHb BJ !)FRRAHiÑ9ŒÐ7ôCûì¡Ú§†bh_0ä)5$Py 0´/6)x¤Ü¡}¯¿þ:qÊèîÇÐ>Ì‘ræTaŽæH©yRzΔøÌšÛ„9RÁÜ)Ì‘J™'…9RÖ<)Ì‘*cž†ö©¹P˜#eÏ›Â)Ì‘ŠæU™#¥ET)b B B BJ@° ›@° „Cû0´Cû²‡ñù‚I`h†öY%¢`ÚW[CûBÑ$æHù„T^QUHHù¢é¥Fõ[?’Î;o$­w£ìÃÜÂòF?3Š·~_¤;_$?½Mµ£SÿöEÿó–ÈŠÊWîþ”ˆ~Ì Œê—µOD<.£ F‘ÿ2£öù"ýYQÿT)OF÷‹¼Tævý=-’ŸŒæ§#óYh|:*Ÿ!0í/ÌgÔiEýKŠð§"û­IC†Œ¤u²L°Íö>ùê5"ŠÈwÇ8ŸuOE\÷ ¨ûAZËûÌ(}ad@#b`Â0>¤?;bçñEí;zD´K¥#â““ÙŽ˜Çh ·w$­Qy­¨*@D/{¢TÊé/Ëseî7ëìí®M‡/: ‰/«þL!T`îTZ]¡çIGés?Í!yžH~fä?ÿÐ=ޏ§’ž»dÖ©#õ‚'ŠÈÇÑùâ‚È:Žg‘O%Í/1‚ŸŽìg”ѧ0âžáOl?˜£öÉ(f”¾„ïV¤¾(ÒGç RÕ/ünEò+°?ŒÖgDå3ëRßíè{AT>3’Ÿ?ŸŽÞ'òЍ{:iï•ù/òh©ˆ~2J_<*ŸµM yÛ§’™ÏMF$¿JFíÛ#êådÕ™õ/¦wP Ûó¤¼QûÌHºž}¢>¬`ê8Bíñ%•׊ìç‹Ôg‰*#Z_µ/¡Ï[gÅϡϊªgDø‹æ)Eú|‘þÂè}IQ÷82ŸJQÞhÔNqL|ÇôEÛ³æP©Hy»„ÑÉëi2#ùéH€N´=eo‡8™DM˜ÏØŸ¡/ˆ èFísQð·ø°ƒ“]Po“ŸA>30„̯’ŽÊgGê‹‚Hèó/X"ªÛ Fa|w#ñqûÔ6k^“ý/ŒäçÙ&÷©}V”A_Ô?O€ŒBÁ"T` W8qûMáV: …IP·¹ Ï.ÅŸÓžN\Vˆ¼˜¸’¢D®Èâܬ+44ê<ÑŽiêI‚«ÚBJ>Çuf¨sÑá?ï¼i:ÿu¦ˆ’áÓ•bÑ–íg!ÅmqììÊ)Ñc -3Tú`R«é¡!îýüòPGí))Ò´E‘g„”è!%C CHI1!ÅáÎ!¤’BCHÅC˜CHÂBÊê—"¤XLiqÔ·BJyœ|kGiS5”C,TŠz¤*)¤N(áÔ©„—Ù®Ôu¢’QTÄ#•&¤\®/U£BŠ=IZ(Yž%ÓãUG©µg­önÕ’G BÊT°öévIá½Ù?ï‘gQÜ‚)!B:G ¡óF©z¦ ¾›í³¼gêü•Ð =d ›'…·o8MÕ ö–:´/§Gjh{èUdoÕñÕô°ÉgÔj:Ξ¬Ð+4œ¦°¨Q^«QW2KÑ^Q޹^9•Å{·&Ó• CèáuJì”:´O '-¤¦§†+'C-o•j—l›ðŠ˜íå¡|¦›"êh8—šÂ‚d8ýXyz®œÒA‡ßƒ!€ë>×𺡇։ávj(ßCêܧ˜CûdÝ"ßÚ^Zû°Á6ú®g.¸k/à›0´Ï'¤Ö˜ž;ÁB1伋Ég¨†öý0´ÏZÄCûò-¾‹¡}jxßÚçP¾¡‰®wÊ‚hгò…T®¡}Ü¡"A{ Q¶r/‡ëññD‡þ 婪öÐ>?B)ð"Ñ6U´a* khßkO!x^”^,d”ˆ¹r*‹&5,PŠœ×‚ß¦àaÑS޲ÎC‹3uÕ0¿°]’«hï”`HŸB¢M¡’íBƒç3Éï,ŽDû”’¿ËùTÎ)Þ.ê’BJãÇârΔRr»1RÀðq‚yUS[yÌJÌ‘Š ©ÉôcnÏä`~”G?ž,Ž¥·‘øÖ<$XüøµHHIñ5œ&«}“,¾WBHùìN÷{†óñôB½Zt™BJÜ‘,®x¾Ôd¥"V(t5· s¤x~æHi1!!%á›!Ñ!¤¤'ÌòbñÜ(Â{Í9P˜#%ñÅ©`®ÖR¾¹QI)Ky¢ù•/¤òx¤’æRq€ µÞÔÉR‚M$y¤Üà}!¤B±x“¤gI·Ï;—KäbcÚ•wjšèdOã|,ÆÔgÙÁ&ry¤Ñ!=JIs¤ô¼*„B KléùU,ºXŒõÒT”Bd•lÂõHéßî?³]¢ÓŠ -nBïU hd° UÇZíÝÒÁ(ºYTšÁŠ=K¦J )Å1ô)Ãl¥€Ò©BŠõÓÁ#”@š¢—ö2IO “Ð#-)Vt¸óJ©¤9R–7J{…ű-”9¿Š…—VÊ+å Falƒ‚‚G*%à½]*AÁs¥Ü:| öšót~32Ý ²üDg8¡S‡°æh% )w¨*ËÁ%’„œu\+…-/ðD©Ê3u\t0;¸õIOÔ4!>d[xhœ!»Ş©0^B?ß~+jŸRæ|(-Šb"ËTìM2ÄÓq7¯+¤„@8n$) ¦(AłŕÏÌî·"ðq9•´` ÷ž<=œ.LÊ$óóEdBy‰”ø™"=TÁÐÀГdˆ*ÕO ©µ\ö\zx­Q—%¤Ô‚º2ÿƒ$ë–ž 3j^$V’;*Ÿ}ë•ö¸]âxk”ø9ª~óœ)ÄA &c»ô„ÉóÂE—מ!á) U¸MÕe•Ìõ¢XÈ)Q×aFÛÓž'kØ_¯ð:©äÝÏ^)RC„‡Jå3ëTß‹ö…©BQų̀z¦8ÓÑú,Á&‚Eèüa¤½Œ¨}©‹ìºÁ'tÔ>óSGô3?­ýÁÀƒf2¢òé¨~Q{Ýè{ž¨|bÈÜANft?3ª_8Ê\KʈԧËézŒ!x2°D˜¢¨{öv{Þ@0eDô‘ò8)ŠœÇQô‚d‰/O>ó8ºŒ/ÚÕiEØÇà¨}V2"ì麒ö§›HÔçÎgDåÓQüä§j“7RŸÉ/)ª_F¾´h}at>®Cx¤ê =K¼Ï8N¹‹ïêz­hyž°ææñü™¡ÎE!Ôõ|(³n³¼÷»^'Š=H¾¤ÜÕ^$ùiæÓûãeÍùYf½H`ùê1ÖiÒQóøÓŠÚx·Ì¨{Výjqàfùð»Y.áXa´¾ ‚žU'ªŸ ÏÁO{™‚Ïh®•.gî¶ÅçdeuT¿ŒÈ}fT¾°MNä>Å/ ™®¢÷É~¢Œ›ÌH~æ|©P(šåÈyV”¼*n£ö‘ø¬:êŸ1ÉôévÚú‰à,ð(SBýaT@':_žÅwÍh~¾!€…Wp,4C m æDÃü‚¹Nw‰óYCûx¢#¤ô|ªós«ª!¤ä°;ö8©!‡á>5äð˜n¯;´/ÜÞ)Bì yXDº3=K¦‡ËR\Fˆp˜`ŠâðçS~¬¢è oÐ9šž ™Î÷Ä| ž_åñH•-¤ŽBˆ‡ZCøŒí|ܵzhŸRBÀüø¡Õø:¢Ú(†Õ¹‚IÏM*[HIaÔ@rÞ“Qü™$¤xhŸ ‹!ŠLRJü@He…<‡RCû¬ðæñmRj(_(Ê ¤Ìá|fØsSpÙß!¤’„Y- )-†²Ö‰òíïG!%”'øƒÍÏõÏð`%z¤bs1”€z#5œÎ»Jp…”šË$÷ S!åÌ…Š„”Uf[¹=ÒÃÅþs…p Ýqcz~”[^U”öD)V¡¨:,‡æ|„Ðb¯TB°‰˜¢a” £„h¨˜rBìë¡yñ j(¡7ØD$¤Ž)±5DˆÖpý'Ÿ⃅‘¶¥0(ÀlT°ŽTàòRF»‡a)‡÷UEHio“1\NÕ“^(g{è‘Òž(F]‹*[LUNH åïc¡d )çZO6è‹ø!.°ë’[¸W - í“Ãñ0´à‘‚GÊ7,Cû‚¡tÚW=ñdz§‚J]W/ìYx¸¼¡}B°Ø‹ñFoîcA$ª$¤’Ž[WªÊB*©®˜ªE!åk{$¸t(s[x¥ ©`ŽT4oªì9RBH%ñåávQª!•ÄB‹¥ZRImäíQs©\s¥0GÊ™ •5‡Êî!!•1¤s¤xNS¡s¤tЏÐ©ÒEW­ )ö,e §¾Ú”³þ”ZW‰¶ºõH9ªJ)YN- )3ÐDÖP?3X‹°’×‘Š Ûƒ‚â¡€îÐ>)'jŸ3„°s¤ëÐ>©pX^è­‚G )5ßÉŠˆ9R2z¢ö¹‘ûµÏŒæ‡¨}*‚ ¢öIñ“gͨ´H~ÕŸ#å›åÙæ oî›×”lÂ;LÏð麲òUaâ|(!8Np2éÛæiSÒ0»"¢'k“9Ç*\Ð7È'óêýÞyQÑð½c"ŸNþã{æP Ñq_RDz†Ç÷Íó Ù3ôõÍ‹2Êè¹OæÐ?žÿ¤“ÞnÍ‘RÇ.  ë4æMÙaÎó͇²ƒMÄÛûìòÙó©DPòÜõDs ŽŠï:eΑ ד ÷å ÇS åʺÓ÷›eôpC_=æ63¬x¸ÝðBYû}Þ©ŒðçzÞVÒzSá~#¤yR¨ó(äy4')Ìk–÷|÷ék@© ‡Å§NV´>-ªT>;¼¹Y.Ú#åêãzͼú8f(s;º'ü¹u†9çÐæF¨sµÿ€8®NÍðèžïÄ6Nf=É¡Ðí0åfÈò"dy˜ŒèûÅwN¾µ¥Ì2:Ÿüô…?7<[¾ý¾êf>ÖÜ“nKˆ"IûıÃd %:}?‡VÊ„¡ÊÍPæ‰åUhsc©´h{űÂd„×)öîûÃd†#×Û£mVXsÊÜ@ê{Rht³|êwËótPpS4Wj·8&'3lBo··å}Á)txó¤æaÒ\´M¦° mr’µÖ“Цgæ‰æ>™b*Rº~ïšQVÈtOus¸Ÿ±`¯k.à+C›¡ÊíÐçfôäï¡Îu8u#׫”GðHqs×ſᑲ=M¶—‰¶Õ ){á_ÇVtŽÔSO=KH`€ À`°Øl6€ À"ÈÚGø      `ÈR…"õah†öah†öy"ï!Ø„˜ße@°‰ ˜‚M¨à6!#ðù‚MØs™l"Xç Á&‚€6a»øÁ&ò„Cwòœ8!¢É‰”(Ø´83>uÔ¿“æ¶*Dàó/žëDÞ œÇq#Z|×MO³‘ÑþÂÀÁqä±dľ("Ÿ•ÏŒÞgFè cDÝKÊëì”;f$5ÐŒÊgî×Á$Ìèv^p :Á‘ò|‹ëºkI©r\6¶ÎT׊Êç‰ÚwL¬Ë¤SÕO{9™íÐs›ŽYQûŒÀªŒ0<¾5WÊ.–‰Žé‹h®eαò­)EõSýœ¨~±è}N(õpA_#*Ÿo‘ß^ÍO'ß¼§ôõŸ²"øeFíó…JOˆð—¥/×v_¿„¨|Qä»(’_Ö¢»º Yù|Q÷ÌH{vù`®•5O´Yס£ú™Qû̼:R µ_Gè3£ïù"øYû£ˆ|a4>#ª_áψ´wШӎà—6oÊ\C*}Á^_„=;êŸ`!>U´¾ýâÜÃ$¶Ñ÷üþR·sä=•ÌÀz›ùiŽ¢öÙI0"õ™ÑöÒ¢ò™û|e8pEîä@fyÏ~_$¿,!eî·Å•ˆÎ·SF(tc¿ŽÆ—µ/ŠÐg©pïµXèˆ{¾ˆ~IÛŒ(}:øƒO(ÙÑü¢hþ€Føs0ÂXÂZ„× ™®¿û#F‹÷òB¾:E úÚü´à "ù™ ÿÚü|QûŒè:ºžŽÞg}Fe­yQa„½(jž`±ÍòEËKŒêç‹àgEøóDðÓþÌ|: ƒQßvñ]'5ÏÉOæÓü¢2V>UWXŒŽg· ¾ÛûsFés‰Ë(xf„?Ï~sî‘/¹¹Í»ß¨ß]ÛÉ-kÍsòDøó•ÏŠÐçÛ_Y„„”5žPåRZtAHÉ0ãÞ5£<ۓŸCH…áÒAeŠ7© äy|‘^)%ÀTxu© LzR¨s)Tqf„D·R²(‚bÁâ„T‡²ÂšCHAHAHAH…kUž)åi‚GŠôMá'„”?ðHÙb©ÀkT0ÔO{šà‘ÒëLÁ#•äÍ‚GJy¥L=ûš²m¡R:êš‘÷Âï†HóEèÓþÌzNœBF§Ðk 3*`ZÔ¿¹sÛhÙ²Åâíý‚‚JGÓ3ÅVa/Œ´gˆ-ØL!cyœT^ŽÆ§Ó~1Dêö¹Ûé¬g6Ñs÷ÒË]GiÊ–ã4mË1·ö0Z²nmßMß~½‡>÷Â&¹d'8rHz²øó ¯l¡OŽÝd‰+ShE!LfDøS¢Œù¬Zµœ:;×ÐÖ­›UÚ$>£´eËFÚ¼¹KäY+ò® C‡ö†bÊŽÊç‰Úgzš|Qù<ÛXD½2ò—tÏO¯¤‘7_#Ó]MWÒ3wþBŠ©…í³heZøÖ‘ωʗAbo¿óÀ`°Ø@Ù6ÀSzºw–•¸ô>#H„7BŸV]×±S %äÄ!/^%_bÊÎoFçã},¢ø3š¿ÐA&ìˆ~FT>í¹2#ê™ÃøÂíQ„=³.n#þÕ7y/9CüòÌoŠåQÑýLo‹(®?ILUUHí;º‡.h!Ò4tþý4e낚R‹Í'NóçÏÿ}ÄTõ…ÔÆ½è\á}ºcÑ~Z¾ÿzyÝazlÙ>z|ùþ0=!¾·¬8 ¼Uè)ñùå Ûè ­›¨c÷úÂË›¨é­ÝôÉg6–%¤Ž9ЍC‡öÓÞ½»i÷î´sgLÛ·o£žž­aÚ±£G‰©å¡˜ª´b5qÔyýO©ùçWÒ›Ï?Jo<ó0=qÇ ôà/®¦—ºƒž¹è›2=-ÒøŸ ƒBG¨ìŽ„5^,À`¥Ú€R;wì¡R‹°j )SLAHÕ·P©õ³¯¦b±¥ÅTŸ†?_{pÛv}yþ½4k÷2úÁÒÑBH-]ºHˆ‚Ò+ÅÞ©vçSÕR,¢>÷Ü&šÒó6µï<)=O-gµX<é䊪gV¤›ÚvÒ_=µ¾?µ‡ºzߣO¶t•,¤\µgÏNêîÞªÒñ¹ÅQ ÚF˜Z#{¦*)¤XD½úØÝôÄm¿ ‘¿¸Š¦>û0-œò<-™þ2½úä4殨ùÖ_Pç¯~DËïü1­ºãG´ìö« ¤ ¤ ¤`°Ø@¿ÙÀ@R,¦|b-Ë#õo.ÝG_öjœöú— C鋓ϧ!Ó¿DC'†¾øÚ¹ôÅ©_¦³¦žG_šþ÷ô3¾@ÿ<åLúó7>'·q²"óÁ#Uë:¦ßÛ—&¤æÎ_F3ÞœCíó—Ò‚%+iVÛš9kmÙº'>\Ïã‘ÒâIÃSUñHMêYHgκ.âiòŽù4kÏ2ºdɨ!¤–,Y(… žûÓÞþmÙÒ•CLUOHè=DŸóŸ¦öœ¤¶íˆ S$¤ž¿9™BJzÕ~úåÜ]ôJçaê,CHEÃùÖ 1´ŸvíÚ.… )K¦J„ïc1ÕŠ©ƒ÷Eb*¦ÇCíTÊ9´EÔä'î¡gﺅ»ñjš,¼Pó^{†VÎ~M¦…SŸ§IBLýÕ/hÔ}#hÎ5ߥ7þ=j»ö²Š ©^l%7=Ùòl¸mùÊ7Äü¬YýöǺԷ¥(‡7í°Øl ¿ ¬[7™Ö®]C7uѦÍÅ÷‘^O|öf!õ—ßê¢wÞ?(;ؼ}Š~M;i¿øþ®H»Þ!ÚÞ-¦ˆïG¾+·ÑÛ"½/¾{–ÿÅAHõ»4X HR³ßZ@+×vÑÖ½½´Aˆ¤uÛP—üѹm·Síq1U BêWé³3Aw­{…^Û>—^Ü6“æíëBjNÏŠšÚÇ)ö²lÛ¶Y ƒ®®Né™ÊBQ=!uÍÌ­tïÒ´hÏÛôðböDíWiŸOO¯fÇûy˜Ÿ)´XL­[×!çL…bªD!µïvšòĽÔúàj¹ù'B0=HKf¼D+„€Ú±n!m_7ŸÖ΃–Íx…Æ?v=w×ôì#Ы—‹¦üü ê(LÏXªÜœ+u4Z<7iŽ‹(÷ß‘£Çå¦ÞÞ#Â%¼„–,"  1…·ß°Øl`ÚÀü…ã©{{-Z¼˜{ü zpäH1·h‰øpˆV­žê½æ}%¤>3î zzùrÞS´¸nö)=·©Ôoû)8õŠ´AüI¼yÌ~úæå“èãŸ}~ï¯Òÿõw¯Ñ_}w:]5þµm;J'YZ½»‡VþI„ÔÀÒ1ýÞZŸbOÔââņ°{PnÚ¹ÿ½)ÿ[{°«Áfü’lî‚…´dÉ"zá¥q4ñÕVj}åzæ¹çE¿a!-Z&•çEZ_ ©?yêô¿_½ŽÎŸðSz¥£M‰©* ©KW /Ô»t˜Þ¡vámº i)ýæÿz“¶ '{¦ø.Ù@ôûŸ¾þû'›ÄßO±Ah©Žÿðo ¤ú]š ¬ø„ÔëS„½ O”) –lÚMí;hþ†´jÛ>ñ²cmØ(F¡‰yP2õ—â ç¶ÝJŸŸ3‚^î~‹Æ÷´I¥…Ô¢kiã‘Úv|·èÈo¤IÂS5ÑI¼í«" …Œ¨g„?)¢ÍɈ𧄖¦ÜŒè‰ÚÇyÙÛtXtœyX§ÃFÒÛø“=%²{ÉÒ…´pÑ<ñ†i¾XóæÍ‘A(òÏ›ÊFÝŒšg“·¶m£B<½Þ}’îZ´—îUi¤2ñUÆü‚ç×Ñ3sWË9]œÆŠïç<·ž¾:q+Ý.•¤GJEþ‹Ö„ òɼbGí3çMñí¥r8{¤~0¢‹þøo§ï‘Üöþ¯È^ºÒ'þ­Ør‚–®Yx¤Þ;Akþô£aÝAýAÚa¤ðXb˜VŸGí;¶ŠfN_EÁ¸ü«liÏô.½òêt9Œ…ÔšíûiÖšnšÑ±æ®ßIK6ï¡[÷҆͋¼dMbhs³¾ªÍ‘Z»#9í:úö¼û/”R/m›KOmžJßXxýÓ¼›éŸçÝFÿÛHÿ4÷&ztÓë4tÎ=UR,†ØKÂ!¹9m1ÿfïDX¸Pˆ'¹oñ’QeyþÔüùíÒsµGtæK[o*.®Ò„TãÓ«é•­oÓC+ÓÍóöÒ­"Ý>/}{Òfº@ì[Ö±šöíÛ ŸíÂûó…±"2ßúë =ôéI=4tæ.ºyÙAz ã°˜¯&„Ô#« )¡ûöíVž§@H±§‰EÔÉ“,˜1µrårá¹Z/=Q:±×jΜÙâÉ 1ßlc8Üo™xKWTH±ˆšõÜ#4{Ü“ôâ]× /ÓÝ´tÚ ´l¦Q«Û©»cÚ±ŽìÙBG÷l¥ÞÝ›iÛÚù´rÆ "ðĽôܽ×Qó5—Óã7\FãÅzS¼:—Îs˜JRÏ¿ íKR¼Ÿó­î›ù>QH•"r v¨6Œþ[:otôßýíí|é Æò6ÓU Ô ÓWiºn/ç ·7XmJìån‡8¦%VÚ`²Ë]woƒ§®TΕ!5¶uZ(¤´ˆZ´qun?H[÷¡íŽÒ†MÝ4sÎÒþR¯JgNü)ý|éSôêŽ`>å3Ó¸îÙôÌ–éô/ó~I.A_]x' éBõÉß¿4ÿ—ô@×xúÚ¬»«*¤X±Ú´iCLH±ˆâ‡‹(\,š–.[$Ӳ勥7K§ æR[Û,1§ŒºRÙ”ÒŽtœŽž8%îÅ÷hÆœ%´låÆþRW·?FçL¼–~µúejÝþ=·uz,=¿uµˆùRÿ2ïv)œXD}]¤‹ÝI—Š4lñt™H—.¼ƒîë|ž¾þƹ„”~Ãe~Z‹ó& íc‘ÄÁx®§*mÚ¸^vîY,°€â!rÜé_¹j™L«V/—"‹oçýœ÷­·fJá•Ç3uË-·›ÂyÕ"·,*Ü¡}ÿúÌZzrë;ôé»èâ7wËôõÉ=töC‹ÅĽ2?—“"ê©•ôɇ—Óߌïi+ýÝÄn:ûµjœÜ-BvÓ·gìCýzé“÷-.,¤XO:±âÅv7‰Ex2Ó 1Ÿìœ\–ù®X±T&ÉVx®JR“ŸE¯>ñ =÷À´ck'­Ãù¶-Ÿ)„T;íÞ¸L¤•´gó*9GЇøu¯Y$‚N,¢-bçüÉBL=M¿‹6,›C÷þäûôÐ WSË×—%¤îzà ¯êÙ±‹vìëƒìÞK»÷ì£[î™[HCÛw 4^¢ÑçEÛÎÝlÿæ·Úç ña䱊öîèšôø¸âÄíì¨ßfgP–Ó"n“ønîçN ª×|t Ýsh²;W‰N.— ¤쥔ۮmtĬã8çœÔ†M >ûºYíм½^9ÅÑêl&ñ×ÇPL²®ÛÁ”ù}¶\m7q«Y9×QÚ^6ëØƒ)”T;c¶`?3xÚœT§>nÌÛéÚ ð¾Z6¤l;ÆÈ°?_þ¤ãå>7s8¯}ÝC®‰ç¤Ä¨a{ò>°Øç•lÛ«ÄË)ý¬‰lÁ2¶‚yÏɰéŒ{!vgÝ£i¶R†›4áyš>ù%ÚÙñ½wèyz´ùvºú²/Ò®¶óiß[Ð/ùÝÞô/´{îj÷½!µ[¤(mß¾ƒ† »ÌR7Üp£ò´ËÊ—[HÝý¯¡ºiî#töË—Ò×§üBŒry”njMC_¾–>ýÈ%t÷›cKRÜ‘5‡öý·¯¬—A&þèÊôûß|MzšXHIÕÄúøŸü¸£XH¤2rßÖÿü1KHùú|iCû–ŒOŽ%ôÏY…žªÙ´[g˵]x¸X ™BÊ*—rÜŠÈT’DÀ'¤šŸ|…f/YK ºvÊ!}˶ì¥n`‚EÔ{ï}@GŽž Û|6—ˆâ!~Ú÷¯“n£s^ú)Y÷ˆ˜ 5•Ælzƒž¢ÉL/mIß^|}Ex¢X@±xú¡N×,¹“®]z']'Òõ"ý|ÉzhÝúÖ乄£°DÏ©Ê1Gн">ž¦ˆÖ¶b]óÀ+è·Ç‹¤„Eì:¸¼Ý|Ú¶®›öL† |mNªS‰Z¯·3&¤=^Qçþ×YÚLÒõOØž÷Ü áË=·œÇí%VÞ±IÓ¾4[ãEˆ¼þ^ϘÇÖ“îwel×Ñutź3¤W3õ^ðϺO’®i³Õy õ/²½hkÆÐs/îàk‘Ä‘úX@…‰‹„î^.B&¾!çJíܹ-·ÒbÊõ`¥ ©OÝ<‹®ë|›>óÚ.úä$‘Æm£OŠÈ|Ÿ½£vìßz¤xŽ×!üx¸a˜:Åw‘:;W…‰÷q¾¢BŠ=K©ç?qÚ°¡SˆÎ¥RD±°âá}j )ŽÂ§EÑ¢¥ëè¿z”F?;‘fÏ ò.[KO‹¹S7ßÛBC@·Þ)—½ùµÍYìRº®ª)2Û¶m¤‡¦?GŸ}9]ñêÏhÚŽµa=²á!¦&Ê¡|ßé !¢ØÅên!œFŠôÈÊ;é1‘‰¿?×9Š.~å¶BBªhÔ>öª°bÁÄž‘Uñ?öLÍ]ùkú«O½F¿ýg­ô‰ÿ5îl^-·¯ùøï†íà¶lR‰Û¦Í¼]´Ï㑊 í‹y’”ØÈR;gÛs­Üz0WªfT›R,|Öwm—!ÍuêßçÌ[A¯½1—^ŸÎs×…½‹m>5Žn¼{´Fº—æÌ]@³ÛDÿÐ(gÖU9!¥B›ÃèöïßE“Ú§Ðß?|9ýÓ˜á4eÛô؆gi„/üå¿”ž(Qw¯º‹Ys=µN,Œ*Ò v×u w«µ†T°ž”}.S´¾”$¾õ¦¸m:Tº-\ Òl!˜ÄP.1„ÏL+„ˆb ‹¬´Äs}¦OŸ*„Ä!‚ùIyÓqžË$óëõ¦Äp:ñÓ ž¥’Î'?EHðŸ>ÒF—OÜB—Î?BŸ|ak ¤^ØLýòú‡[gÑ­cESá™Òåö:H¯´o çÚ:ižÓ¹t§û¸çmÜH¬ßp6î!Ì÷ìMR® Ó¿gÌxCÎâ•ÚYñ0Höî±×‰<*®=p`¯ôÔq0`ûA9OêÈ‘ÃR|uˆð°œxX‹Uöèñ9†áÍõzRî§Œ¨g§ï»6RbáÝ}›WÒá•Ú¸dºÎÇBŠEÔ”~Hãú=¹mý¼×iË’i´mÙ›´î­ tüèAzã¹ýBJ´ý˜NÆq“Ÿÿóe·„BêØ±ãÂVß=Q,¢ô?ÎW¶ŠE¶3çtÃߌaSW…QøœŽ©9T*K”ˆýW‰yJúÍò†ÑbxŸðNÉÀFY~Ë.ópþÄãF^¨Ä@ò͹g^”ö ÈŽ”í­2‡DyÐLoƒË·ÀÐ>‡©5d+Éë Ú¨‡OÆßâ;CM”v}Lï…ÛYÖÃùboð(kIx_}Ö5RórÜ{‚À í'«Sï Œ]ۤ뤇µšý„a`îp»p˜«/@JÆÐ>åòzf²Ä»#âïmš HBè³GWTYs#ÍáyÓÔ{.Q„¥ØtÚ½à;Þtí̾ò…LªˆËÜb´™¢S¶Q¼\\I³çÌ…ÚhfÛ[Vš-¶½6m*ÝÿØÓ^!µ¾S g+!i!ÅA"Xllܼ6™i‹ø-Òÿzaåã4ìÅ‹éo~]ðó‹è‘gŸ¦Ù"ÒŠëB¡"‹*¿Q„…æ²¼€¯¶'>9é0èî' )Þ¶‰ŽHøU5A´W„7ò2:÷‘aôüÚ;èžµwÉ!}?ÃùF¬¼›~øÒèÓw}—>}ï%É©ù{URÓ¦½NS§¾FÓ¦¾KSÅ6N@‚=O® âù>Ó§OsƒX$Ì- Â€% ©7®¦Ï Át㺓tn«Ö÷¬R/n¡O¾¼þZ“¸`ô2úÒí³èÚ–Eô1/ꊇçÑýon ñ›Ð›÷S{Ï~1ø­Ú}€Öï;@ ×ncC“=RIBêÍ7§ oÜ)¤oÔB¹Þ¯ËÅBjß¾=RDéa|<Ô…¯ËÅBŠË­ZµBŠ0þΕó—*¤žºãGZ3voXD»…§i]ûkR4½~ÝåRDM¿å*zåêïÊm,ž:EÚÐþ*­|ãE:~dMxìöÊ )á:~œÃ¾¿#&2¾/=Q濲…”ꌙ¡„åÜ:“BÙ~Ójuä ¡’öÜé¬%ÎÝÒCy8„# ©¶Y -³}‘À²'Åá“NºœÇ¡Ã+« z‹/ì²[o8×+1»G¨Ê6d‡lçºõ¹†×ÈÞÐÞî9Æ:´ÆP(k®—3GÎ7¼)Uæp*wØg<¸‡¯–í9si¬°×úº$‰uk{4Ì:¦Åܘö é:¹÷‘/Ÿ{<ÐLÙï{Á‘rn¾¹H¾Ž~Ò9÷l—O¸çd‰)ŸMZUž3nÄÍÌsRm7…—ûòȘ«fÝã‰÷IÂ˦ ©_#TÄ:Q3…XûR+=ß:>–^|eµ<û<ÝrßÃ^!ÕÙÙ%úÅ“)¤XD±Wi£™„bAôgWœC_ñ-:ë§Ò½-£iê³ÄßðU⥧Z"qÙ0©ò]",4—-*¤XÜpúÝ‹7Kï‡9ïÙsZ m¼—þßßyžînÞ(ÅÕi1ðOÌV‘ó¨&­yŸ~ó«Ô:m®Rëÿð#BœŠóÑIÔ×å¤ ]ÝbWHñßÝÄð繂JÃóØ+e„QO6¡‚QÔ¡éïSÖBŠEÔúõÝRL±ʓƾ<5S»„gª}±˜ ˉº:»zª/¤XL?~ˆV,_D>q-sÿ÷è¶Å×Ó“n …7ê±uwÐ[/k2ñÚLvZ"»•Ilçdz™*í‘êéÙ$‚Ml¢í=›c©GlãÄë™BЇ£M›6Y–˜.æm’ÿ¼^(+ D‰BjÿþÝôoŠùh[èÑ®ôÉ1Ä<)!¤ÆBêï§m§ ÞÚCOÙL¿xs+\u€žY·Ÿ&lØGÓ·ì£9båæ…Ý{iåî}´`}­Föî$ íKR›Äì•â@,¤tôB^¤—דš9s†Qìiâá‘,”XL±ÐbÆÃøÖ­[#E{¥f̘&òó‚¼¥y¤Fßø=)¤8ì9‹©î³iͬW¤hšpÍ÷èÅ~ƒ^¼âë´åýÏÈm3^¦ÕB@uL‘VLKGî¢çîÿYE…T07J„p=Ì‹*UH•ºÌNAê|˜2ç”t>urL—{Ÿ°Ê²Oþh§u̬60{©*O7HÈcÓ'÷@™¼:}–î°™ÚçÎ¥3gÒ›³fÅÒ¬Ù³èÕɯÓ}£íµ¤tøóÎuDЫâÉR,¢6ldqa$!†Xýí•_¤Ÿ?8‚&O™A ­{tTâßaRå7lŽEÙ¢BŠÅ §ßþÑ)’Ø/Åi×;D_»fýÞ'šé7ÿç3ô›µ€~çœ6ú÷¶Œþà¦Ð÷îxYä™Ä¿Ž?üáQ`¯‚J¢¾ nÚ°MìïNRU騳¨’Á)ð¯Ö„BŠ…ÏúmRLuæLœ÷™—¦ÐˆÇÐ=“×üœ;o™,¿NÔÕ¹¡»o„”=b˜Ú†õtç« ïÓ%tý¬ŸÑ¤Í7ÐÄ®›„ºÂެ§‚Cèaxq¥‡òUnhŸVSyØø·=,¤´7ŠÅ‹(‚¶{wOÉ"J†(/QH±ÐX¸l×4…[s„n].—?·YÌ—!Î_í¦ÏLÝN_ îþhÞº}É~µj?BêÕ„ÚO³YHmßGóEÔ‘i ;iÿ}Žˆ †FÉŒæ}?zT¸ì•˜âh{:ú‹§9sÄ„[!®Â jŽ™žo6þ\9·Š…T ¢ØsµLx´ö”,¤šöuzÿ=&U ÕÛ¶rmZ0…V ‘Ä¢é! Þ¼ý§4{ÄÏèÙï9RÓž§¯?C+…ˆZ6i Ù·ž¼ãòŠ )ö6åIy†ö•Ô‘(«£é·ñ}È?Ô¥¤öW°c3pŽßOœ3l%ÞÉïvfÌ­ÐöRyž¦çs0-è;îåÛÅ›ì"É<'-¤Ö­Ý _O¦bµ¡‹Å…‘„bA4éÕ©b2ýB)´têbŽJæö¨üVY¶¨bqÃéSßZ/ŠbSÖ?^¸Wþã Tb]©ÍŸø=1@Ô£“ªS×ÍŸë7ˆö 1åÚ—u¼"ûw/ŠõoFè+R òV€R,¢ÖuŠèÜü™3q^NO¿ø:]Ú4‚žx¦5,»VÔµNØYÕ‡ö…"H)S»wwÓ‹³ÇÓgï½”.7œÚvÜJW !µaÃ:9t‡ú­X±D [ÛY’ª)8Gޤ1¯/¤ó~þ\ÝK·­EKÄçá=[é‘/ªˆâ?Œ,ò¦Ð9@!a°Ø@um@ ©µkÖ‹(ÂÅ“%¤„€Zß%Ä…79Käí2’-À‚:Öwm‘ŸÅ…Tp¬³¦žWrÚò{ B(‰óÑIž›:׋¹ÏBLU[HU]à! …‹¨µëÄtþÌ™8¯N¯M›Ck×Ge׈ºøwŸ )VŒaÒœ)b½£Ëéëϧ+Æ]>`7 v3{y&}púC±öÍÔÀÿ㼑¿{‡Ü™CâŒõ›Ü¼„bþÒ¹4äáÒ§›FÓóÕiF´†Þéh}¾cšQó¬u¤ÔP=ïÚRì5 "ûùŽù†Ê7cÆTê{£ðÞpP eÏWgVÔ>#BŸvÔ>=ä/ŠàgFí;Áó±d¹`ˆ‹©¹ —Ê9Sçß4‹®·Žî^rîX{Œî_s”š×¦§»Ó“ËvÐýÓ:éú'çÑ“¯/‘w6Ëxñ Ýó íKî'Ö]umÚÔ)ç9±çŽ…’™´Š?9±È²E‹¥hnÔqž³%Å’Ø®SÆþ{¯þ<‹þ¾ûŽˆ'R´ï97êÑoœO}ãýµ°öé|…ºÿš/ !%÷3g^Oøó/ pE Bê%r¶ ÑÉ…7yDQ(’Œ2 ÷ö¹ÒA(V­ZJC[~> „ÔìÙ3Œ ‘ò/üÛ·BJz„Ä0¿µbAÝWEüŸŒz‹þéÆéô¹ŸOÃþ¦Ñ?ÝðýëÍÓéŠߢ§&/¥k:iÇö­†ˆªŒbA¦ÅTÍpZf †óEa×ËR÷]ýTNºW”çTTHñÍ„°Øl6P6Àb*QBŠÅT¤z¸8Çþ}Þ$‰(VUõH™ž§ ë× !µÇ *Q{BJ¯7u@DËÛ´q½\Tx™Xk…X x¥+«W/ÖÐÎÛ¤èâüÙÁ$Šy¤t},¦8’aw÷f™¶IoÓŸ¦ˆbAX®Z!Î[¦FÒÛäv!$erò¨ßËÅ'§"BJ{Ëì2ÑšSÑv!4…‡ÍJb]*^[+O:"òEI,/À^2£¬¹ß[Ÿ7T$.¥x–>LÆ1{ÅvNÞýÆvÏÌÛ{¤W”U©W|ê¤ê4ËXß{yݱฅ“.+ËGÇ?,~N‡ENfY½-Çç!‘‡“Y^o3?å1TòíÏÜvHÇMêØ\ö JEê9(êKO‡Ä~NQ¾â·N¾²Šý*eE¿Ã¤ê²ê9(êW逸”É8ŽYg¸_瓟Ñ1}û÷‹ýœÌ|ûE¹0ûE:`&#?ïË—D‚´O”á”TNïß'ÖûËŸD½û9™eô6w{gï¾(ér{y»Já6#Ÿ¹?Ì'öïSɬ³œï{D}Þ´Wli¯‘ÒòqÞhÿAñ=H»Eý:Éõ[mß->S{y8™ù|Û¬ý¢ Â{#Ë¥$ÓSäËg{ž´W©:)í½Ê#¤BO—ònõ¥GŠå ’ðb餼_;Äg˜Ä¾*…^2£ÌQ‡Nºýæ¶Ê}Ú±Cx>d2Žm~ß.öqÊ<öNQ§JºLÒgÞ|Yõdï?$Ú®’òYeô6þTù,Ï‘á1Òå|!öDédGÊwLs[Ÿ )ïp=s‘]sˆ 1Œ/ da,´«óVahŸÁφW+)sñÞ¤E|µÐ ÷gFå+MH¹‘þÌùMIaÔõör…Tx,k8 1¿*œWe 4òšb(ïÐ>)[\AHEbÈERØ‚‚Ê+® ¤!Up`(¢ ¤ò ¤¼ù²…R ò’„TÙs¤¬¹P>q!¥Â¢ç›#Å"BJ #”B*îå‚GJz¦ {£Ø TŠW )†ù¼Qœ©¼"ŠóAHAHEs© ̧‚ ¼UðHUmŽ¡TÊÐ>#úŸð çd !Œ×i糆ö©rö¶¨þcžðçQ„?Žö¤£ŒC%]—=W)ŠÐm7¢ö)ïÖQ9'I%OT?«=bnÑ1•¼ó•ô\)k~”Q¿wÞ”¹_OŸ_å›Åó¤t²æ; ‘s„“5˘C¥æAõŠýa çM©²²g•ü¾¿ðü'wΔ!δÇIέR©$/T9s©ÜùTª.{>Ô!áéâ ÈÛ2ç1…y‰¼:©rbîС0å›7¥çOÉOk^”!v2çK¥Í§Šê±æ6ñP?™¢yUÙsœô\&5WÊš ÅóšâóÌ:Cï‘ÊÌ ê²…ó± qzÔ¼(=?Êù´†Ê‰ãowR8¯J«ÓÃ粆ٙû!wª¼åÑÒÇ ëæüžú­9NÁ~sŽ“]&~LsnS˜×¨Óšï¤Ÿq̬9Rf›¼ó­Ôqì} P%}ŽÆü­ÒæH%)3œ¸þ~’E’L‘@²òyŠ7<ºG%ÕéJ‘XIwe %Ñny2©ã[BÉÈknÏû=iÍ)w{²8 ÎÉ:žg>”µ0p¸ß=i›¹æS0Éœ¥=Qö¼(SHß­ðæ¾!y†`’ÂL¥Är²ŽHHÙßõܨüûÃyS–Š{—üÇñ‹¯Hõd°ƒIÄ…”_ÅK˜‚ÐZ*o†è1…’7…WHùÄ•"!˜„0"ü®¼ZEPX'‚cWYCûz… ’É¨× Á(G˜EB)B>Q‰$S0yÄ“Vj»p"Y¼/Z¶h*"”‚¼f@ ";(E‚ÈrHÈß*ð„ÀÂ,ÂνÃçprVô[‹¢H…A$Œ2¶P †äYÁ(¼b(º•àÀAòT‚HŠ"•/ø ‚QøÄUþàP"#Ø„)€œ2E˜‹ï*íŸa ‚Uè@üi£0ƒT¨ï>Q•%´,¡¤ƒMu{…N‚hÒyMñSòZ˜ìâI'KÔøXø„–/€E‚ Ó'’D‘Þn %%ºtôX(tÝÜïËk†L7ök±d ©´mÉâÊ'´âÁ$ì¡e ð5[hEåuP 3X…>–Uƨ3,cŠ/S$yD™×ãä) &[êí¦¸ÊZJ(ÙùŠ1•×#jì`ñ æyX¢F  "é3)…>f¹p›!½ÃôL¨’¯ž¤!~Ri(Ë{d‰¢¤ ާ(»L$„N N‘Èó{ÍüÇYO*Á£d‹žxÔ?[ô‚Ä:†H ñòxE“)¦ ‘ã 6¡óZbÈ#zÌuœŒ¼i¢É–w*D‘¨±÷ëHÙâ,Zg*¬×hS´Þ”/À{§T2=J†èòEØ =[¦Ð1¢ï…ž'£N+‚ŸöŒ™û=¢)3ê^¦ÐJŠÆgFæs¼Cf?-€LT†(Š„Täq²ƒMÛ½‘ø’¢ñyD¯üaá…Ò)ËûäÛï-cz‡¼)-˜DS’ˉ/ðFÙõ°Ø “Ï+åó4ñ^ie–1D—C–xóì/$šŒh{aÔ=3ªŸMWJ¥P4%ˆ!]Þ‰² )]Ïܯŕ%°"!z±ÜÈ|ê·@¶Š‹&Ëc¥„–í…2ËBÎO,¦ÂF÷‹„˜W@™bËM¾~uÔ=Ÿ¸JŠÊŠ+;`(|t]IQÿ2„Vâð>IIž&ßü*Ÿw*c[¦Ðò”÷Fí3EW<'¯hŠök”å/ï~;€EP¿W\É`ñ€¾!w–Ç)iMªPl)/V’÷(s)-ªâBÉŠÊgDè‹–áÙòy¼Ì6eˆ"0Ù •]O{¬²ò™ûU™DïUÜ#åúg”/Í‹•OhY©L¡¬ó”´Ö“)º¼BIxÌz8™ÑýL–oý(O^$@£mžõ¦L¡•)¤žzêYBØl6€ À`°Øl6Ù@¦*ºº0òƒ€€€€€@-è=¼›ÖoÞHÍŽ¡©Sߢ)S8ÍŽ¥Éj[ø9y6Ýw‹, !UËWm¨8R£Kë×o.\7—á²R…Ñ¡€€€€€À@& …T©ç!U*9”°L!õÁéÓT$ñICH ØK†ƒ€€€€”JÀR'OžK5åOR¥RG9MÀR‡k'Ú霿ÿûØ6Bj@_z4@@@@@ T¦Ú)Ö2‹(Ü}üBªTê(     0  ˜Bjã¦]¤“)¢ôws?¯˜êé!BØl6€ À`°Øl Vl Kå™BªcMq2E”ï·Î! € À`°Øl6”6PDH-_±ÙQü['S\émR¸iåMS+oAм‘ƒ À`°Øl6Ð6PDH-[¾‰8±hÒßÍOw;„„„l6€ À`°Øl`PÚ@)BÊ'¢|Û ¤pÓ Ê›o~úïÍ؃=l66ðÂø× ²lÀ½Ÿ·mû€6n8F:ÑúuËNkVí ¢É<.·£k}¯l·Ím/„ÄÄl6€ À`°ŠÚw ñÒ°¸Â„K÷–}tݨ‰ô‰¯ÝA g5õkú/ÞLÃ~õ<õl;H[6Ÿ‚›¬ñ& × × 6€ À`× ¤·ˆj~p MúM™Âiv,M6¶éï“'Ϧûîo¡)o¼%áø„{XD ½i,ÍYµµß!vvï“mùŨIðHáüð}zpäÓôÆm±ÄÃ"}Ûy—aVå=RóÓc wÑ]2¡7æ}8¦7Îá²Sh~)“K ܰj8§¥´ã”Ò6³Œnç°M¹'–ÞÞM4ޝÇ9‹im¹íÎS¾„s+ô¬„ú“Ù)6¡Í–c»Emù ]÷<¶‡<¹Ÿ'`ûoðÙ@ k×c©‘î.Ü©´}tÓÝçpÛšh\ <§*#¤Ú¨©¡‘Zºk_\ÔS µ:yòMÕR³éçž¼à‰mófiÔÞ½¥ ©R‡BêaÕR‚ý0/SH©ÔüaYB.x¨sw·¿ãÁõ’Ï!Ǹ°ÈhoꃹBªÂç–ƒWY"4W¥¼(x.5ðuðu¦p pMaõiJH k3þ®«mýõÒ4|Æ{„Ôü:ÇjkßÙme„T=É“s®ZH:tŒŠ¦"Bê]1 (3m}‚Î"ê|!œ¢¼ÛèÑ ÄK…kfg—ÏqŒJ)®ãÔ©÷B^û÷÷§;ÐæÍ{dZ×ÙC+E€å+6Ó 5G¬:s¤ªÝ‰ÎÙñÌRÁÃuØ8σ«/Ρð1RڛɤL!U´­Eóg¶ß¹F…ëOcçg³öî1Ò«:ÎgEÛ‹üð’À`°>°Ÿ"’#„ç¤ÿ½RÆß2Qü–Bjà(”ÒRŸ:çïÿÞ+ªÜíE„Ô;§>¤¬4ýê:ïÑmž|³éê† hôÆì:²ŽQ !µOˆ(N;wt„Ô–~Ra§w±² 7ÎRuX]”þ­†\YÃÓœaY†)SH±ï½.vçx<Ä0<‡)Á9k›šç¶;ê ¿!½cAzìîÃÁG íMé¸'¶×¬ËdlzSt[ÆÃ.Ýc%qöð‚”ëâ½æ®w'íºæ­?`_»žBÊwý³Èý¦} Û1½¤IuZCS¯já{É÷v3éžÉº}÷¦´>ߤƒ/®;l |È+¤œ!€¦˜ N‹ŠçaÚ»¤†ZÞ®¤}¦GÊÉöERê ÛÕ¤†/6Ò9îPAÙOÐ/ˆU] ž¸d”®×ÔÅ©‘Çîµ9¿¥¨p†öy¸\S›©<8o4ìÒÞ7@Êi¦R;w•ƒÄbI'½-i{!uòí)=ͦ«„XzdCV>Þ¿9Ï–{Þ´>³þ Þª©}GhŸH;v°Gj·LÒ#µº?½5Vð²:È×M-n˜[U-]¦…”¶ÛS¼Ýý­óRG¦Ô´î Òp MÉÊ'öO¹RØÇ•³ÂúÜßiÇ©†²†ömBJ¤`h_¿z¤´çÆíäçRw¹ó•TG4ôð8Ã&Ò…TŽùQìAÒÇÔ^ÓÉ)àÁj“ë]HU°½YBÄÝŸ%”’„e’°h}%·GyàÊagy"Ï¡(%×õמ1%Fó2ðIðÆê̺—üC"}÷ŒR±û Ñú`8:Ù}ÓÉçÁÏ9!Ø„é•Q^s~t `”@É8ûµ¸‰ÙËRykž‹U¦Ø\°L”pBÿÜ€æo÷»ò^Å‚RTK4ùêÕBªcM™ÉOú»›§ˆÚ¹û(¥¦Å‰á«?¦V#ßâû†Á`†Ð½‹¹Ž)tYƒþ®êô”M:„T8DL¿íÏÚ§‡¿ex"Ò…TÆ|£ÂÁš—è‘2:Ê^!F‹K‹RX°½Ž'/b–ѳƒmyÿxäÎï¶Ç#‚}Çˬ¿ì†öYßœ¬’Ï!Yh™CƒsOðŒ%y¬b÷’ÓqL½g\ñ†ùaƒ¿Ó ak<ØlÀöHùDM¸-)²ŸGÐXžµßb~ÏS4L)˜‹OH%ÖëSeÔéiWš}W^HiÑ·(RŽ·ª/UEK )Šà&SLùöRÛºQzz~Ðp.ýj¾/Ÿ±oþƒtvÃèE«¾´²v}R™BÊxÐ«Ž |kžp UH¥Í7ò ÛKéHGoðKôH±¸Éð®õmo½x¤*Á®L!ezpüÇ(¼}ÖþeßÑ|Á¬!–¥y¤2½Næ}žÕØ"fC©– s¤jIH%‰(Þ^i!µQ "WL±pâûD ©½Ä‚«áÒ ¡àr§ ¶ª©}"ü¹H;9ØD8GjûÀœ#•Îß’+j_¾õ˜tÇRΕ§vçâ莯\ìVDe³Îy†JQ—£½ÞhnæPÁ,¡’%ψŒÍª¨}ÉÞÉìÊRé¶™7jŸ½ sdwAtÈ7dö¤á€Y)ßšlˆÚ7x;‘¸¶õniQûÌPãÎ\*ï¼#3jŸ»ˆn¹Qû‚ë¤EWäIʵ½[öuÖõØëb–l ‘wŽà mèdGæSbJíGÔ¾êI«,T¥„Ôú {)_š@ßs†Ò~ïÙeô˳>G¿œ¥ëàßFÔ¾³î§Y9ëüBªŸ†dEíTp3†9ªsí'{ªú²É|*q­QG½wèqþöèçõJáfyÁr>{± oõ„L×ÜWBjMÇ.ª…!•ó¦/úp©«N+„T ÎaH9ïÒ‡NWÑûùa3°Ø@Ul`À )í]s½féö!Õßr§zÇï+!µby7ÕBÒBjß¾¸ÍgQî=¼›4¯S§Þ -æa}ý?´OFTsaíƒ8_¨ŸŽ_%a˜úBª…OÖ6‹R?Üýa8fmÚ#® ® l Û’ÒÁ,ÂExó÷± ¤²ºØw? ƒ­Û¶‹ùF» '.§…ÛˆÛ÷ìZ„~ç‹7SÛ¢¥´pÁ†šHÓfÏ–m:x°rBjßþ^â$ä5æH­ZÕ òâAý #0‚ À`°Øl ŸlBjà ¥¬–:|’î½ïqzjÌ+…Ó=÷>NSÞxKÂ'¤6u Üù<}á'Óë³çõ»š³p5^ù ]qÏ‹t䄨ýô@­Êðœ ì6€ À`5iRYrû}BjëÖ÷i×Îtã¯Ò'¾v5œ%¢5öcbOÔî}‰Ž{›Ž‡ªÉ‡ DFþ¡`V°Øl6¨}àN2dÙ€{/oßN´g‰¹DDG’/'N”žÞ}—¨h2wò$ÑÛoñçîÝRRxs€ À`°Øl6€ ”mY~Ť`˜#ã+ÛøðF±öß(âáÁ`°Øl6ðÛ@ÙBjgl¢³s;­Zµ…–¯ØL+Vn /6ìf÷òLúàô‡Äßøüƒ7ò÷¬¸x¸a°Øl6€ À`°Z², “é‘‚‚A×’A£-°GØl6€ À`°¾°) ÑÃ=Øl6€ À`°Øl   !•ÕHìZ"<´ïˆX÷íä¡}›÷È´NÌ‘Z¹º òÖ´@@@@@²@HeÂ~p@HÁ$@@@@@@  ©‚À@@@@@|B*‰ÊºÎZ¹ s¤`5     uN É#õÀ#cÉ— ¤êÜ`pú     DðHÁ @@@@@@  x¤ Cv(î‘ÚBËWl¦+·Hx£ÄÀaÃn¦q/ϤN(¿7ðÿøoäïø     ƒ‰@q„Ô`ºþ8À#U4¨oðHÕ÷õÇÙƒ€€€€”@ qAÞ}GhŸH;v Í›wË$בZu¤JÀŒ"     ƒ‰„Ô`ºš8>!!Õ'˜qÁDBj0]Mœ €€€€€@ŸHRû÷÷§;Å©-bŽ”HrŽÔ*„?ï“ ƒƒ€€€€€Ô.©Ú½6h€€€€€@€ªÑ ƒf€€€€Ô.©Ú½6h€€€€€@€ªÑ ƒf€€€€Ô.D!µO›i'/È›Ø^`û¨¥q54¨Ô8ƒºSy9ù›Ö¨Ük¨I×Ñ0‚ZÒ+©Ý+RJËÚžŽøi!—ì Ûš’Ùw·Œ ¦¶ì:j%·7¿-Å[Æ¢ð9vÏ Æ~¶ÉŠ_?ËÖž¦`· ßý^gãYVoÏ¡Â7 €€€ÀÀ&PBŠÈêèdu>¬Îé0£“_ŸBÊfuâ[öå´Ìˆ›UÆà\?B*EN’±lý)¤ªuýêNHyl"· îÇü÷b©††r   ýA &„9â(­ãa óx= ©$ï\Sòv¸M–¦X-Po?e-ËûPiñÑoBªŠ×¯Þ„”Ï& )SéeD?ݺ8,€€ 8µ!¤(ïð¾ ‡w‰Ò\~‡9I¢t`uËR~e „Tð*W´â6‘CL†CDKj[¹3GM   Õ$$¤ö‰Åx9ípæH­Zµ•–¯ØL+Vn‘ÍõÈX6ìf÷òLúàô‡ò{ÿðFþžç_®á}‰ÃúøyÞkg‹g.MÚ~sŸfèñøÞò§ušís‹suYÈ«cWµ%Æ(é:{…TÜ3½ì"Ïõ³¬®@Ý1 Áu/Í1wªèqÍF,[U›P âϸä—C¹îŸ2Î7*ê´ÁfhÚ‰iEÚ–4¬ØgÏñã¹÷m3nÝå ôÝÉõÅ™e?‡óü…CÁH f„Tžá}ÉÃú²…TÒ}˜ ©3ÑàmÈúŸ*ÂR,(µì|©ùs ¸øü<q«³Õhw¢Kuˆ*&¤ž¦&3‰3©ßí%vš!ïì%̲„Ô#x„T ø«ÓR-ž  ê|’:vµ Ç–‹ÖR‚GʵIˆ%WÝ;¥”­ªM$ )¡Áuó¾p„gcÒýSÆùÊ¢^±©lVØA.!•Ò¶ÌëbkMMI÷?;’„L sÍÒ8×$“YÆ}:;8'lµ#¤Ü? ž¨sÖïØþT®9 †‡"E¤ÙÉã‹_”ÌN‰gxPE„TxžÑ¹Ra„<·•RÞhh¦ˆIy“ Jûí·W´xXÄ…C\<Ùçïxb2;rÉçÁÇ®¸]÷L)u—Í£ÄsÒwL©mö ©JÙD’÷-ŸG*ñþ)‹U²‰ÏyúÂIÛÇK+µ›1Óñ¤ç¾v©÷{OgøxõŒ>ðÕÞy™ÕYDØì>r€€@]¨!!åþ!vÿhf cK1ió¯œ?¶Þ?ªæNß[£¾!iYæåv¶o’Û±ßòg1I?xòœ ôzc-_¨õŠ )»ó’ÆÄ×iÎã%Ldáv[R=”1!•vN­švQNÝ•äQÄÖËhsumÂß¹O{áRüþ‰AN}.¸×Ⱥ?Ó‡–æi[’7Ëõ‚™Ï*·½ÑËŒôgj®!ߞǜ{Q[Üãéëgrq®©Ã³œa†Y°@@``ÈR;£u¤:;·ÓªU[ª3GJbK›”9Á;AHeMøÏñv5üÃiF𠇧D`w~ASH®È5¤Ø(GHõ¦z÷âCÖrun*(¤Ò‡ÌÙís;Ímæ[òĵµRXd tsÍi,2O«Þº8_¨?5'¤ìá}ªž§ó–þ]žg<ò€€ Z™Bj§R[vË$=R}!¤\FÀJ}˜2*WÀ‰Èb‹8z&(—0ì#÷+£ë%Zmêü¨¸rùg¾µ–ŽGÁJŽnÖBÊ™Lv”²Xä²]Žâ¾µ kÊ\œ »ðÝ?%×]²Î©tÉöš0 ®MTIH•Å*;p‚y/R‰‘(ÓêWGH¥ÍÍùüIXk.-cÑ¡‚ƒ¶÷€ -¤¶nÛN«;º %.3ꑱ4lØÍ4îå™ôÁéå÷þÿàü½”¾Ž^ú°ô€YãÞóMèO_ç)ï™çèøxEdi“ªÓçGmöv´Uò ©ŒIþ"|¼*ÙÐÐBÊ}ëìqÛ /ó¾)QÜx„x™B*O$±†„— i×/8µmÎäa]CWè%yKzœî½ï zjÌxOzÅ¿ýéñtó“RˆUEH¹C-bôb—'Gd?oç"ërÚ\ ç¼tËñ½EÍ;œ1ïü¯¬ùQºõžoA!%kJJ”å°Rs¤<»C7ƒZ•Hò$ú:Í®W&Wø‚Cû,#*j B)‡÷v0ͶXBj_tçêl–rNº!ÅÊf‰ƒŠØD›>«=ñªŠoT>~Þ§r‚MµûxȪõ‹R ¼Â/eä@’P,2÷µ€9 +€€À&°{O/ê=A‡Žœ¤^‘Ž}›Ž{‡Nœ8E'ß~NzÞ}÷zï½ ½ÿþ‡ôÁ¿—¯ŽÀPÑôAD Lïà "S4JódšÓlj€€€@…@HU$ª¤ ¤é…ħ•l"æMJóxbF85¨© @Dƒ˜„Ô ¾¸ƒôÔr-,«†´æ†9H9á´@@@ LReDñANBj_àÁyzYÁu2ç N,8+¨(©ŠâDeƒŽ„Ô »¤õsB)!ÀKX¦¡~¸áLA@@ ©|œ @@@@@BR0(HBª 0d)Ø€€€€€$!U²ƒ€€€€€„l@@@@@ €* ÙA@@@@@B 6      @H†ì      !‚ ¤ Cv€‚ €€€€€€@AR!;€€€€€@HÁ@@@@@@  ©‚À@@@@@ ¤`      P„TA`È     R°(HBª 0d)Ø€€€€€$!U²ƒ€€€€€„l "zè!¢•Éž ÿ@@@@@`€ä¸/NOЍMŠCM‰?ñ@@@@7!¤þôÆrÓà¾,ëìn½õÖ@Hi1¯ÔÀº€h-€€€€@a5/¤X@ýõ“ tÖ›AÒßy;þÕáÃÕ>Ú'ÄÔ›7'_›9sæÔF£Ñ (ƒ@Í )S ñ÷Ô&í•J=ßîÅ4¦á.ºËJchqw”Š•m˜B›ÜrÞ¶ÝESÚŒŒIe‹¶Aç¯t}žv\|ñÅ$‡÷±'ŠÅ”ñ÷qÚ¶m›LÏ<ó|¼†.wùæå÷¼óæ«ôqóÖ‡|    5I f…‹¦ÓÇ¥ˆâÏÜ(_Ǹm ÝÕ¸˜÷Õ%HRŽÀR"ÂS•lg¥…BBÛFŒA<ÄOz§ŒZ@e ©¸ð<Ü2¦o¯[%¸÷ïJ4u€€€€”N &…”¾ÇЇóåQÌÁÛ‘ÝDSLï†ã E ooœBSÙ£¥;ö\V{¸ŒÎ¾Ì»˜7EÞ¯ ž„ü‰m;Xè5)ÿ•Ùþ"í‘6`›½p¾¶¤OtîSÄyÙ\üB”=Q7‹kÄ×êÝwß•^'þ}ÎEÁð>í‘ÒŸÉ©$ž¹ÝÓvu"‘|˜7Šso³½‚R”…×ÑðPÆì¥èõvn¾L!•bæõk¶ÅçÑ]úÍ’    Õ#P“BŠÅSI"*I¬X)îÈ&t¤cÞ!î”ßEcZT7ݬGåMÞ—G¨ ëŠ'-⊴GøÛ¸­¡øÑmu:ö›„@J;³¼™/üîØâù矽d¥“R•óH%] %œ´àÐÂ*‰)·?v£k•ÅÇËÎd’!¤ëw®_ ü ¤ª÷èCÍ    PšR:¸D!oT(¤ŠÌ‘2¼Un'8Ö)6:íiy‹ í“×/¡ å´GÛ…U‡ã™“ÇUuŸgF ‡ÜԔܡ¿P ÁÔâé D§?;»¨Gʽfâ·93åZ°èÐ'ü;ï„s(•Oákì\çðº§ð‡*ïé†Ò    PE5)¤Ì(}¥ )7ÈÈžh¨—Êï. ¢¬’B*Õ#å 'L ¤â¦N8Í3#½ Iy½Oˆ­”ùe,¤X<ñ''öD-DÔÇ>ö1Ù€’ïkþ8€€€#!UŒrƒ€€€€€AHÁ@@@@@@  ©‚À@@@@@ ¤`      P„TA`È     R°(HBª 0dšR½½½„°Øl6€ À`°Ø@­Ø@–T¬ !•ÕHìZ"!UKWm ¤ÄeB#A@@@@j‰„T-] ´@@@@@`@€— ¨%Rµt5ÐA &„T­„8èí¸£¡85ˆ4ÐÏíGèSØl6€ À`°þ´,5!5ˆÖ°b%T„Ô º¦ýùðÀ±ñÇ 6€ À`°z¶©:êT—+¤†NŸùÌg¤ãOþ]Ï7Î<`°Øl6€ Ô¯ @HAHåC×]wMÙCôî»ïÒÁƒåwN¼ú}€àÚãÚÃ`°Øl6P¯6!UBJÏ2=RìUÊ;WŠ=OZD±Á°˜ÒBêÁMÏTØP½> qÞèÀ`°Øl6dRuÒ Ö"ŠçH'õÐC­NôÔpš3g]|ñÅò“½Q,¤x˜2xÈÀ`°Øl6€ Ô“ @HÕ‰b£.eŽ”Q›v2E¤å°>=´=S,¤òz¶êéÆÂ¹â l6€ À`°ÁmCHuŒ¤!j(š’|§É¥ !YgŽòyóhG÷È'鮆»Tz’Þê0Œ¬ã-z2i_cÜpà äKÌ-iŸïf¿õÖ[!¥Å”ðJ±Gê™gž—¶Ôà~@à®/l6€ À`°¿ !=#‡PÑÔQ@`ô»!h¡4|•×Mo aA5VÉsXEXDÉ}½´j¸ø>ä-ê.x~,”.\X(qŸg‰çGñ>9´Oˆ©7ons¤øÏ“b «~çZÚ‹? °Øl6€ À`åØÀ€R½1oÑdz®”ðâ<–Øê ‘C†ÐÈɶGJв°¬Ø¯½DyŽÁxuœ‘à œ†OÎ2N%¤´Xš™'¯2„U¯KoTÃBð‰ÿÒ„³1bñ?w)YgÿYm|X°Þ*T¸Ùk÷fǵÁµ À`°Øl6P9@B*òô„ž#ÓÓóEŠ—?áw+?{² /”)XbùÌ!†F9÷ø9çVÉá{jhŸž;e{¤ª'¤ôð¿$!ŧ›ßl ÓÇåp¾mÛ¶Éßç\ ¦óÏ?_ (ò1U¹9°„ À`°Øl6PÛ60€„”!`Øcä o@ %ŽÂá}†wÊ+|<ÃòÒ°Øl6€ ÀÊ·)¤Dç=˜Ód•T1/J{±Òò'Š'ö@UÖ#Õ›)¤ŠÏ‘bq”÷æHRÞ‰§+žl þììø>WLaÎTù7gÞkˆ|` € À`°Øl ïm`À ©^é ž™Ð#ãÌ‘r„’&Îo2÷›s¢tà-˜œzÒçHåvJ8=9²;ŠÌWÁ¨}y=Qn>_  R,žø“{¢Î"êcû˜W¨i1õä>‡Yß?ÌÀÌa°Øl6èKÀBÊà¡×ŒÚçDÍ“‚(9Ÿ5\.%_¯uŒá”âñ²×‘Ò¡Ï•áW`){­-ÏÜ2Ïš\>£ãà¿%„“N, 8ýÆoüFªÇ‹‡öñ°À¾4d NØl6€ À`°¾´!¤rD«ëKh8nRØl6€ À`°Ø@}Û„D¼†WŽÝ\ì˜QÂyZõE,‡O¶]á²Mádѱìs Žï–õ_;Ì|r™þtÉÛËX ZKÖ.ÿòÛÊvT ¾zâög^oµwGùç4„€ÚKªW¯ aÛR$/çøY÷€Y7ÛWÒ3«œ6T¹,³KüûQùs*vOøžÏžçbì*aÿÕy&ÔÒ´¥Ê÷VÙý1´6Zû60p„ÿ¡ë ·:¿°êÀ$uö aUãY48üž÷_V†ØLí BÅa*ET¸;¾vÎö¦]gm/[pUæ<“D\LH‰vòºY–¸rjŽýQHbS‹7~¼Cl±É<×RΩRÐìzŠuNK9§ŒyoùÿvŸSÕîPTIHi±šO g_§ÂÏãjsSõ[¢Ø:f¥Ï©H}î¢óž>¡VáYT•gBî½>²‰ZµU´ 6¨-8BŠê,¤¬7‡Á³áÆ[oë-µ•Wü¡áßá›ã„7§9c-àkG–)=1/Lì“ùG•Û'Þp'zg”á¤Õ‘µÏêÜEÞ K„¦uÈä¹MVnßíUäG&x%â^!£3u.îµõ½ OëXç¢Þ¼ZÛz)³sž)TÝÎs~Ž$Ú®hב“›bûpÞž[å;𾎖²¿É¼Xµª[•O´o^:å>ŠÊ‰ûKÖ‰ð"÷LZ=~/²#ö]¡#Ú<™½¡§Ö|«o—ÍÃ3Õ6¼÷#dÝÂ-[5=‡îKŒ„}Ö1m/½í¹+à1’ ù™›æU4ÚÃÏçØbæq¯°ÅθN¹Û)Ï5ÉÛl¶‡ŸÃæùfqõ½0 î‘áÃMÛ žÅIçáÚg²-§, o,ün=ŸŸ‹Î‹-ùBP=«Ky~˜ŒU=‰÷Eìo^ÚyÕVR\Øl \0B*xcÈIãü12üãízøXlèñGÁçÅÊ}£Ãj'øcãï¬Xí‘oÕ !%žÓéôtŠÓêH­ßz‹y{Üa n¸Cäï´‡—Õ^ëMi¾Î›yýÊ?ÏŒ7¿¦hm>²#ðnjæ™"IyCÓ<n±NcÄ%ÍvƒŽX”7fo–W3¹ÓëÖc>Õ™RmK«Ó½gÒêñ>ð|×J>Ä5e»çïü’Býv½,¦7Âç{cÒßþÇ_¨bÇ÷˜®;Úçxb­ŽsʾPHy<¹¦G9·—AwÌÓ†öÙûìëèܦÐs¯ _W|&¶3¥^5ØdštßÈ{Ýy!áÇÊŽ Q–ó‡ã-I³eë¾òa¶W·MYÏÅÐV¶•úüˆ=ž3îý›|^è°•ÛaCyØl öl`À)ýPŸ,Þ ŽTó&‹ÿäð§ãlü±t;7IìãÄ/¢ù‡#þ‡Nç÷´/ÍûãíȧÕQ°~ù߯ßÒkAçtÊÒ†TXí5:šy‰êìFB²üóLº¾–TiGº †G&k8“åa1Þ›v÷m²þmÛIšíz†P†‚/¡\š¸“8ÉÑù¥ÓŒ 7£þˆs¼-’OB[’ï™bõh¡äÇìºípïÑèwNžI6¾ÅOxq`”“ϱ‘Z¸ücÏ„bûØ;o¿ Êx¡pO»ÂÎ{/ÄÚšv,S|*¯¦ôêhV¥µ3x¥îßÔö¸â7~çØ1²;úæµq¯SÊï¼<3½:Iö§9«Oë%)ìÀ%¡W%jž€4jŸôлÏA§\Ö û¹â‘ò>?liÛ’ù‚†ó9¶] ÞzÓÚ“&PSçI¹Ìg[ Ï‹œ¶œè)vÏ'ë>käû»ÈψT{7Î;5_òs0Í^j‡åÐÙ… Àj͈ŠêºÓ¤;tá[¸Ô?ZÙo#ƒ SÊqŒNSš÷Åû‡\y²þ@j¡W¨Ž´!9Æ|«Xäºx$ÁØÓ´öæíŒ†1×¢ÜóÌ!Þtgq¤ááÔÛx.MfgÓÙóvRì$Ív=6¾ÎìЫëì¹^z¨™ôä˜ÇˆÕé¼·ûbå ûK«Ó=¿´z¼Þ_GÎñ~ÄÚlÎ…q:Œy¢¦Ýçªþabj^;xWm=Sêå†7¸Kþ}~ÏJº-˜ö«‡>ºÂ7û9àÚŠáõtlP>Ç…'8Ñ“`³Á3:¡ÞX™œ¶›)¤ÜyVöÜÄóH±e߉ ÷9–ö\´^Œ6]êó#õ™`täœús‚E ¼4l60ˆl``)ç¡ÎÑû´!ì°È?2Æ<Ù¡7Þòº_‡)Ïqœ?ŽÁ[·øÐ°xÇÚ3Ÿ in‹l»oxPþ:Ü97ùÞä=kžPŽvo6“Ú›ÂÝß‘*ç–·ƒ”`»¦«y6‰C 3ì&>äKuHÍö¤Ù·ï ¹>'çþ³ÞF¹gÒêñ>xÝ—$ާ5­Íz8ghŽ')‘§sÌTñé¼=T/’ækéÿáµ²:ÏÎP[s_,Ÿ²'·m‰%é-gÚýä´ÇäåÇ}>ûž×^iy¦Öëyn$Ì-,:Gʺ^±v¦ åtŽ>'ûÚ7$ÜïiµçÕy=ù®2„y ’õ‹„{·žQyó©y£Î\ߊEÔD¯Z{£ŽöÀË(݆2êÖ`Ï02_Ô;·ãšä±ÈysX‰uêGê›à°Ä!xÓEK¸¨òüêHÚç=ߤ!ªÓï®C«#O{=\Ë»–0„)‹UƒLO‘9gÍ Ì êµ¼Qi¶bÌ‹Š¯¥åp4¼^;Qì ;èHPE³r"‹%”ó{ÈÖºÒux"ö™ömãpæ”….ÕNYÎèp¹gLˆ[O.A+ÎÇ 2cz]ë»9yº'Û‹“\%í…OØQ4ï-7xHÂ>ŸÀç&¬M—KTe½˜0ÚãDí³˜ q£ÿq½¶-ÛÃõ’¦×›ÜíÍrï1w4‚mcÁ3ldñÑ·Ö]òúki¶lol8nÂó9ϳϰÿ@ •ðüpž ö0Àè|}ϲôó*½³‚ŽØÁ`µhCHáMÜÀuiYXAßç$w­šP0UQHH¹W–ÈÃ~¼5‚ À`°Øl6èGøB*Õ#e†ÃÍ·h¢2רÛ^,R,p®’pŒDQ—#¤µ%î–êÍ‚oŸgÑXwÁÐ@ÐBjøp^ݞ錽´òËc ‰þ ˆMæYlÒ úñ¦è/ãâíl6€ À`°cƒXHCㆌì”z‚ç§×¯2WœÏy S(ÏÍH±â¼+f‚G×ïÚ—¶Ï4º$Ï“BZšç*¾›¬äwëüÓÎÕFiL ¿Ð87r¸V°Øl6€ ÀêÛÒÂ#ø ;ýI©˜2PÑ!òx„”5,-÷1Œã©¹Lá=gH\T\H¥í‹ndGðXžW`™¿Åwµãdá±ÙáœڹƆõe!Äð¾ú~ðà®?l6€ À`ÝJš—*¤lñ•¸Ò}R W<¸‚!-FÒд4Ñaís„TÚ>k”!2cmp‡Ô¹Þ5P‘ ²„dÚ¹ú¼}ILU› ¤ððèO´6 € À`°ú¶A.¤r¨pÅFâœ+G+qTšQBŘ·Ä D"ò%ö¾¥í“7±ëéò ¹4TpüáÃ!~©óÏ/›òfy(Yž=̇ÂDQØl6€ À`°fƒWHɹF CÍ‹äzSă߃’óŽQÄ;Œ¤Ž˜ážKDé`)ÎL$ë‰P¤Î‘²Ä‘ãéJcŠ9RxH°‡$Þ6Ö÷ÛF\\Øl6H²A,¤"á ½;þµ•zÉŽÈçf´:ÛSD‹¶YsŸÂhw®æ‰&˜_H%y«âçž‘, d›]Óܦ׻ÒÛÎUtŒÝà‰LC/\ ÞBtÀ!Â`°Øl6€ ÀjĆªXƒþ{™Â°îß>Äæ~%—Çü¨‚laÿøƒ€ À`°Øl ælB Fi,lxãJà’K \ƒ^¸–ÀL Ba°Øl6€ Ô† @H¡3[sê‡Úx8à:à:À`°Øl6H¶)))Øl6€ À`°Øl   @HUŽ73°Øl6€ À`°Ø„„Þ>À`°Øl6€ À`mBª 0¼}ÀÛØl6€ À`°ØlB B o`°Øl6€ À`°‚6!UÞ>àíl6€ À`°Øl6!!…·°Øl6€ À`°Ø@A€* oðö6€ À`°Øl6€‚ÂÛØl6€ À`°Øl   @H†·xû€ À`°Øl6€ @HAHáíl6€ À`°Øl6PÐ ¤ ÃÛ¼}€ À`°Øl6€ À ¤ ¤ðö6€ À`°Øl6(hRáíÞ>À`°Øl6€ À`RRxû€ À`°Øl6€ ´©‚Àðöo`°Øl6€ À`°$¤&Óð†ji9œ†Œì ÞÉÃÕ¶á4¹7Ê3|²º¸#iHXn¸,7²£ö.üäáúÜøIR叮ޑC¤G+iyÄ•ãå¢ÅOèÓÞ%ñ©Dö8…ç¡óiGõ )ÝÆÐWƒ5ܸ}{ã‚7xÃ`°Øl6è_BŠ…DšÐ‰†Å)£ÄKV¹R…”%ܤ 2¼H‰)öŽÙÞ&«%àbBŠë3Å–û;ðCLž„ À`°Øl6€ TÔ‰òí3…”%¶Â¡%ªXÇód‰¼”¡}–°KÈç )ÛẽWª¢7Þè”x/à ;„ À`°Øl Nm`@)(Q©¡}>/PYhŸÇ(Q¨ ¯”ò.%µ#æ‘Ê1<±¬öשჄl6€ À`°Ø@9600„”èìb‰`“ö¥›HNW|Ž”3D/uè$CÍÉ>Ò®gˆ™ø©øÀr.8ʆÀ`°Øl6(ߌêµ8ý¡üÞÀÿ㼑¿ã€€€€€ &Rƒéjâ\@@@@@ú„„TŸ`ÆA@@@@@©Át5q.     }BBªO0ã     ƒ‰„Ô`ºš8>!!Õ'˜qÁDBj0]Mœ €€€€€@Ÿ€êÌ8€€€€€À`"!5˜®&Î@@@@@ OÔ„êíí¥”¾ô¥/õÉÅÁA@@@@@j“„T "Bª6­¾"!!ÕW¶†ã€€€€€À !!!5hŒ'    }EB Bª¯l ÇAC Î…T;5JC‡6S{AU‰9RGÇ£–3NRO_˜Rw 564PƒLÔØØH-Ý}q`@@@@'ºR­MC©¹½—Z›ú^Hõ¥9µ˜ÂIŠª )®§©­/OÇš!P×BJ‡\/.¤®¢Õö R/µÍ# ¼KüÛð0í8Eå6•.W‰æ´è¹Nʘ¤Àá9Kæ<1Êö—2dOH3‘WÌ{jã ãÃµÍæRÁ¼­ èð‚ê@@@@@À$P×BŠ£ö •áÏuʽ/ .Å ¡ƒ?ð\(7Є J!Áe¢¡Á\+l"L†7KͯŠ›È1GÊ á—-ÁBzc°-™$×¥ƒUÄæ4Å‚Qˆ¡„á6IªnQ¡-œ‚혅‡€€€€@­¨k!¥£öý¬Ä:Rå†Un‹’+†÷•\E% ²Ø²¼b•¨u€€€€€@å @HXˆW ®~R–7Ë TQ‚q8Þ*x‚J`ˆ"    uIBj ©º4Qœ4€€€€Ô)©Ú³J´@@@@jœ„„T›(š    µGB Bªö¬-'!!Uã&Šæ€€€€@퀂ª=«D‹@@@@@ Æ Ô¹j§æp1Þ&jíê¥ ïTn!u­¾°Wx”z©mQà]â߆‡iÇ)š(·©tá):* ätX¶å {QÝž›tïQ›,gïŽ#Ž;VןíÕ²„”a¤Ú# Ù¡n †Æ– —ãµhÔbDçã¼:¢ª»¥1Ú®;èª {øLS :.ŸÃ;#ʦŠ&Çãæó¬RÞsÙÙÞ±¦&åÍÉà’êK8‡ ìEÔ¬œtÒF<“Î=«-ÉÌ4/Ñö¶øuO÷ &°Vç§YDTÀç4š    Pƒ ¤Ô©®Ö¦È;•1o*Ú"RòŸNZÔ8û椖PH™ùm¡¤ëœ(òöp6³7„U º²ET\™qî¤ÚB¦­)Úo~')Œ¼IžÞ®…˜cê·ìôòwÃ+f#áárÉÎ/Ç»’0D¬J9nb‹5¶wÌ`šÃ#oKú9H¡¡È×Á€‘zJá™qî‰mÉ(ÇM “bås¾k”ÊZjý@˜CHÕà_4 @@9)!šXDåÖÇ^¬¼B*ôFù^ Bjìé˜8cá4Qo ·Vê K¤¸"Àñ°Xb&A0Ø^ Û‹K%®ÊRž¹OxRiç E`ÚÆ¬ýRˆ9^¸Œs°EìäUO»F9„Tä ÄZê¹ËÃ9BÜɦ‡R~·½gY×:Æ%Ë–¬    PQu/¤Ú›‡Qù„”¾FzˆžÇ{”"¤|^®Š ©”ÎpjG×çqð Š”ac¥ ) ‰‡*©Ôãeuî³öWZH™÷%=*<ÌÂkŸ1ì/QÔe ¿ŒÙ]¼qR}¶£2*¨k!ÅÁ&†6·ç 0aΧòy¤¬¡vÖ0?q}¢© ’ÃüJÚçé\»V)pxÎ’9$Oĸ³†ý¥Ý2ƒAć jÎ#¤´GÄÕhî‡h; ;£yÌìk”ÈÓ=?óÎ=½-)ÌRm0ã:¤²Êb~þš€€ôúR9SÒšR•XGªÜ‹-E•À¢äJã^’«BAè+,øO^_Ç©U¿ )Ë›åª(Á˜·ýx³_C¨KRIHÕ¥‰â¤A@@@@ ö@HAHÕžU¢E     Pã ¤ ¤jÜDÑ<¨=RRµg•h€€€€Ô8)©7Q4@@@@j„„TíY%Z    5N î…T{³^Œ·‰Z»z)ií(s{%Ÿ‹ëæXP·d…9o¤ÆF^·÷MîbµÁ»ëú†Ž    ÐWêZHIÕ܈§öæè{†—ªBª¯.0§ÅNRTU@„p=Mm}r,¤‚CE ·5 ¬sèP8€€€€@Ÿ¨k!eyŸ ©«hõ…½Â£ÔKmóˆïÿ6†(«Ú*›šqÁémÅBŹ.2€€€@]€êj¥¦¡<¼¯™ÚsΗ Gˉ–áUƒª«g!N@@@ €Òâ©G*Ÿ ½Q>Á#…W‚{:&ÎX8MÔÛc­À5—"&DѰ9Q‡+,¯M’6Ÿ©ü!zq!•gþT˜ÇŠØ‡Ã \!d ¤ /TkS>¯T¶GJ_y=DÏã=JR>/WÅ„”hšíÊ9ïÈçùñÍ‘â+i(\5„ÑËzÇž(9¤Bj<’ÐDêXHµSóP#R{¤šZ Dí³‡ïYCí¬a~Â|¢© ’ÃüJÚç Ñœ=×IÙ¨8\PÛc!¥=ZIq,J 6Ì«@€7²‚€€€ nRµO{¥ y¤„aHq£Dèà<Ê 4¡‚RHS2ÊDCÿ‚¹V:ØD ˜ o–š_6‘cŽ”5?Ê .Ú¶ùNn7CŠå\q£ƒUX+dQ´" F!Y¸ª[T芜$!UzøóàX41¸d8;èkR9׎2½W•XGªÜ -E•À¢äJ£á}%WQ«YÀYž¶Zm(Ú    0@H $!ey³Ü@%˜ã­‚צ†(    P— ¤’ªKÅIƒ€€€€@퀂ª=«D‹@@@@@ Æ @HAHÕ¸‰¢y     P{ ¤ ¤jÏ*Ñ"¨qRR5n¢h€€€€Ô)©Ú³J´@@@@jœ@] ©®Ö&ªãŸM­]d®•ô½ëH‹ëæXP·d…9o¤ÆF^·÷Q±~ã‹W¥%’›XL¸*•£RL î…T^ñTk ò1ºS8IqP!Åõ4õÄèni¤èpbáJœCˆÈ     ©œ^([H]E«/ì¥^j›Gx—ø·áaÚqŠ&Êm*]xŠŽJð§Ã²-gØ‹êöܤë|Úd9{tqܱºþl¯–%¤Œ‹ßÖÔ@ .Ä«=?-A.Ë“Õ$<¿êÄ@@@@ o Ô½bÕÔÚE¾…w“¶UBHõåen1…“ì„÷#\ˆW{~´7Äòd5 O–êä{‘<–ŽÌEw†ÊmÊ'õÝãe1K<¿œm©ôùAH ö?8?M ®…T{3Ï‹j¦vp¢šÃïé‚*¯ ½Q>Á#…W‚{:&ÎX8MÔÛc­€A;lK ¸âÈñ˜ÔU<ï+ð.µqé žQHH9Œ‚sñͱsÄÚüOŒ"B*õür´…[Péó³†XЇ"š    yÔ·CúºZ£9RC›Û Ì‘x¥¸Q"tðž åšPA)ä1ÊDCÿ‚¹V:ØD ˜ o–š_6‘cŽ”0 .ZH|!>,MÍ >Ýο9¬Ìšo F!ÄD¸…‹ ö *t;Þ^!•Ü"ãṵ̈âa  %bÂùG‚ƒ-Jt0 óüÁ ]ddäSp¬¡B˜zç%ׂqµÄµmqêdáW B!,@@@2)h¢Xê7!ey³Ü@%˜¡ãÍ'¡†n‡i.µo|5ÚR:+€U€€€€@_€*(¢‚u¤8ü9þ€€€€Ô+)©zµ}œ7€€€€”LB BªdãAA¨WRRõjû8o(™„„TÉÆƒ‚     P¯ ¤ ¤êÕöqÞ     P2:RíÔ#!%DS{sµvå›TŸÙ&    5KBªW„A7£÷å˜'TÍÚ3    }BB*‡pb/”™ ¤úÄ6q¨YRR5kœh€€€€Ô*)©ZµM´ @@@@j–„„TÍ'    µJB BªVmíš%!!U³Æ‰†€€€€@­€‚ªUÛD»@@@@@ f Ô¹kH JC9XK áÏK³ç¶¦jhh¢¶ÒŠ.ÕÒ(Ž×ØR¸\z6jjõ6õÕYT¸ù¨@@@@ "êZHµ7¥æö`¨®Ö&jjí²Ö‹r×Ò¿!¤J·½–Æ…Tw 5/BôT\H‰s/©-¥3CIÚ#PçBª‰Z»Ôb»]­ÔÔÜž[HõÜÔK-gôRÛ¼÷¨M|¶œqŒVïÐø4­¾0ؤ“Ôûvœ¢‰üû¦“²LÛnpÒ    0xÔ·*a1^Y5þÜõVõ«}V)äx¿ž    6©ÄTM )ÇSEàëS7½?ðÒôÏ5ÀQA@@@ªOBj  ©êÛŽ     à€‚ÂM     P„„TA“Av))Ü      @HAH4d€‚Â]     P@Ý ©Ö&½ o3µçUnøó`ñÖ&j+ÙA@@@@&ºR]­MÔÔÚE¼Èno{3 mn¾g$WHµ4BD LóG«A@@@@ 4u-¤Ú››¨µ+N­Mù¼RR¥J€€€€À`!P×B*æ‘j «$Ï„Ô`1œ€€€€”F ®…Too;5Õs¤Ä0¿&©ÒÌ¥@@@@@ ¾Ô¹²çC•2´¯­©[ºëËjp¶     Pç ¤T` k˜‚MÔùmÓtu-¤X< ÕCûšZ3£õé9S˜#…Û @@@@ê›@] ©¬0ç6Qß7Î@@@@’@HåX7ÊTX7€€€€Ô7© ©ú6!œ=€€€€Ô)©ú³zœ1€€€€”IB BªLBq¨?RRõgõ8c(“„„T™&„â     Pê@HµS³\+ª™Ú=¢©½™÷%ï÷…@w£öõ¿Ùœ¦ÕöRËœŽÑêýÙ¢6jjh 35µõgƒpl¨8A/¤Z›†Rs{/µ6Å…”\·¹=Xˆ·½™šZ»r-Ê[{BJÙÅŽS4±ß…”hKw 5â©­© ¥*~ï¢B~$0è…”ö(ù„”½Mx®šZk\H½GmÒë”à}ò )Ó[ÅåNRip²ŒQç…§è¨ÞŸ¶/Íh!EmMÔØÒ-JhoU#µ´ ±%½VMb«úÇåBO–ÈÃEô?QGè媬¥Q”mlɬ“E\XNæç"A]ò³©ÅÓ–~¼)qhÚ'!e ÷ó‰­ÚÚˆ¨‰cO+«âßÎ0¾©£cQËMï©:‘Õ6Oýœw’ZB!•¶/ð!ÕÝÒhy¤q£„’%² ñ$E•Yæw©ƒnsëyÛB™&eJбÓLˆ(V²î \[“#ÞjÿþE A@@@ú‰„Ô@R–ÈI°–!%Å“éur„T´ÏôVex²Ò Öò,iÏQTÀ+XBAå ˜Øg dÏ–áÉ’BÊ/‚,{¡”›+w¢.å©‚ê§§    „T(¤j|h_©BŠËž+Û#¥-Ö2è ý£´} ïís²õ™â!|¦3Ä„Ô|Z¡É    PCêZHuµ6Sk—4Ábª«•šk:ØD hÂaxˆzn2‡ú‰ >”%À”(Ò)™ßNfù´}Y\ŠâùSz“ª¿¥ÑÚgì“Ãòrx¤Ì¡|\%Ï«‚G*ëâa?€€€€@ƒ^HqÔ¾ ¼¹'̹OMáö¦HTe¬-ÕoQû’‚?H¯“„"øˆ.sˆÞIZ­‡ù±˜rë3…ZÚ¾TËrŸ›!wÈŸ1ÔŽ« RB‹žØ>!ª,‘å„[7ËÉ j\¢I®àဪ½¢ÖÜ­.ÛßQûjÑÐ&¨'ƒ^Hñ¢»Íí½ÔÚRzŸVíÍ4´©•ò¬-!UO·Î@@@@â½ÒÂÈ'¤lÑ$¼SR¸G@@@@@r€Rs¤ºZ›¤ç ©Vƒ,     Pç ¤„b•wXæHÕùƒÓA î…T{³4‘sHŸöVaŽî¨ou-¤8ØÄÐæö\ÃùÌ!Rõ}ÓàìA@@@@`Ð ))–døsTô¾®Vj²¶'…HÏ›‚    õM`Ð ©<Á#Šæªï›g    R*j_1!…@@@@ꛄ„T}ß8{(„„T fƒ"     Pß ¤ ¤êûÀÙƒ€€€€@  ¤ ¤J0ú&!!Ußwξ&´46PC§&j«‰¡    N „T;5Ëõ¢ÔúQŽpjoÖëK5QkW|Í(_4?Díc£zÚÎ8F«w½ÅÒË{ŒZÎ8I=E«íëü‡ÇÒ…wßDóúฑÈPb£±%û¨mMJ˜hÒ@M®Bén¡Æ.Ý-Ô g~ç“<¹äš}ÿ½´nËfZ'>gOÛ,ÏýÀ4ñ[i*È$#ê»Ø“KÕïìë+ŽáÍŸ|  mN[zÓw[j[“Ú) m™;÷u[ÌÊÔ¹H6ÏP÷’©´dI¯Ì ÏÇd&Ï-ëÜSÚ"kÕÇóó®U|ò>ç‚æ‡ö ÏÁís8é¼mI«2±JPDçÀl„`’b#àî“ÎææcÇâ,íÜÓÚÂâŒç;g)4´(ác(AŠ"ï>ß9˜×:Yܘ¦9(Ze][æЧ@¨k†içˆZ¿}0d ½Ò%Ÿ²¼MRqÞqŠ&žÑ+S†íX|¡,sáÓz¿(ØÀjSk,ç{zlîG‡+¤¤÷Fˆ£ÆpXœ-”l!%#¼TJIµ5% ¥cÁÅûD]m챪þp;}^Áùˆã™BŠ;¸ª3¬;ì˜î¸Ûçgä-puZ{¸³=SàõêfQ%:æºCz8´§GyIdfÓK$Ê›¢"ŸGÆãùJjgLHÇ—"Å+IÆ=çN)mq¯‡yLû8¶JÜÇ, Q£EW$ÂR¼DF;-”çüÌcšÓÎ=Å>C±è²¹ozd¨iR]­Ô”2ô¯~‡ö‚¨M+…y' ±Ä6Ï#%‡êÝôžqd”“bËIˆš8ö´ª'ß±9ó¼)†˜‘BÆŽ—á‘ “*/Ê^¸ØÇ8V‹2ãìd-Èøxú»30¨»ØÐ@ŸGJ "-/ƒáÕ æeEjÛe ¸xÇŸ‡¦ÅE“WH9s½bîoHµ„”ö¬™çrÌðJùί?„·7I(&žŸÎsçEåðÈx*!+€€À !!ėÊþ§òÖ×)sH"AH±è1H: 8 ?:RÍψÔÇ©¦Ö ð‚M¨yP†pŠ ¹³‡þõܤ†ÞY¢G‰0Ë#•PNÛgÊоp˜¡È/õÎv¼>>! «`^”é5²†zŽSÈ#å;˜OU‰¡}ù=RmMqqT)!UÎÖ 6Q "·³ìvtÓ†öÉøgO‡<,~/SR=±¨}"£Û^w(ÏóótòãCÏ¢á<·+<üó KKº\ö;ûbùƒMLÚ"Îcl"i_Á`…”÷ür©„?·lÔÂ22¯ô>9(¬`'iµægŠ)_9s›1,0š¥Q˜õÃùq¤ç>ùÔ<©hdÂ'Ä|§“þ<òq4?$BÍ™ò„?æOyC’Y2$º=ÏI¨¨Ö3Ì žçxþðçv‡5SHIq£Ê˜C»b!ÖUžXØñhøžÝŽ€§ƒGxBkk3¹œ×cFÜ3€[âÈþÜšäS3‚(xßç9÷¤¶H†)aè­¶8‚"m_RÝ"þ!–†uØHašy~Î5r]˜vïŠÔô{"i²Z÷êèƒ^Hey—JÙ__CûúÇ0ÿQùSƒÿd3ϰßäõDˆËlld¨¥vÖR[úúZ9wÎ[p ²¾>@@ <R9†ò¹b Bª<£«ÛÒŽ§Ë>Q·Tpâ    0 @HAH HÃE£A@@@@ ? @HAHõ§ýáØ     0 @HAH HÃE£A@@@@ ? @HAHõ§ýáØ     0 @HAH HÃE£A@@@@ ? @HAHõ§ýáØ I Z§(Z·)³2€€€€@• Ôj§f¹¯±¯#žºZ›R÷#üy•­Ð¬^.ž9:Ì}ÚNsÁPgQÓR.gA×u[œŠúôü’OB.ÀªH5¿s‰hÁ]ÅGåkk²n ó¢ÅYÿÄBÅ-žLjcgŸ\ûÊ›?ù8AÛœ¶ÈEó´O×›ÔNÞïYl¹)8sï±³”±¿>Ù‡Sç§ÎÉΟÆE,ÜÒ(Ø7PS[ZÙ-@нjmúÿÛ;Ó箪4çÕ¼˜ê©žê¿ «æMO¿˜šžªi{z§§è¶«Æ®Öiµm‘°¹àhQÁ•5$ :ˆQ!¢`h%AMö,„5²±±mdyæ<çÞsï¹ç7ùZõø[îvÎ÷~7çÃs–»©h{7•„”­‚²ˆíÝ øÁpCÛn¸¦ú9åz\ª‹$Üy0•%´È$8 “p"êÇâÂ_£-¡ìivk)ÕÊeÖG!C·¥QUU·sê’Q^01?‡•a#¸^@£Fr·m3>§¨TÒ²D2´œ|ƒ™V }ßœ¯mœ3Yu£Á&Ù9öŠ(K¤.âžYkÔ¨’–œ‹€¡€P °C¤…g£¢Akƒ”l´ÏUŽ ª*µ3E •5`S[¯Ž¯¥•î/-ñqA)oV¦n}µ¸íæð_sNuP×øf¥[&·~ÞrZ’¥B'UNôú •]Öt}‚jÁÇÀÊ×ah°ÊaÜu¾­½ %.,öW V?²…ë¤K_äR‰ŸF;z&›Hq"¤Ì†uT×>yFÒf^\’òLüÀ@4ƒ Y^.Ÿž±áìKàØ hʤìñLºÄž2y²# ¥Ö‰*¢íÝݤÏö×PV@wß-Ö²£ ¬Á³=lf@ÌÚ—«Ý2vÜÅRºkÎtÚÙçÅZOÈZkÈ]ƒG¬ƒ´­„F©5‚Ä6s­}-¨m²:úÚHbí"^§H-ñ£¯û#× Ò=õ®‰ä®Säû^–Ó^KÉ·Æ’w=¤°ëùÏ©-ÊÚG÷D­V×l¯§e¯æ\Θy1ɺVîºiþ˜£¶ÅVQ–%ɢαg’;ÄÖ=Ùi°€P @a£€RÇ;$D©àÏ*‚¾ÏHñ‚¼EÛ»©¬ ¤’“U©>ú}0¸”÷2¶ôÅ9û¢ú 6žUE\©…S5áK—ŒÒBíT¨ðmžE\%Ty¬õ_Ç:ÿ¶w?†5}aVo¹¼åä}Ýê18&»ž~N ‡A+¯öæý‹» *› Ä°‘ë´õQ 0'_œ¹7+kœ+¦î}xeœ @( @&ÐAêÐáVâС)è³Ú/ ¥ •‹?wÒô9ß§ï˸‹¦—O§Ò‹öyd¶ÆÝ¦¾o«¼Kî{×{þmòHÏqÓÅ~vƧiº}uœxM U9SÕí.*mRuq³O;ËU9¼©Øúéõå/eÞ+ß)~ü0Ôë R:Œ<»qyCO‡-uŒ¤$‘y , Ü’×5^âD{p–§´RîÊ ëÐä. %ËryËÉeQY'YÆÂ¥ÔÒ­•0¢îRY‘iÔá9‘¦Ø @( ÀU@©}µM¤B‡)õ^ßÎï‡Hé]û8s•d¡ßá’‘b˜®Ö“°" C‚Cˆz¯àÈ€A í,׎“СJŽÙ£žœÓ&»L¢ ¤<-û O7ÄïÓR¾ œ™i³Ž73RIñf·FÙ=‘£²Ya)/$‰n6dõ¤Î­×@Çɺjp¥¶èÇyÊ«’Ü×üóG 5D[¨€P 䬀R5ûI¦Ìmü9ó å…&KåïþV䢠&88S£ Ë6 ]zFÆÈ<9 Ænʤ¨çô–Õÿ; ) õú‰rxê#3HɆ~ÿd¤<5•çRf—>u.µ¢ŒTb²!È«•ó¯‡ö'H‘ÊnÉì‘7;¦ª•å’Ù' è¼Ù(;[ef¥z¨‡P @(0œˆ)†%†© ˆ‚ <Ž ðsè Hé§cpÒ!+WêÁ9H™z§ÌHyºÌÇt§SW ‚Ÿ@ èÒg‚Tд7ºöeÁ®åd¡T6*ƒ)OÝB²G¡ ÅffÅÄwI&ÇNQW( €=Q ¤Â j€g  ¨¬ÁîÎ×PFeèÚ§¹©ô==ó":ô‰®jV×7#Ë$¾Ñ37á)#CRXYc˜¼ž «÷윽 RF}¬ñTi3Rþ:òìuN¯´ÐI)ìã’€TÄø&ýêA eÂŒ¾¹¿91…Þ³.)¸yÎ’9ëÉ0í±á)·žKÕã®}¶¸gïä$¸C¨±Eî÷ ÚdrL‘>y…u¬ Jî$ Úâ~k9žÓ¬›(‹;>ÊN5Ž*º~D]u @%1‹Ö…Í7–GŸÝ;k^Pæ…Ç…’cŒÃ¤†×o¡jëÏÜõÁEpJ(Ð÷ 0 ³4öýEq( €ƒ[R'NžrÖŒÒ׊zÏÇ,z£”Æ›A+?ÚD×®ßïóøü¿ä÷qÿ¥™Þ wG‡ñv#Óß5qk…ªC( €P à 0H]¸x…æÍ‹ÞywUª˜;ï-jïºR|‘žÆùó§  àx€àx Î]Ý„€ð<åž² ßÚÒÞãˆÍHm:NEÅïREÅV*/çØë´mëÖm¡ù…%ÄÇ×=NkÖn¤Ï>Û†€ð<Àð<àóÀ+×SÍþzºÐýÀð@¤zƒOú¤¸ÿßÑ£je×tù;>ŽçøöÛkéÆÞP @( À°Q€Û Ü^¸pé Àð@¤zƒOú ¤zòW Åçøæ›«hÀð<À>p;Û Ý¢‰€ð<å~Vôä?>¾_AêÚõëb ðª¿aÛÔCQUöÂ…Ë„€ð<Àð<`z@µ.}ù5! <Dy@±EZ6áýÕ³¦_AêÊ•oÈ †(AÛRøC‰Æ< {€Ÿ ü9ì~_àáëÕføòò_ àx Ê ¤Ò² ï? eþqÓ!*ìŸ RgÏvÓŽ3—ˆßg&Έ²f$¤¾ˆ>Ðà’8g6b0ÿ®ø™°ÀNǽZïŠg?·²ƒùþûÊ–‘ç*?ÿ­73ÿ¼çß· ºÆÍW_}ChÀQˆêíÇ(RííçI…^@ý{ó=@ªÿ ìûªÁ“ ˆbØÌ i€TßÃØ`¾ÿ©¾z>eù¼~ºòõUB@xˆò€©0‰b•)µº¯^°¸ÕMjo?Gímç©­M¼òû¬—7#ÁÚf&ÄýoËHdÆ«ƒü7Rü|Œ‘™ßž­}ö¬Î’²ò\årò³µÛâkU›Sab.xˆó€©( c–©C‡[I…^0ý{ó½ RMM]dE'55# A†<àxWy¯îï9¹q •Ë9ûçáU~ne!ðlÅß—,y@=[E™U›á¯½FhÀQP Æ!Q¬2 µ¯¶‰ôÐ hnSŸR…,ýሲ¤ìIMA€êˆß® xËÕ uõê5B@xˆò€© ‰c”©š}d†^Рí&HÕÕ·R6ã”(7¢·5¨šf%z»î}{¾Áý;ãI&ø9öšÍgÄàÖ|ðjŠçj_< ²ò\årêõWm4 Ñ€†à8(JË&¼ÿ ). ÃTDéU•Ýàí?ÐLûÚ¯üÑ«g'šEY³Ò³Y ù;ËBdè÷Ÿ•{gk¯>Oõ¿OÙy®ò߀lö×½Õ®S]s€ªkú¢fß!ªª> ƒ·ógµû«:RgÏž£3gÎRW×i':;»¨³ó´]ÔÑáF{{'EE[['qœ:Õá‰ÖÖÒ£¥µdˆ>ˆzœ<ÙF'b¢©ù”Xm àx€àx*¨Þ³^‚ÔP©êáþ6“ÞÛµ{iÞÇ…4ùÃGiìÊÑ4ºô~·$Ÿ^\ú2mÝ»3Öê:M­bB<7Ž7¶ˆ öüq¬á$éÑpœ÷i© ºÉŒóç/Ò¹sRÇ™3ç”§Ó§ƒ£«ëœ€µsÐÎFGÇlþhk?Mfœj;M*ZOuÀð<Àð<}Tï±2R¸—ƒû^îØY)&¯û˜V¯^C«×¬ïWÑvñ]Ô}‹»·MbFð.¦'7=BEÝ3iáWhþWÏÓ«g¦Ñ -TP=‰îYò;šQò56·†^˽N§Ø‡Ã¯åÉ–N2£ùD;qœ8Ù!#vŒT0)8Šz=}úœ€'70ùÁéŒ(?(µ P ŠSm]–¸âJ¯--"“eGk§Èn! <Àð<ÀYö@Õžr RY®ÃP/ûºòM´®b=½µ¤„ÆæO¤?Ü?š–,yGŒ+ÚE›6ï½wQ÷¶ñD+M/›Nŧ^¦÷¨˜Ó\zíÚLšwy½Òõ4=â)ÊßvMÝ7‰Æþù~zhn>Ó‚´»Ž¤°WRMÍí"ë•`Ö>€¶CýÇŽúÁãð<Àð@v<°w÷ºžÔ®ù4bä|Ú5ÐÿÀÞ—åèÍs§<wy{sq‰§·é©)ÏÐüÂ×iæÌÙôØSiñâ%TòÎ2Ù-.è7uo9ŵŒ^£%4ŸÝ|•|ó"͹øÍlÿMÜýGúÍ·Óä5ãé‰]ãhôÊ{hêÂi©®Ó? U[L·ååQž×XÝö‚©ì<œð‡÷ €àx€³öîþ´ÿ@Ê«(ßiÿN  Æx?­]w¯§<×’÷>¢÷—/§iϾ(fÃÛâèðáGŸÐ³3^¦÷ß_NK—¯œà{»ç`µìÎÇ™(†¨7oΦâo­lwë{±e ýbñhKÃçÒeËé‘MÒ¯fþmÚõ…ïZaê?º­˜jÅ8) NûŠèVaž0˜H%0é@ÿ«®ÿÀƒFЀàxp $H­—Gù+­vÝ®9·RÞ¸U–+'8ß·´2èÜJ³wÅ´ÿмšR¡e$ 5öÑçhÍêÕôß>éóÊ}ùOÑÒ¥ËhâSϧ©ùkЍøÒLÙ3Q Q<6jÖùgD6j*=Zõ Z8‚nܼAW¯ÿ•¦ízœ&mM~t=Y8eƒ”‚©[‹hŸVVÆêaZÍã£jl‰í…ÝLÖÄíÉ%*-³ö¿•æUzÇIaŒ@l0ÿËÊÂð<Àýç½;?‰ÎHé€"3E*sTI³GŠ÷úv@Üödèq ÅÞßm·Ž˜3_’‘… ì"ÈÇ…€”‘¹R€ä¿¶¦§¸Ò”Á§ª‡v¬>·~ ˆžr(L|Êÿ¯¼:WŽJ/½úšpBîíã+'ˉ%xLÔ«—¦ÉL”„¨Ž?ɱQ·½ý¯´ªöCé‹[é‘Íchâúÿ¡qŸÜOwL¹#ñu&#%»ô­¥‰šÖÈW7;µoÁm”7q­Rüýj žº>žDy¬ª»ÖÒ„¼I´JÎÔ'&›¨\@#G. Jmv>€Tÿ=œð‡ZÃð<ÀðÀ`öÀîíkcºö¹°Á`?Ne‹ì.r tÈòW'­ÇÈn{Q#ADÛ7¡àa‹Êh9ç»¶ÞmÐ6óšqeð^ÓÍ¢Yè”%(ÓÊÉ· 5ýÙ©4û¥i´~yØ>•ºï¿èÇ?üm(Σ- òèŸþá{tÇϾKæQñŸ~àœ°{ËSœsê•óOÓ-oÿþXñ[šqòI QU¡ŸÎýgºzíªÌH½¸÷i™¿î4vÍè§ý$ñu’€Ô†ò÷iCù²L6Ác¤ô®}:Hq7?•™r²S“hµ‘ªÙ) RÕÖ~ÕüjŒ·ÊË›He©Oæ‡(ʆ?òð<ÀðÀðôÀö­«bÇH­ge„$1( P¯²í”1À(5HEtë3Äù¬2CPÖS˜ º&×; Ž‚2f)@jåòÉôÄSShLþÚóÅdÚ±þ;Bç¿¥ïüýßÐþy4mò÷Äç¿£Ÿÿ<Ê–ý£¯­voG¿¿ õøá±ôü¦§éë«_ÓŒMSD=D·¿} -Þ¾PzbÿÙj96jb…ÈFñÌ}ßKÿ6élj¯“¤*–Éëå6ýyH)€Ê¤PUªŒ¦?@¡ÿ9<Àð<À!Øü¹Õ…+ ¤e&j¥š¤ÁîÒ§Æ1Ù`°Bãä\Ë€šTÝêbÆF…‚”â JU#+–¤$Di]õÌ]ÂY—.˜DÛ—§ = ‡ìì|-þ÷·ûà¡5ytËw­Ï?úÁw}÷0ìÞN(/׉â Tõ¥Ýòþß¼y“ÞÝ»˜þå¥Ðå¿|)?Ï­z‰&~& êS‘Z}Ÿ#õËGÿ3ñu’€O®þKRgÏž'çÎ_ ³–n»MŒ‡ߟ9sÎîÖÇÝù´÷Ÿ•ÛT×>'uú,uqتêÓܵ/&”¹ã¢Ì®|mb­(gÝ(aöV3ÄZR-­bͨ€°DáųЀàx€à,{ f¯µ odvΣü#iĬJ¹ßŽYã)?<ÍÚ)î½Ø6bÄ<Ú!¾ÿ ?ÏÚ‡÷·÷=q²’f‰c}óÆÓv’÷ÏÿÐòÏŽY#)/¿Ì*Ÿ3Ï>FooÊïÕñe¢kŸ»<žËvmu­¼|­è2Ø×rŽák•™ëè~ïÔÁÖÅ©Ÿ]>¥Wœo¦ÏZH+V—ÓŠ²ÕTüæ;¡1»p=ððÓ¾{vog–¾"Û}¦n2M;ü}Ò^F×o\—>èºÔ)__s»ô)»—î-ù-MxaRâëpýšO´ÉEwõhjn]ùÚ¨±ÉŠã§dÄ‚”JFسô¹ÓŸßF…ÕŽº¬àqNnw½‰büSÀØ'9ª*Åb»m{ i¤Þ½O|¿'œtâ…võ…xå»)À"`€àx€†´ª÷¬©“^h9ñáx Zô8À`ƈY¤ôé˨²À…Û¼”Yû[m`°$CU ¹ %€‹Ëaï7B€(ú9Õu4XÓ'„\ æLRç/³µ¿6Ù„8·ª«¬¯*ŸrQ0õì« hÕêµ´tÙr*|m/¤ùE¯{‚¿{áÕ9tÏØÇ}> »·ÛªwÑ=oÿަÖ>LSj&QAåDZtd>ýßÕËNvhQM¡•¿ö4fÕ½4zÅïég“o¡ò-_GA”'~uÇDY¥>')G¾W‘U’“CÄD‡ë¤2L§‰#mÆIB’NÑÀ¤gŸø½&Yâ³—´¡ô€àx€à,x z•‘ÊBY‡[ fÌ¢¥ï Ài!=>õYzrÚt_<3ƒ&LžJ£~?Æw£îíso?OùbÜ/¶;yG>=¶m,MßU@§¾l¡ŽËí4ž»ó‰É%DݹàW4ö¹ñ>Iâ!•’àd‰bxÒ?Çf¤LPR0Ä€äF4 ©®yf=_W=Ñ=³La t²¥8Lnn>E2d*.8t²Ä{+=‰€ð<Àð<5Tí)— •µr‡ò>?»ˆÖ~ò)½¾ðMš;Í+,òEaa1Íxéúý˜G}÷0êÞÖk¢1s¢Ñ+ï‘JðÌ|<Åù£ë¢—¿xNމâî|œ‰bˆºcÒoèp]C O’zèx“èÂ'Býg~Ž©VÑ]ŽCAú¬w«“ßÙ$_íc‚€HÁPÔ«F Œ×&ñ™£±©5Q4o!4€àx€àx ûØ»{l×â^¾{yÇOPš0ïaܽ=$À¨àµ©ô«—)'’àu¢8 ųóñgõ³Ço¡1ÏæÓCõ¡ѯs¼±5t¿c -ä ÁÇ´ˆ©¦æV-­"ããFcc Éhò†ÚW½o<)bÅGCà Šcâ{+š­8f‡ýýQñ9IÔÕ7Àð<Àð<}ìÝý©)ÜËìßKó&½·ë6o Çf?A¿žòk¹NÔOþ1ÝþÈ¿Sþô ôqŧ±ÞPש?ÚLGêPc Ž~àïÕçÃG‰Cÿ RuõÇEa쨯*ôïíGŽ4Pš8tø¥‰ƒ‡ŽQT8x”ÌØ žÐ€àx€àl{@5‚q³}ƒî_Ý[ý:µûëI} ¯ÞW[—nŒÔA‘KÖ € ÚýGDAãc_íaÒ£fßa ŠêšCÄ¡¶©ÏA¯•U  àx€àx€àx€=PU}ÈÊjLTÐ罕’¯#ÅÙ¨ú£¢ ]“è^ç µ¿ÚΟƒÂ“í Èp©cŽˆ,X’8,2aA‘&Û…}Óe¡ô‚àx€àxjêýÆuT™´ÚýuñëHqß?4€àx€àx€àxÀë•m¢k×oH^Êãÿñþ àx€àx€àx÷€¤ø 4€àx€àx€àx ÚNF );¤máx€àx€àxHîAdmin forbidden = kv_files = languages = en-us [options] root_as_url_component = 0 default_file_view = log checkout_magic = 0 http_expiration_time = 600 generate_etags = 1 sort_by = file sort_group_dirs = 1 hide_attic = 1 log_sort = date diff_format = h hide_cvsroot = 1 hr_breakable = 1 hr_funout = 0 hr_ignore_white = 1 hr_ignore_keyword_subst = 1 hr_intraline = 0 allow_annotate = 1 allow_markup = 1 allow_compress = 1 template_dir = templates-viewsvn docroot = [PATHTO]/docroot show_subdir_lastmod = 0 show_logs = 1 show_log_in_markup = 1 cross_copies = 0 use_localtime = 1 py2html_path = . short_log_len = 80 use_enscript = 0 enscript_path = use_highlight = 1 highlight_path = [PATH] highlight_line_numbers = 1 highlight_convert_tabs = 2 use_php = 0 php_exe_path = php allow_tar = 0 use_cvsgraph = 0 cvsgraph_path = cvsgraph_conf = cvsgraph.conf use_re_search = 0 use_pagesize = 200 limit_changes = 100 [templates] [cvsdb] enabled = 1 host = localhost port = 3306 database_name = ViewVC user = [UID] passwd = [PWD] readonly_user = [UID] readonly_passwd = [PWD] #row_limit = 1000 [vhosts] viewvc-1.1.22/templates-contrib/viewsvn/templates/0000755000175000017500000000000012265242273022442 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/viewsvn/templates/diff.ezt0000644000175000017500000002044611040367645024105 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Diff of /[where][end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "diff"] [include "include/file_header.ezt"]

[if-any raw_diff]
[raw_diff]
[end] [define left_view_href][if-any left.prefer_markup][left.view_href][else][if-any left.download_href][left.download_href][end][end][end] [define right_view_href][if-any right.prefer_markup][right.view_href][else][if-any right.download_href][right.download_href][end][end][end] [if-any changes] [for changes] [is changes.type "header"] [else] [is changes.type "add"] [else] [is changes.type "remove"] [else] [is changes.type "change"] [if-any changes.have_right] [else] [end] [if-any changes.have_left] [else] [end] [if-any changes.have_right] [else] [end] [else] [is changes.type "no-changes"] [else] [is changes.type "binary-diff"] [else] [is changes.type "error"] [else] [end] [end] [end] [end] [end] [end] [end] [end]
[is left.path right.path][else][left.path][end] revision [if-any left_view_href][end][left.rev][if-any left_view_href][end], [left.date] [is left.path right.path][else][right.path][end] revision [if-any right_view_href][end][right.rev][if-any right_view_href][end], [right.date]
# Line [changes.line_info_left]  [changes.line_info_extra] Line [changes.line_info_right]  [changes.line_info_extra]
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end]    [changes.right]
 [changes.left]  
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end] [changes.left]  [changes.right] 
 

- No changes -
 
 

- Binary file revisions differ -
 
 

- ViewVC depends on rcsdiff and GNU diff to create this page. ViewVC cannot find GNU diff. Even if you have GNU diff installed, the rcsdiff program must be configured and compiled with the GNU diff location. -
 
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end]  [changes.left]  [changes.right]
[end] [if-any sidebyside] [for sidebyside] [if-any sidebyside.gap] [end] [for sidebyside.columns] [for sidebyside.columns.segments][if-any sidebyside.columns.segments.type][sidebyside.columns.segments.text][else][sidebyside.columns.segments.text][end][end] [end] [end]
[is left.path right.path][else][left.path][end] Revision [left.rev] [is left.path right.path][else][right.path][end] Revision [right.rev]
[sidebyside.columns.line_number]
[end] [if-any unified] [for unified] [if-any unified.gap] [end] [for unified.segments][if-any unified.segments.type][unified.segments.text][else][unified.segments.text][end][end] [end]
r[left.rev] r[right.rev]
[unified.left_number] [unified.right_number]
[end]
[for diff_format_hidden_values][end]
[if-any raw_diff]   [else]
Legend:
Removed from v.[left.rev]  
changed lines
  Added in v.[right.rev]
[end]
[include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/viewsvn/templates/directory.ezt0000644000175000017500000001135311677110026025171 0ustar cmpilatocmpilato[include "include/dir_header.ezt"] [is cfg.options.show_logs "1"] [end] [if-any up_href] [is cfg.options.show_logs "1"] [end] [end] [for entries] [if-any entries.errors] [else] [is entries.pathtype "dir"] [else] [end] [is cfg.options.show_logs "1"] [if-any entries.log] [else] [end] [end] [end] [end]
Name [is sortby "file"] [is sortdir [end] Revision [is sortby "rev"] [is sortdir [end] Age [is sortby "date"] [is sortdir [end] Author [is sortby "author"] [is sortdir [end] Last log entry [is sortby "log"] [is sortdir [end]
Parent Directory
      
[is entries.pathtype "dir"] View directory contents [else] [if-any entries.view_href][end] View file contents [if-any entries.view_href][end] [end] [is entries.pathtype "dir"] [entries.name] [else] [if-any entries.view_href][end] [entries.name] [if-any entries.view_href][end] [end]
[for entries.errors][entries.errors][end]  [if-any entries.rev][entries.rev][end] [if-any entries.rev][entries.rev][end] [entries.ago]  [entries.author] [entries.log] 
[include "include/dir_footer.ezt"] viewvc-1.1.22/templates-contrib/viewsvn/templates/query.ezt0000644000175000017500000002011611677106750024340 0ustar cmpilatocmpilato Checkin Database Query [if-any docroot][end] [# setup page definitions] [define help_href][if-any docroot][docroot]/help_query.html[end][end] [# end]

Select your parameters for querying the CVS commit database. You can search for multiple matches by typing a comma-seperated list into the text fields. Regular expressions, and wildcards are also supported. Blank text input fields are treated as wildcards.

Any of the text entry fields can take a comma-seperated list of search arguments. For example, to search for all commits from authors jpaint and gstein, just type: jpaint, gstein in the Author input box. If you are searching for items containing spaces or quotes, you will need to quote your request. For example, the same search above with quotes is: "jpaint", "gstein".

Wildcard and regular expression searches are entered in a similar way to the quoted requests. You must quote any wildcard or regular expression request, and a command charactor preceeds the first quote. The command charactor l is for wildcard searches, and the wildcard charactor is a percent (%). The command charactor for regular expressions is r, and is passed directly to MySQL, so you'll need to refer to the MySQL manual for the exact regex syntax. It is very similar to Perl. A wildard search for all files with a .py extention is: l"%.py" in the File input box. The same search done with a regular expression is: r".*\.py".

All search types can be mixed, as long as they are seperated by commas.

CVS Repository:
CVS Branch:
Directory:
File:
Author:
Sort By:
Date:
In the last hours
In the last day
In the last week
In the last month
Since the beginning of time
[is query "skipped"] [else]

[num_commits] matches found.

[if-any row_limit_reached]

WARNING: These query results have been artificially limited by an administrative threshold value and do not represent the entirety of the data set which matches the query. Consider modifying your query to be more specific, using your version control tool's query capabilities, or asking your administrator to raise the database response size threshold.

[end] [if-any commits] [# uncommment, if you want a separate Description column: (also see below) ] [for commits] [for commits.files] [# uncommment, if you want a separate Description column: {if-index commits.files first} {end} (substitute brackets for the braces) ] [# and also take the following out in the "Description column" case:] [if-index commits.files last] [end] [# ---] [end] [end] [# uncommment, if you want a separate Description column: ]
Revision File Branch +/- Date AuthorDescription
[if-any commits.files.rev][commits.files.rev][else] [end] [commits.files.link] [if-any commits.files.branch][commits.files.branch][else] [end] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [commits.files.plus]/[commits.files.minus] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [if-any commits.files.date][commits.files.date][else] [end] [if-any commits.files.author][commits.files.author][else] [end] {if-any commits.log}{commits.log}{else} {end}
  Log:
[if-any commits.log][commits.log][else] [end]
            
[end] [end] [include "include/footer.ezt"]viewvc-1.1.22/templates-contrib/viewsvn/templates/revision.ezt0000644000175000017500000000554011451677216025035 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Revision [rev][end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "revision"]
Jump to revision: [for jump_rev_hidden_values][end] [if-any prev_href] Previous[end] [if-any next_href] Next[end]
Author: [if-any author][author][else](unknown author)[end]
Date: [date] ([ago] ago)
Log Message:
[log]

Changed paths

[if-any more_changes]
Only [limit_changes] changes shown, display [more_changes] more changes...
[end] [if-any first_changes] [end] [if-any changes] [for changes] [end] [else] [end]
Path Action
[if-any changes.view_href][end][changes.path] [changes.path][is changes.pathtype "dir"]/[end][if-any changes.view_href][end] [if-any changes.is_copy]
(Copied from [changes.copy_path], r[changes.copy_rev])[end]
[changes.action] [if-any changes.prop_mods], props changed[end] Revision Log [if-any changes.diff_href]Diff to previous[end]
No changed paths.
[include "include/props.ezt"] [include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/0000755000175000017500000000000012265242273024113 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/0000755000175000017500000000000012265242273025360 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/viewvc.png0000644000175000017500000000244511067250275027376 0ustar cmpilatocmpilato‰PNG  IHDR(-SsRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEqJ”½^•½`»Õ™æææçççÿÿÿû¤tRNSÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿS÷%tEXtSoftwarePaint.NET v3.36©çâ%gIDAT(SUKÀ C%ÅzÿÓ„Øie¡áMøÚ´/¨NPD‡ƒ‰KB‘é¯ÁZ+€þ.;$ý §|€MˆØ`лGÙ1¹î¨äÎ2¨DWh‡ )õ ` ó}§ÿk÷Ù³‰}Xbný¼IEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/download.png0000644000175000017500000000044710615044233027673 0ustar cmpilatocmpilato‰PNG  IHDRíÝâR+tEXtCreation Timemer 4 giu 2003 10:11:54 +0100–êÙ¨tIMEÓ /Q| pHYs  ÒÝ~ügAMA± üaPLTEóóó{{{ÆÆÆ„ÿ„ÿ™T*tRNS@æØfNIDATxÚc````Kd€¶B\ ¶´BA+½\EJIIIHIIÉ ÀÂLŠ Ê ¾I”ald(ˆVd(A€£ 00ô šŒÐ²IEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/dir.png0000644000175000017500000000127110615044233026636 0ustar cmpilatocmpilato‰PNG  IHDRóÿasRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.01®$EIDAT8O¥“]OA@×ÿçj ŠCÔ¨âMˆcðDBL¤„ÀTù "A* P¡Ø¦XÖ jwg—ã”ãW&93¹3÷žÌLfÊZBpv‹Uo-þž"üÝ…=×X ÈéôÄŠ6ÝŒ:q¡Å4£Ä¾ù˜ëºˆo¢7-‰òñÉeb3¡î³ñå5³í¼o-Øg¶ÃÎÒ̋ߤÊdƒ ÃXø3ºcï«£Œ=´áõz‘(òÐõiV-òŠ˜ï:ûÎ0ÿ¼Õ׳/QÞÔœ –[.DßZI,ìµ9š’mÉšDŲžÉœ^Þµ0>>¾+QFîæ G;˜tÙd¼ƒ%‚Xú¨d+ÞƒëDlµ!6±<}#Z…·>¦¦¦¤àUE6†ZÍXíIo²ëÊ=ÅÚjÇÜlF¬×c¬ÕaDªÐÞ^'®9­É¥±±1)xéÈBJ®Ì‘ñ–,2טÑ:D¤±\‰¡ÝBW,”ðãó%†îO úJ3ò(Ï–‚AÌÈ=LY$´ ¹3F¸}ñ ñ`1áÁ¶lô•e¦ýe§Ñ܇º}J Z1µrDøÆâUŒ` ú§sÄýùÄçr÷g±ä9JCqn·;y_wîóGè¼íö Jr …’‚Dçñxp¹\8Îÿ’È›ššJ½ƒt?Íßò~¨”öŸ•(±IEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/down.png0000644000175000017500000000024710615044233027031 0ustar cmpilatocmpilato‰PNG  IHDR H%v?PLTEâÇÃtRNS@æØfbKGDˆH pHYs  ÒÝ~ütIMEÔ #èe"IDATxÚc`Àöä0ð70°300AÅ+q—‡Ž™IEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/favicon-svn.ico0000644000175000017500000002262610614112411030277 0ustar cmpilatocmpilato(fhŽ èö ¨Þ00h†00¨î( À€€€€€€€€€ÀÀÀ€€€ÿÿÿÿÿÿÿÿÿÿÿÿ÷wÌÌÌÌÌÌÇwÿwÌÌÌÌÏÌÌwÿwÌÌÏÌÌÌÇ÷|w|ÌÌÌÌÿÿÿÿ÷wwÿÿÿÿÿÿÿÿÿÿÿ|ÌÌwwÇ÷|ÌÌÌÌÌÌwÿwÌÌÌÌÌÌÌwÿw|ÌÌÌÌÌÌwÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ( @øóïïãÛäÏÁع¦Ì¤ŠÈœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Ï©‘ع¥â˼ìÞÔøòï÷ñíéØÍÛ¿­Í¥ŒÈœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€ú÷õÈœ€Èœ€Èœ€Í¥ŒÛ¾¬éØÍùôñõíèæÒÅÕ´žÈÈœ€Èœ€Èœ€Èœ€ú÷õÈœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Ó±›çÔÈûøöú÷õé×ÌÕ´žÈœ€Ý°ٺ§Ó±›Î¨ÉžƒÈœ€Èœ€Èœ€Èœ€Èœ€Èœ€Ë¡‡êÙÎÿÿÿÿÿÿ÷òîÿÿÿÿÿÿÿÿÿÿÿÿþþþú÷õôìçïãÛéØÍåÑÄáÊ»àȹíÞÕÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿú÷õöïêøóïýûúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ÷ñíÿÿÿÿÿÿÿÿÿÙ¼©Èœ€Èœ€Èœ€Ë¡‡Ð«“Öµ ÜÀ®áÊ»çÔÈìÜÓðäÜÈœ€Öµ êÙÎûø÷þþþíßÖÖ¶¡Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Éžƒ×·£èÕÊøòïùôñèÕÊØ¹¥Ê …Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Ð«“ßŵíÞÕùôòòéãæÓÇÛ¿­Ñ­–ÉžƒÈœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Èœ€Ð¬”ݲéØÍôìçýûúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ( @€€€€€€€€€€ÀÀÀ€€€ÿÿÿÿÿÿÿÿÿÿÿÿ÷wÌÌÌÌÌÌÌÌÌÌÌÌÌÌÿÿÿ÷|ÌÌÌÌÌÌÌÌÌÌÌÇwÿÿÿ÷wÌÌÌÌÌÌÌÌÌÌÌÌÇwÿÿÿwÌÌÌÌÌÌÌÏÏÌÌÌÌwÿÿ÷|ÌÌÌÌÌÌÌÌÌÌÌÌÌwÿÿ÷|ÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿwÌÌÏÏÌÌÌÌÌÌÌÌÌÿÿ÷||ÌÌÌÌÌÌÌÌÌÌÌÿÿÿÿÿÿwwwÌÌÌÌÌÌÇÿÿÿÿÿÿÿÿÿÿÿwwwwÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿwÌÇwwÿÿÿÿÿÿÿÿÿÿ|ÌÌÌÌÌÌÇwwwÿÇÿÿÿ|ÌÌÌÌÌÌÌÌÌÌÌÌwÿÿÿ|ÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿ÷ÌÌÌÌÌÌÌÌÌÌÌÌÌwÿÿÿwÌÌÌÌÌÌÌÌÌÌÌÌÌwÿÿÿwÌÌÌÌÌÌÌÌÌÌÌÌÌwÿÿÿwwÌÌÌÌÌÌÌÌÌÌÌÌwÿÿÿÿÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ( @€€€€€€€€€€ÀÀÀÀÜÀðʦȜ€ÈȂɂɞ‚ÉžƒÉŸ„ÊŸ„ÊŸ…Ê …Ê¡†Ë¡‡Ë¢ˆÌ£‰Ì¤ŠÍ¥ŒÎ¦Î§ŽÎ¨Ï¨Ï©Ï©‘Ϫ‘Ъ’Ъ“Ы“Ы”Ь”Ѭ•Ñ­–Ò®—Ò®˜Ò¯˜Ò¯™Ó¯™Ó°™Ó°šÓ±›Ô³Õ³žÕ´žÕµ Öµ Ö¶¡Ö¶¢×·£Ø¹¥Ø¹¦Ùº§Ù»§Ù»¨Ù¼©Ú¼©Ú¼ªÚ½ªÛ¾«Û¾¬Û¿­ÜÀ®ÜÁ¯ÜÁ°ÝÁ°Ý°Ý±Ý²ÞòÞijÞÄ´ßŵ߯¶àÇ·àǸàȸàȹáɺáÊ»â˼â˽ã̾ãÍ¿äÏÁåÑÄæÒÅæÓÆæÓÇçÔÈçÕÉèÕÊèÖËé×ÌéØÍêÙÎêÚÏêÚÐëÛÑëÛÒìÜÓìÝÔìÞÔìÞÕíÞÕíßÕíßÖíß×îáØîáÙïãÛðäÜðåÝñæßñçàòçáòèáòèâòéâòéãóêäóêåôëæôìæôìçõíèõíéõîéöïêöïë÷ñí÷òîøòïøóïùôñùôòùõóú÷õûøöûø÷ûù÷ûùøüùøüúøüúùüûùüûúýûúýüûýüüþýüþýýþþýþþþÿÿÿðûÿ¤  €€€ÿÿÿÿÿÿÿÿÿÿÿÿõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõˆr[C%      –{_B 4L_v    rT/ 44 8YrŽ   –uV0 4_4 Fe‹   ”sP# 44 Ms˜   ‰`7 4_4 a‡Ÿ  ™x[9 @aƒŸ  ŸŒrZB' 7Zu’    ž‰ "C]v õõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõõÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ(0`€€€€€€€€€ÀÀÀ€€€ÿÿÿÿÿÿÿÿÿÿÿÿ÷w|ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÿÿÿ÷wÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÿÿÿÿÿÿwÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌwwÿÿÿÿÿÿw|ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇwÿÿÿÿÿw|ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌwÿÿÿÿw|ÌÌÌÌÌÌÌÌÌÌÌüüÌÌÌÌÌÇÿÿÿÿwÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿwÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌwÿÿÿÿ÷|ÌÌÌÌÌÌüüÌÌÌÌÌÌÌÌÌÌÌwÿÿÿÿ÷ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿÿ|ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿwÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÿÿÿÿ÷wwwÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿÿÿÿÿÿÿ÷ww|ÌÌÌÌÌÌÌÌÌÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿ÷ww|ÌÌÌÌÇÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ÷ÌÌÌÇwwÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ|ÌÌÌÌÌÌÌÌÇwwÿÿÿÿÿÿÿÿÿÿ÷ÌÌÌÌÌÌÌÌÌÌÌÌÌÇwwwÌÿÿÿÿÿwÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿÿ÷ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÿÿÿÿ÷ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿÿ|ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌwÿÿÿÿ÷|ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿwÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿw|ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿ÷|ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿÿw|ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇÿÿÿÿÿÿÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÇwÿÿÿÿÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌwÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ(0`€ €€€€€€€€€ÀÀÀÀÜÀðʦȜ€ÈȂɂɞ‚ÉžƒÉŸ„ÊŸ„ÊŸ…Ê …Ê¡†Ë¡‡Ë¢ˆÌ£‰Ì¤ŠÍ¥ŒÎ¦Î§ŽÎ¨Ï¨Ï©Ï©‘Ϫ‘Ъ’Ъ“Ы“Ы”Ь”Ѭ•Ñ­–Ò®—Ò®˜Ò¯˜Ò¯™Ó¯™Ó°™Ó°šÓ±›Ô³Õ³žÕ´žÕµ Öµ Ö¶¡Ö¶¢×·£Ø¹¥Ø¹¦Ùº§Ù»§Ù»¨Ù¼©Ú¼©Ú¼ªÚ½ªÛ¾«Û¾¬Û¿­ÜÀ®ÜÁ¯ÜÁ°ÝÁ°Ý°Ý±Ý²ÞòÞijÞÄ´ßŵ߯¶àÇ·àǸàȸàȹáɺáÊ»â˼â˽ã̾ãÍ¿äÏÁåÑÄæÒÅæÓÆæÓÇçÔÈçÕÉèÕÊèÖËé×ÌéØÍêÙÎêÚÏêÚÐëÛÑëÛÒìÜÓìÝÔìÞÔìÞÕíÞÕíßÕíßÖíß×îáØîáÙïãÛðäÜðåÝñæßñçàòçáòèáòèâòéâòéãóêäóêåôëæôìæôìçõíèõíéõîéöïêöïë÷ñí÷òîøòïøóïùôñùôòùõóú÷õûøöûø÷ûù÷ûùøüùøüúøüúùüûùüûúýûúýüûýüüþýüþýýþþýþþþÿÿÿðûÿ¤  €€€ÿÿÿÿÿÿÿÿÿÿÿÿ…lY?&      ˜|`F(           ™y\> '=Vf}•         ŒnP) ;XmˆŸ       rS. 6VpŒ       “sR( 22 Dd‡Ÿ      kI 2•2•2 CfŒ      Ÿ‡]5 22 &Tx      ”nD 22 Ds›      wN 2•2•2 I{Ÿ      žwM 22 (hœ       œtC o         ’cd[UK>2" Gž                 ™Š}qaWF7' p                           ™‰zl_UE8. 7Ž                                      œˆ„‡˜                                                                                                                         ‘‘™Ÿ                                         v* !0LW]f Cuž         t* T€Ÿ       Ÿu9 #XƒŸ       ‹Z% #U{ž      Ÿ‚U Fm’      Ÿ…[1 4[Ÿ      ’qN" Ad‰       kO+ JiŒ       ˜z^C# HgŠŸ       ˜eQ6 @^~›         –€ 1RjˆŸ       7Uj† ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿviewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/text.png0000644000175000017500000000115110615044233027041 0ustar cmpilatocmpilato‰PNG  IHDRóÿasRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.01®$EÃIDAT8O“ß/‚QÇß.ü.ºpáÊßà?âˆ4?fXÚbÍÌ2ÌV3Œ)b óëMkdó+”ÌoÑ$IõuÎÃKyÕœíÙ9ïv>Ÿç9Ï9¯€ÀÇèfñDZH'S,Øœúšé;%* „Ò¢„PR¬T .à1,¾±és¤ÓÀ{ xM±w ’úmטñű¸å[ˆ¡äÒbpå%'Žúá º€q6ïÁä[`^|&ÁïÌ1w«É­aŸ¢¡k‡*ùô;£`•g•-Á×Ìí Ä0¿~KÑbÚƒÅbÉô9"$Μ _Ä€³(Œsë!4w`µZ³½¶057,ìât†m¹À4"A>ø0 ÌŠ÷ÐvnÉ=ã7$à ãgÎ,›gæðþ`ÃдyäãØ% òÁ;ìölâêšÝrÁzN‚\™9콦]¨u¢\ 9%Ôí̲%Ø}L¹Q­Y“ :‡NHÀ¯*¼&]O¨T/Ëíæcäƒ|ÀÄF ª¥?Ž0pD/Œ?]×6´[¨gÝ®e «i¡beóÌUK(+·Ë~¿Ÿžçƒïçÿ«ÍILˆIEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/up.png0000644000175000017500000000025010615044233026500 0ustar cmpilatocmpilato‰PNG  IHDR H%v?PLTEâÇÃtRNS@æØfbKGDˆH pHYs  ÒÝ~ütIMEÔ2›Î‘IDATxÚc`ÀL ì ü òì@Ä—Gb2ÞIEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/feed-icon-16x16.jpg0000644000175000017500000000175410413061470030474 0ustar cmpilatocmpilatoÿØÿàJFIFddÿìDuckyPÿîAdobedÀÿÛ„      ÿÀÿÄ¢  s!1AQa"q2‘¡±B#ÁRÑá3bð$r‚ñ%C4S’¢²csÂ5D'“£³6TdtÃÒâ&ƒ „”EF¤´VÓU(òãóÄÔäôeu…•¥µÅÕåõfv†–¦¶ÆÖæö7GWgw‡—§·Ç×ç÷8HXhxˆ˜¨¸ÈØèø)9IYiy‰™©¹ÉÙéù*:JZjzŠšªºÊÚêúm!1AQa"q‘2¡±ðÁÑá#BRbrñ3$4C‚’S%¢c²ÂsÒ5âDƒT“ &6E'dtU7ò£³Ã()Óã󄔤´ÄÔäôeu…•¥µÅÕåõFVfv†–¦¶ÆÖæöGWgw‡—§·Ç×ç÷8HXhxˆ˜¨¸ÈØèø9IYiy‰™©¹ÉÙéù*:JZjzŠšªºÊÚêúÿÚ ?÷‘<“£ùÃ˺‡šüí£ù’úîòþv½ó¾Ÿ$71ÅÅQ‰žÔò“ŠV¬Áztéž+£ÒbÖéç¨ÕC)õË /ªUdË“í¥Út:ˆiô³Äˆ¬R$óúgôÙè çÍ'ó¯—.?+îtûÛÈî ½O­ù_Ìö`*Ê«J:^.µZíîhµÝ™©ì½D2C'«Hòýu¡Øöv»kbœ' ôÎæé¡N´8èß–:3¥gùƒa©D.­ùñ¶6‰ÃÖ·ž.`@aBµ©Ïìíd{.2¸äŽª97ßÐaµÆB÷½úu[Ùù;[ õBZi@×ó¸÷©DÖÝ:ô"˜Çž0t=èú·|¥¥jw—·~m‹Sò¨·Uô º†—vÆŽx/ªÎÊ£ný³o©Õéõxg¦Á¾^,b¹ Tyì8¬€6Ogvf}Xjõˆ †S|ÌO¢\·<4 æÿÿÙviewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/svn.png0000644000175000017500000000101210614112411026650 0ustar cmpilatocmpilato‰PNG  IHDRóÿasRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.01®$EdIDAT8O¥SûKa¼þó""ˆ ¢è—J”²’4,²°¤IæÛ|£i¥ùÂëòRÏ”œn7¤ŽÌ„øàø¸™ÝÙo€  $ ç“kŒ{–­a\Ý#ª¢\o¢ÛûÀH¥Ý0î<â9r󅪌›h ¶ó VlLnµt±qœ„7^$wÐPIômv¦0côí”;0:¦kP:=ôû}$ò"LG‰±FĆ¢zø ‹+­!Κ| Q¨ÈÞE§ÞGxbeD³ud‹¯œ€-¥‹šÔ†ôÖa“þÃ/DÈåLAbǯ#Ï\Ùéy`CB°«±lŸ¤±ºÅÂNý+V‡¼àEV¾R Š£vÄiî¶j2ýëOVa8Œ3g¨À¼9ëY†Û§(+b ¦Öµ‹',ZBœ³Ë÷„X®ÎùS2D¦…!±QÝ ä6™u*bKšÛô•ÿ@T×Câ—¬ç%÷mä N4¶×¸IEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/0000755000175000017500000000000012265242273027757 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/log.png0000644000175000017500000000062210615044233031237 0ustar cmpilatocmpilato‰PNG  IHDRóÿasRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.01®$EìIDAT8O­“1‚@E±áÄ#Xy¬H<€­¡6ÁB³½¡5&ÁŠŠx ÎA÷Ý?8‘$ù™l†ÿæYf¼IS4É,éß§çyÜàl5"óýh}W+Öáõ@–e-@!#R8€$yFç R¦lNQU•ô@Ç58™Ž™=ª®kQQ\úŒ1à*’¦PãW'§¥‡=hšF¦ð¼Ú¶kh­½QÁ`-¢Q¥€Á›ÃË´;yP-Ñ  ‰•âäКƒ ð£Í­AE#åûþÏ õÿaÌõí¾ó }«Ã+W©ZIEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/checkout.png0000644000175000017500000000107710615120021032256 0ustar cmpilatocmpilato‰PNG  IHDRóÿasRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.05Ãà\™IDAT8Ocüÿÿ?>pbSÀÿßO30°03üûóŸáïß ¿¿üaà7Pa°ðÛÀÈ2Þ¿Êöÿï“:ÿÞðýÿçÇÂÿ¿ß÷üÿö êÿ‡ó6ÿ?m”ÿ’ÇkÀÑyÖÿŸÖýÿóª V6äªá7`[‹íÿGs„ÿ¾júÿç‡Ôÿßîûþ{Tõÿ£e"ÿŸV²ýßWcŒß€­¶ÿ?œ°þÿóeV4`[3ºÝÀ0ˆD„A­n040üGÇ¿?t ˆ lH£Ñl†ÿ6» Ɖ{ŽiH• vî8†¹` ¢ñPƒ!Ä 0°iþpÂñÿÏ @œ4”âà.8ÔˆdÌ™ ÛAζ«ÍöÿûcN@òÌÐ4 » MÈšA.p¦èÿÏ—M€é ¨Ù˜4 é€ýÿæ4u„`³dÀ™ !x X5衎¨Ç7úÿ?¶ÐþÿñEö@Úöÿ‘ÙVÿM5ÿ(?X ‚Â#‚/ÃÁ À–pˆ1­ó‘EµN‘öIEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/patch.png0000644000175000017500000000113610624010164031552 0ustar cmpilatocmpilato‰PNG  IHDRóÿasRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.01®$E¸IDAT8O¥“M(qÆGXqprQâ ¹ ‹lжDÔúŠÝ$ß%ÒF)EÉÇIä$rp’‹l)Ö‘ÃÞXåàêdéÿØçÕììØR¦þÍÌæ÷<ïû¼3)Œˆ_MOóð‡–c[Ö}gh©Û¾ nWkð¨‹HÍÝ(Òß;eS€ÎnP‹àê<ža¼g¼uÀxõXDD nQKp«”EÆQ|Ý'vçµI»¬—JD`(´"î„é\™AepÛêÕÑ9ðe:›<7Ãy:ð#@§áÚ›¹.QSÈQ> Ïè/ø>Ç„4lV gÏm×ì©+i£HM ë³ ù/ýè8ŸEfq6œÝ¿‡H‘±»UÜ«0jOJÏý”²So¤¥õ‹Ýäl®Ì!øD©š‘ŒX¿yg­K±1VöšUHˆººjø¢›p…'M˜•P„«j§ë'D¦N€tÍWÃ[ÐÎN?qÃqT ÇF¹u œ;aGáñ#bhÉ`Û #~Ó°žNå~蜱_c:ëg–›9¯8'þL®-¯ ¶Uýåþ }~óËIEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/diff.png0000644000175000017500000000065310624010767031377 0ustar cmpilatocmpilato‰PNG  IHDRóÿasRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.07õÍpIDAT8O¥“1‚@Eñ$ôÞ€F+.@l µ  0ž`iCᬬ8½”6\€nôMÜuQŒ$vwvÞü?,3ñþz˜‘$‰AQ߬ÝxßÜ&§i*§›HÛ¶Ò4ÎìA@%“Ìi U2¨DÈ%©®k)ËRâ8Ö7Õÿ¦Bx6•‘o,ðŒ[».ÌH¢"*PPG…NRE‘õìö‚yžç ´@Ð|’¨Ê Ù÷}Ù®céÊçGü¤iŠãhÛMÓè=Å÷SÀ“¬™» ±€âŒÉ$ ,KàKâ~&¨ª qëÊÓ à8÷R(@ÍçBn¢BãÛ˜„v2 È²ì °IÁûý¦ÿhÓbÒrÚí€{`B'ð£Žñ ¡‰±)¾÷<›…Ár>£+1Ïsp”áï€ð`fðý![%A·I´ÀíÉ`7]×µ>Ï—ýÆ0 ÕÈцÃ9ÞU4ZÝxbE=Dº²IÌbÓ›V;«Y0žwa'  ‰+Å“1»®{·u Ù­®¥–ö*+)Çqþ‰8Çÿ£vé9š‡IEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/repos.png0000644000175000017500000000100510615000017031572 0ustar cmpilatocmpilato‰PNG  IHDRóÿasRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.01®$E_IDAT8Ocüÿÿ?Ed%«æãýÿšjôÿÐLÓÿ¦š€i‰£[†aÀþU¶ÿŸÔùÿó†ïÿ??þÿý¾‚b Œn†GçYÿÿ}Z÷ÿÏ«.(ü?¶üÿ§ò„ ØÖbûÿÑáÿŸ¯šþÿù!õÿ·û¾ÿßUýÿO7ØW½á‚­¶ÿ?œ°þÿóeÜßDýÿpÞ†8¶5£ÐýÿÛƒH¸²Uð»`{‹ Ä/@.Xôÿ÷‡. pö×á7`G+º@‰7`WÈ[¨ €ÑvR4âwÁ®6P8B ˜4 h@"‰0àý1' ùÀ0˜4 h@"iÜŸ)úÿóe`:Hjö¦ÿ–‰üZÉþsš:~/œÙòŸ«fˆP*¥÷c íÿ_d¤mÿ™mÌ\æÿÅÐ3ë)ɱMµˆIEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/back_small.png0000644000175000017500000000045510615044233032552 0ustar cmpilatocmpilato‰PNG  IHDRóÿasRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.01®$E‡IDAT8Ocüÿÿ?Ed%˜"Í`×Sb;} ШaøŽ‘]× F³þÛì‚`$3Åd »õ¿Ã\fX )Îý¯Ö ¢‘ &hÌ™ gãÓŒ Å MÄjÆi,ÐÙŽÕäP'Û!0LÈŒXÀ–pˆŽrò³àÿ'#–IEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/logo-svn.png0000644000175000017500000000221710614433142027625 0ustar cmpilatocmpilato‰PNG  IHDR, iFšzsRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.05Ãà\éIDATXGíXiOQ5š¨_ŒúÁøkÕ¸Ä]QDYU–* ZhÙZZ P–Ò²”²/Ç{^‚Qé ¯ êo2„¤o:gî=÷Üs{è³6ÿX;zØ8wæ¤qþìIãÜi¹Îœ0Ξ:n9|È0øo^—rÚ‘UÖ‹ª¯~8†B˜™[ÁÎÎÌâ¾öÊŽühtLÀ\ÄúÆ–)8~@ìsÑ5xóhíľ‘߉âzÚÜS˜ Ç,³·²¶ _pA+û2Œ'å.\yÑñ Ò˜,¨Â78Y†Yq7·¶˜‰*p¥ ^Ü/vhQ3eÀ—ž·#ÇæÆ—îq`Û„82¹ˆ†Îr«úp9÷×ÌéöQR€ot¡¼É‡>«ë›¦ ͯ¨ æ½ØSZ]€¿ŸÓüø]¯dgÁÐ’i™™ÜÀÌjÛÇQ¢WâD[ήp£¹'ˆÈâªe'3“y·°[‹‡‰‚üùüÀÞö ÉTRb[Û;p ‡ñÜÖwà ÷¾ž×‰š–LÎÆ,AòC6•}`úd3^%Œï,ØÁ:±[G¶hc*%µº—Êq¯È§Bź!T6ûQo Õ5§gCcs0t€îžy'bž XJßáwv… E¤ZªÙäÂá axbAc Hpxa5­#J¦r*ÝŠ¿ü¿ä“6f£#€o®I°j>dË«j¼¦+œ®‡¦ò=ÿ§’={Óšar5¶²i1=l&r™£¹®cLu|©pœ&üçääÅ> 3èàÕW v1›È;¾€n1Úô³UÒí…âÌ(uTªA2*²{›z¿0ÓQeŽÌ†t8u¯BŒ PïŠ.^ÎM ˆÎKÐbꨉ¶ùÑyh2gn¾îB×`h¿Äªáæö‡~ã0{‰Œ§ÚB66­§ì¸¸?Úµ0óIÿ Ô‰²Æä&™nf¯¾´ãíg¯Úå¬f W)Ró^œ-D&zîN»o¡ @çW¦7ÂOš}³l’»Ì$Õä~±Ó²qÓÎá‹9mȹ¢?öËJD/¸-Š™±ÉzÏ FçåÓF‰L¡½„ÛFL¼ƒYP©D¯D‹SÚé¡©C±ÿ(ì‰`I…YD—×Õð`oÜ.HÏ6bI‰kÒ$G‡Æ_e8Á¶MJLÐOé©nñ#óMv™uéðƒì^N+ú]®GäW¤ý„œgœâi+›}Jeyp²g –M'Øá£SQ´ˆ7à/9·Dð“}h*÷Å5?7!ë¼½Zišþh—ʃÒu¯ÁÍÓØ=!nÊ‹‡Â½\¼—üÜqç©S‚.IEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/logo-viewvc.png0000644000175000017500000002067211067250275030336 0ustar cmpilatocmpilato‰PNG  IHDRâ>6ìǃsRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarePaint.NET v3.36©çâ%!IDATx^í]`ÇÕvzuzïNïÕN³ãT·ÄÉw°)¦½ƒ%$Tê]‡º„¡Þ»H!@4ÆvÜ‚8ÆÆl„àûß7§‘öV»wN@±wÅp{·»SÞÌ7¯ÎìW8‡C‡ 8p(àPÀ¡€C‡F 4õ®CeWZz‹àPÆ¡€CËLÎ#Å(ïp¡ 6µ](/sw8Å¿Ù)°å±uX¿% YµA(ß’‰žƒí§|³ §ý—‡OT£ñpª€1Ù•á(ªÏpÀxyºÂ)Õ¡ÀWtÍDVÛ"¤W/FFi(š¶–9€t†CËAÏç pûr¸j"»"åÍù/GG8e:è}IôÆK‘R7 ®Êå(¬NsÀè ‡—ƒ{Nå#»k6kg £b%ò+’±s—ÈËÑN™on t>ŸŠµ3\7SôÆåXW•Šî=ßÜÃÂiý¥¦ÀÁWËÐüô¤·OEJ}²ªBQX±Öâ¥î§<‡Ÿ©@Õ#AHmŒ´ÚȪŒÀúÊlŒÎÐp(p©)pè\rzf ¥iÒª!»$µ›0^êŽpÊ{sSà¡s…h|6I­”¾èªBVQ,:·µ9`|s7Fë÷?×€²îddŠî•Q*b_8²ªÃ°¶"D¥ÌŠ0wª\åN†ïYå«‘U‡¢Š¼‹CÃîäT¯†«4ée+±¶€Õ93ž;]Ê•sùŒÈá§»œˆüiËŸŒðu¼n4BŠþŠ7Þ‚ÅÙ7cqò݈u…¢¦¶æ‚ʾw÷=wãª/_…Ï}þsøÂ¿€¯~í«øÆ7¿¡Ò׿ñuÜ7æ¾WçKA§  ¸s×ÎUg —™Oæ‰x9«ãBpüä 8wþÎö÷ãL_äýçÎIêL}çÿ­@ò Žâ8zpàô:,ÉQ1û&DÕŽFRÛx$´ŒAbëX$¶Ü俉Hlœ„ä†H­™‹ T{c–`Ï=è;߯’*c ¬sçÏüÞ@~—’^óσxåˆo9¿@dÙ(¤UÏs KGÖjŸþì§^øÃýÛ»·;@¼ÌcÄ¿¾!AQóи¹F¿û y¸ÿwý8/ÐàßYãqáR‡ð2ºP¸kæf^‡•Å· ¶ñĵŒFTÃ]XS§%Á™Ö<qˆ1(,ÉÁ+¯¾bÈÙ}zÎ#¹Ë!G|H9;ÐöL æg_…Y×#©ñ~qòÏP±¨y%ièÚ1rÂßnùã-xïûÞ‹w¾ëx÷{Þ|ðøà‡>ˆ}øCêü†op@8âP0*Ôñh!Öd.DlJ8N¾ö Ο;þ³„TÒ‡Š”äTO {ñ7”`aÞuX{-ÖÔÝ†Øæ»Ýx§ñvÄ·ŽR`Lkž%–ÎYXˆ]{{†Ð\AØ/BéüK¸á>ù¿U¸ëdÌN»‘•w Y¸mRÃD¤VÍ¿b­Ço„UUWauäjÌž3÷O¸·Ý~›J´|/Z¼)©)hjnúϽ¶G3š¸éY‰xþØóÂ…ºÿ©Cx¤úâ¿W<¿‹Ø¸»O‹+dý0'óg¾»ãZïB\Ó}Ê,î•Q‹°ïà>µbŽnÝð þ)béãÄ}H¬ÀâôÛ³u‰j4uÐøæûX7U| a((sYãÇ?ù1>þ‰ãÃù0Þåû•hH‘ñï|ÞòÖ·(]îú£WBR”¤XÉ<Þõîw)q“Ïñû•¸R‰¡½í¯ƒy0?^ã=oÇÛÕuÞÇÄòo¾åfŸ—îJǯ®ÿ•ª3ë«Ëeï{ÿûT^o}Û[Õ5æOÑ÷#ýnºù&äåÛûv?û¹Ïª¶3±~¤óc9ûøÇŸo[·)S§¨2™ô³|þÞûîµ}¦¤´ýØGU»ùIÚ±¾ÌƒßYŸ¶vw`ÆŠW(:ë~â=¬Ûζ±t um³WCTýÚ¥éÆçH/ö Ëüôg>í•þË–/S®&­^°™H3æùž÷¾G]c9o{ûÛÔwÖ*ɺÂu>ûÖ畺i¥RcKW»mÎ øÜúÐ@â[ÔáŽ)Ññ94"{ÇtÌt] ’›”HÊ”Ô4 1›°<ö¤fÅâ̹3MPçäÖBpŸÀ›ºáIÀþçêð`òýX³aú`#à4Ü$5LQˤòJ“ÑÕ³eHTíJÐÈìZ`µo¶Þ+§º¦ÚÝqîv ";#,Ø»§×ã_ƒÈ_ï£eäÖYÙYJijˋµ±©qØš8i¢WÚF y l iÐ2b*1)ÑçdDÚÊà•”œä³Ÿ¿û½ï“~”j⥞{íÊ_‰5±k.ŒU»D—[(J(*gES3 Bw:=œ§ÄhÓ®ñX{ –¬»ÑU°2c"‚¢ç+±QsCï@¤kä_"–>&ùí’­#¹hXc¢Ä$–ÙÄÆ‰H«áþ6Q(¯Û`ÙhŠ{ZgóJTÓ,K%üSŸþ”×Nâ`ªo¨÷(÷ç¿øù0Žhä¼¾»w÷à3 J?óÕá_üÒÁ‰aò”ÉÊHCÝŒyQÿ²zVë²Ô‹Öop³¨}ç»ßñZ–•žÈP=oõ#÷íìêô CZºlqâe€S£8jfnv"°1¯ÅK{N‰œ^ÊcÐèb,ùx“^|õ‰×ëo½W}í*4··^8÷+EbA0V¬Z€ý‡ö*ß¡6Öœ;Vm´h9FÆÅüS‰‘/ áUw`¶ë·ÉåqS‘èZƒ£ÿ<ª€Øw¾oÀ&¡Wòæ1äy‡^nDDÆ|„­ Ö”-c”ø›Ô4鵋$`=¥5ö1¯4bx#ÅOr óìL™ßøœ‘kQ»ó®;‡ÕÍŠ#2ý,E+­±¼I“')#†Gä³¼‡€µ’È223põ5W{mŸQ/¢õÏL Zþ>íiåÐr¨l³)Š™uR%)x†Òç,º{|@‰°¦Ã¬ß™óøÚ׿æAÃîÝøÁ0X–¹¯Ï³OçΛ«ÄÍðˆpPOýÖ·¿¥že¿é¤Ÿ1´h„òi ±º¡¢+ÆŠ[  ME¹èãl¿ÑÐ2$bÒ Ãh›Sb\yIï›ÿ‘€ÙI7`qÌ8áfóQU_¦"sÜ t;êÝÜQç$FÒ¸øŒ¢!¨Ë¶¦!pÍ\ËFˆ±-wŠ C‚È«Ö`S¥ýŽoÏhݲë`vÂ=£îñ(‹–P#'3…œ¦pýpðÿâ—¿VŽ©Ã~ÿßW+ƒ³°Æ¾s­uáŸü„׬Ãê(ÂÑŠ¨iÁv¦"ƒ 9°ÑÊj§Ã¾w´Çsz ZÝÏö*}Õæ uÔ6† æä{´ÉêYºÌ­q²3ŒÑàd墨­«í¾& ŠÃćŽW#¶pÂcW`ï¾^7§b˜Û€ûbˆ#›ÇxÑÓÂ_Áñüµ!¡j–GÎ@Dô*:w–ýäzˆîó!s[Ìý·Ä•¾„Ýx팛…˜äÕÖ@Ü:qÍ"š6S@ÌÑ´¬Ö»/‘Ö=oDûÞ÷¿çQ–Yä3ÎrÌGéž5ærÌ@Ô:bfV¦-µuΪ~!«B|úª*«*¡:Ü Ò33¹ñW¾úNÀúiŽ &,ÃA‘˜õó%Â)£ØÀAÑ›®+i‚täD`¥‹êç—,]âá*±j—}+.o|†€£ÜØ.Šözrµ£ÿo÷[¯ ÚT¼I‘* 'ª@ìO–ÇöSçi”€.”¥=QX±:@-q:yêe¥+┈"¢>«´»+FtrˆÈ DƱº-±îû@t[JËóOâE &/îŽÂʘ¶DHÚ|ŸbRã8¸j– èˆÞß¡A¬·J#9 U°®Àã^³ÿŒÄ¦aÁЍÚý`,ˈsÈm˜¿hÄŽ¤YŸ¢—WQ·úæ·¾©Î™8ЇPV³;ý\º¾FQ]·M—OîHq”÷R·äàò6Qèvò9-!Ð|oä6¼GOdüT“¢—ƒã†“…Ùgi¤éíwÜ®òà ·’BŒ÷*—’éà„iÌߊfÊäã žËv“+3QD§šCK3ÇÁê+ÛëOV!,M¸bL =ò@FS‹ÖPjŸ@é´hŠ'Ï=‡'Ž>ªbJµHª}’FñT3Ð\Òׄž`p÷¿Û±:{bÒ‡|sÆŠæí /<Œ9uÕ,SVӪƯ ¦/Ì<8ŒÅ_ëGJ÷à.Zþ7Åèÿ2‘3àL jޏpÑÂA úã«Ò"$Ák/ñˆ×)ëº2ZÄØ>#PXFÐJ·^N=ú!i ÅS¨>ù©Ok#¯k)AÑq€~f 3/³j5Ç789inlì%ŠËA':Õo40rO]%c~æ‰í¶ÓË/Xó`éÖd,xK×áµÓ¯f o(8œ`#oâx¼Ö¯´B·ÑGŽÃåúD$}Q8éAqYìAÅöT,ža ªŒî)H1NbZÝ~ÄåÊØØæ)zXµy츱¶ÆB¥¼¼¢Ÿùìg<‚cæ¥Ë–ÚÖÏ ˆFŽÈÙ[[MiÐ×ü¢?€³»GY>‡IŒÕí# hÒׯ 3—¥…”õ6[¢ù< "eåe0˜' æÏ{|IrcÝZ‡6‚…å‘cÍ ˜é„tY­v¡dáÍeDñ¹µí",ž¾Æë­[üÏxç³%ÏP~Å¿=õ·ÎÓù`Œ¼2½ Ý3<šTÿâÖ)’ž^È5Ž/‹nxäµÄç?ˆ 5ÃÍÓº}é]ˆãÔ*ÄúÉȨ V‘5›»Z|v.AfÖ]Œ—"'ã8feäšð7Љ=;{l˲Ze戈tâ3Oo¢¯qv¦î¦ÃµØº|"¨m¹°E´m¡X€õ%…Qëx£ç€àjÍñŒ÷3ˆ\aq\Œ¬âB}8šÍd´*Rdò6ŽüM5°­˜1„® Z}#×Dªd®ä`b€^Ù¡ãwþΪ>ù E3³{Æ(>šéAà «9A¦ë§ðf'¿¦uU³ `ŒÊQ†?ú€u¹v®oÖpêÒf#.ZÅüúpäû¡©îp+¶—md²â²Ã8kkO%ºwoõ›]ÏGLö ¬N Á³Çžt:ô¥»Á}hHY­(4 ±žë7ÜOéÀq®âxV\± ±"ÂÞH³¶kЬ¼`oIñÍc_=k‹#±¡,½ûwùÕ.:¬ÏWg¯sùZéàk…>µ£g‡ª+y»XV]®/ÆŸczØm6nði 5Ó†œW[Yw£Ä:r rò¹:sŸò¶ºÇÛ¤ÉÕ,¾ò¥D@‹´]}ý‰må˜f5Ý÷÷6”×_ØfOë[â¿@ÜEƒ@t»"|ÑÓ_hâÐÂ߇UàxëB,›âµ“ÔÊıHhÎX=Om—Q^sakü¬,›¾:…×;2«òå\§jŒ¬agzs&³\_33c6)Ò²ª­«4fЬN–VUëËÙ\íŸãgxgz³/Ο(?ÛH±”˺.ˆ®/úXµVpmýµ*¯´¬Ô#rÊN_$(^ÒpC#'QŠ£¾ÜEºNfš Öecuºwmó›»þQŽTyEF4Ž>sT¡¯ï쇄O£è9U:\wÔt/üåz×%œ-&g™¬þÕ6GjÇDHÎ…Æja°ˆ¥iUâº(JBS›uÔ‰]^ ææìlÔÝ|Åz²cý‰ª·ÒµÁ‚ep0‘‹ˆuäŠ(Èy®ÿõõÊšÉ%R4s-õŠÂ´Ä.r@3=È1´èK_"½XWcô÷±<>ëÍϨuLãr1ÁÈoÖg½MºÊEããàŽ{vyiQä¦ñˆZ£ÎkÔñÍ6”¼¬,¶ªZõÛÖÉBÚ [7•ݤ–HµniV1¨\&5ü0/frÎʈã^øû² îæ*ÿΧò±*a!R3¬}s›/ÊÚF DY­Ï=p¸꺒µØµÛÞxbÕhEÔ3­¡™øÆN¢s—\ÄWçšuD­_éN¢ÿÊ,ª¨¨ž£.“u1.µâ€àw‚’: K¼ÏhUÔÏëþç¿üÙ²ÞÔ-µxi\úy= ór1m´>¨u8» bžZ–v æ‰Âè1–©A¯|Á>PZ`[u;Ì–Y#G×n¶ÕìwÔýlüÝ*r°J{ŽÖ ·2õmþo/±ó¹õIš‡¸TwܨÑZêÉíŒ3ž|Ó‰ÃÝàŽOÜäâ%O°ÉKí¨ÚÉ ïU¢irãTµü)«8FÄÒ> nÕœÍÍþ8;®@ñ„Æ_ËëfÑÔÈq˜¿Ý2žoºÑ«¦-«æp8ý»¥^˲Xor†ÒYÕÀb¸ÔÞ8¾EW…?˜äý¡'Û.œÌhÕÕíPî?stÞÆþ7rx]†q¼'*½pXOìö§Ý«VÖ•ˆìM è=à?'ɯ‹Æ’U⯫õˆÚ”ãÉ;Uð·pCnÉxJÖv=V„ÐTÙF#;Á’xÝÓÄ/Ûb“ÚÄ(‹‚“øÞÄÒpÙ×Ô…ŽnëE½¾úÜâ¦æDV‘šØÔèWó•'¯[‰¦Æ›Ó¼ ŠÏ‘KÒìí+fÔÌ©ìÀÃ|è׆!»ºÓgèK#×4¯mÔù1ÏŽ›ëfÝâ=õ=Zj°j¯@´¢ÚE=Ù•G]’;1Ø­¹4–©WìÓc‡t øJµU+óãêÕØªüß ·ý°l½˜4K¢mV*¿§Èi!©sÌ"ª{á/»Ÿ•ýn jc%8ÜÚRºá¡yî(®ö0ª­eéSrÕ\d'bSÙÅqCMT-òi=˪£éÀöwИ¨EE­'^÷«ë¼æ•›— †oyówzã\t¸3ÂŬÏÙÕŸþBú ¾Bsþœˆô–æ|Èå(Þy㪜ˆ¬–.ùKSÞGºX¹*8ø Ò™uàDçkò±*—Ü‹îö·,3m´~Lr‡«lÛyät6uEbmÙ*×åú5ÈŸª—wR<ˆea³e“©-Ç…'ÐÌÔË¥Üü‘+,¨>¥Vj(‘7~Ò#‡ÕaÓÁEHÚ"ÛaHJÜ,»ÁµÉ'÷Jå‹KËW  Ø5l Ü…t,ï¥Ñ„>h¡kƒ;­qög§p6þv!A»£˜­}̇çŒ!0X¯ûSOºIèdp2ÅGroc„ŽÖk¨ë2Z‡¢#ó窣1ÈŸ²&Æ:¦¦¥ú»ÏÑÂè-Fʰ}Ll7}—ô{òœùÆþè×¾êJ_(õZÒU÷¹ëÍú³ýuõu~Ñ×®,®ê(ÚT¤6‡¢´Àx^NŠfcÝN¤;Es¬\t¹=Çr‘ÑÈw†£¤Þ¿—Ép“©Õ®%HÉŒÃñSÏ ´Ü „ÝAl:1ÄmhÉ”ûºû7÷êûg$"õaÅù&à (ë-û¹\|›ìÚ.Ûn´ßƒáŠ)MS$¶t…ìÜʆ sYøêäÿ•ëy.hÿ+méõ$H/dRö«=œ+GÕá0÷2¢ê”7Û¯çÓzµ®²•Mš‹Æ®M8Þ÷4^”ôÒ¹§ñBßxñ¬¬ž8û¸|/ý;Nœ{Çûeõ¾$uÏùG𼄲E'zŸ+Dz„ñˆÎ’0/Ó‘Ò!«+Zï¢pÃy-[zýl©Å_•# ž+Âýj´s“C‘HÝ'rQ¸s)Rç §>R}²õæÃiX˜tVÉ ü„ÒYH,›¸’•âå{|™|JJ®˜‹¹–P> q¥²Ó·¤ø²²Çé4u)+,‚’uáôC'°ãù¨<¬­³„óNVï¯HÙ<Î)35£"gåZãtµgiNE,ªÛޜґ3:œš\R p“áÖ§‘»}¶zšDË$ÊÔ†Yà‹M3êÕbÜ­q¨Þž…Ö}Eè<\…žÇë°ëÉ:ìx¢Ûþ¶ mGrPµ7+Ö"/Ž‘m-Å9ß0Z6c ß]!\Oƒ1®IôC¾XFÞ©˜Ú(kÅM‘_‡Úvÿün—”PNa.ºžÍEQïRqŒSÑ,Lä’\z”Ð0A^,3Y­~p§é*‘{êsõ»¼—‚Q0 ²å!AçÎGô?¾C|„Ü2_'õE‚°q2Òë"£R^@Z†¶®×眽´rÊp(ð_¥ÀžJP÷p426 ÀL Œ­÷ÊûG©Í}èÊoê=ˆ\A  $¾$Fn é€mFÈðwn™OINÉ î´ºµ#[F¹ø4[ ѽǿ÷ÿU"8™;)Øõ\ Ê„#cëL%ª*.&7)@‰u“nwºÛ#%mq_c 6- L꜋{›ä\6J«§VRäUÇ¢¬±»ýßWg¤ÐÈ©‡CKBýÇÄ ñtÊ÷E o›ìÓ2ií“”£Ý 21° Ò Ì¤v&ÑûÚÄ-!/+Mn=³u*Ò[gˆÏr–Š’!,¨Fyk¶îð½ùÓ%i¬SˆCÿ ìx²-»PщüÎ%p‰èšÞ.¢eûI÷«¤"aÔwáxm²’^ÀçjÎW¿Y5Á¾”µd¢uG¶íÝì3šç.N \6 ô>S…íOoƒÁë$ r·¼F»'EÛC±¾S,«ÛÂd×îH”÷$¢vWÚö¡ûPö<²Ýßeë5§`‡ 8p(àPÀ¡€C‡Þäø³Å9µÖoÿ IEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/images/broken.png0000644000175000017500000000036710615044233027345 0ustar cmpilatocmpilato‰PNG  IHDRíÝâRPLTEkkkŒŒŒÎÎÎçççÿÿÿ999F‹…/tRNS@æØfbKGDˆH pHYs  šœtIMEÕ  ÷w [IDAT×c`€P P1LCCƒ`Œ(Ã8ÂMKƒ2Ba G #Ð54$Ä`Ht© `L`2TY@ŠXX@ŒPW„v#HÄ'K!©¹°„IEND®B`‚viewvc-1.1.22/templates-contrib/viewsvn/templates/docroot/styles.css0000644000175000017500000001611711677106750026163 0ustar cmpilatocmpilato/*******************************/ /*** ViewVC CSS Stylesheet ***/ /*******************************/ /*** Standard Tags ***/ html, body { color: #000000; background-color: #ffffff; font-family: arial,helvetica,sans-serif; font-size: 12px; } a { text-decoration:none;} a:hover { text-decoration:underline;} a:link { color: #0000ff; } a:visited { color: #880088; } a:active { color: #0000ff; } .img a:hover { text-decoration:none;} img { border: none; } table { width: 100%; margin: 0; border: none; } table.props { width: 50%; } table.auto { width: auto; } tr, td, th { vertical-align: top; } form { margin: 0; } hr{ color: #eeeeee; background-color: #eeeeee; height: 1px; border: none; } h1{ font-size: 24px; color: #000066 } h2{ font-size: 18px; color: #000066 } /** Navigation Headers ***/ .vc_navheader { background-color: #e0e0ff; font-size: 13px; vertical-align:middle; } .vc_navheader td{ vertical-align:middle; } /*** Table Headers ***/ .vc_header { text-align: left; vertical-align: top; background-color: #e0e0ff; } .vc_header_sort { text-align: left; background-color: #e0e0ff; } /*** Table Rows ***/ .vc_row_even { background-color: #ffffff; } .vc_row_odd { background-color: #eeeeee; } /*** Log messages ***/ .vc_log { white-space: pre-wrap; background-color: #eeeeee; } /*** Properties Listing ***/ .vc_properties { margin: 1em 0; } /*** File Content Markup Styles ***/ .vc_summary { background-color: #eeeeee; } #vc_file td { border-right-style: solid; border-right-color: #505050; text-decoration: none; font-weight: normal; font-style: normal; padding: 1px 5px; } .vc_file_line_number { border-right-width: 1px; background-color: #eeeeee; color: #505050; text-align: right; } .vc_file_line_author, .vc_file_line_rev { border-right-width: 1px; text-align: right; } .vc_file_line_text { border-right-width: 0px; background-color: white; font-family: monospace; text-align: left; white-space: pre; width: 100%; } .pygments-c { color: #408080; font-style: italic } /* Comment */ .pygments-err { border: 1px solid #FF0000 } /* Error */ .pygments-k { color: #008000; font-weight: bold } /* Keyword */ .pygments-o { color: #666666 } /* Operator */ .pygments-cm { color: #408080; font-style: italic } /* Comment.Multiline */ .pygments-cp { color: #BC7A00 } /* Comment.Preproc */ .pygments-c1 { color: #408080; font-style: italic } /* Comment.Single */ .pygments-cs { color: #408080; font-style: italic } /* Comment.Special */ .pygments-gd { color: #A00000 } /* Generic.Deleted */ .pygments-ge { font-style: italic } /* Generic.Emph */ .pygments-gr { color: #FF0000 } /* Generic.Error */ .pygments-gh { color: #000080; font-weight: bold } /* Generic.Heading */ .pygments-gi { color: #00A000 } /* Generic.Inserted */ .pygments-go { color: #808080 } /* Generic.Output */ .pygments-gp { color: #000080; font-weight: bold } /* Generic.Prompt */ .pygments-gs { font-weight: bold } /* Generic.Strong */ .pygments-gu { color: #800080; font-weight: bold } /* Generic.Subheading */ .pygments-gt { color: #0040D0 } /* Generic.Traceback */ .pygments-kc { color: #008000; font-weight: bold } /* Keyword.Constant */ .pygments-kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ .pygments-kp { color: #008000 } /* Keyword.Pseudo */ .pygments-kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ .pygments-kt { color: #B00040 } /* Keyword.Type */ .pygments-m { color: #666666 } /* Literal.Number */ .pygments-s { color: #BA2121 } /* Literal.String */ .pygments-na { color: #7D9029 } /* Name.Attribute */ .pygments-nb { color: #008000 } /* Name.Builtin */ .pygments-nc { color: #0000FF; font-weight: bold } /* Name.Class */ .pygments-no { color: #880000 } /* Name.Constant */ .pygments-nd { color: #AA22FF } /* Name.Decorator */ .pygments-ni { color: #999999; font-weight: bold } /* Name.Entity */ .pygments-ne { color: #D2413A; font-weight: bold } /* Name.Exception */ .pygments-nf { color: #0000FF } /* Name.Function */ .pygments-nl { color: #A0A000 } /* Name.Label */ .pygments-nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ .pygments-nt { color: #008000; font-weight: bold } /* Name.Tag */ .pygments-nv { color: #19177C } /* Name.Variable */ .pygments-ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ .pygments-w { color: #bbbbbb } /* Text.Whitespace */ .pygments-mf { color: #666666 } /* Literal.Number.Float */ .pygments-mh { color: #666666 } /* Literal.Number.Hex */ .pygments-mi { color: #666666 } /* Literal.Number.Integer */ .pygments-mo { color: #666666 } /* Literal.Number.Oct */ .pygments-sb { color: #BA2121 } /* Literal.String.Backtick */ .pygments-sc { color: #BA2121 } /* Literal.String.Char */ .pygments-sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ .pygments-s2 { color: #BA2121 } /* Literal.String.Double */ .pygments-se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ .pygments-sh { color: #BA2121 } /* Literal.String.Heredoc */ .pygments-si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ .pygments-sx { color: #008000 } /* Literal.String.Other */ .pygments-sr { color: #BB6688 } /* Literal.String.Regex */ .pygments-s1 { color: #BA2121 } /* Literal.String.Single */ .pygments-ss { color: #19177C } /* Literal.String.Symbol */ .pygments-bp { color: #008000 } /* Name.Builtin.Pseudo */ .pygments-vc { color: #19177C } /* Name.Variable.Class */ .pygments-vg { color: #19177C } /* Name.Variable.Global */ .pygments-vi { color: #19177C } /* Name.Variable.Instance */ .pygments-il { color: #666666 } /* Literal.Number.Integer.Long */ /*** Diff Styles ***/ .vc_diff_header { background-color: #ffffff; } .vc_diff_chunk_header { background-color: #eeeeee; } .vc_diff_chunk_extra { font-size: smaller; } .vc_diff_empty { background-color: #c8c8c8; font-family: sans-serif; font-size: smaller; } .vc_diff_add { background-color: #ffff00; font-family: sans-serif; font-size: smaller; } .vc_diff_remove { background-color: #ffc864; font-family: sans-serif; font-size: smaller; } .vc_diff_change { background-color: #ffff96; font-family: sans-serif; font-size: smaller; } .vc_diff_change_empty { background-color: #eeee77; font-family: sans-serif; font-size: smaller; } .vc_diff_nochange { font-family: sans-serif; font-size: smaller; } .vc_raw_diff { background-color: #eeeeee; font-size: 12px; } /*** Intraline Diff Styles ***/ .vc_idiff_add { background-color: #aaffaa; } .vc_idiff_change { background-color:#ffff77; } .vc_idiff_remove { background-color:#ffaaaa; } .vc_idiff_empty { background-color:#e0e0e0; } table.vc_idiff col.content { width: 50%; } table.vc_idiff tbody { font-family: monospace; /* unfortunately, white-space: pre-wrap isn't widely supported ... */ white-space: pre-wrap; /* CSS3 */ } table.vc_idiff tbody th { background-color:#e0e0e0; text-align:right; } /*** Query Form ***/ .vc_query_form { background-color: #e0e0ff; } /*** Warning! ***/ .vc_warning { border-width: 1px 2px 2px 2px; border-color: black; border-style: solid; background-color: red; color: white; padding: 0.5em; }viewvc-1.1.22/templates-contrib/viewsvn/templates/file.ezt0000644000175000017500000000623111233104040024066 0ustar cmpilatocmpilato[# ------------------------------------------------------------------------- ] [# CUSTOMIZE ME: To avoid displaying "binary garbage" -- the contents of ] [# files with non-human-readable file formats -- change the value of the ] [# hide_binary_garbage variable below to 1. ] [# ------------------------------------------------------------------------- ] [define hide_binary_garbage]1[end] [# ------------------------------------------------------------------------- ] [# setup page definitions] [define page_title]View of /[where][end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "markup"] [include "include/file_header.ezt"]
Revision [if-any revision_href][rev][else][rev][end]  [if-any download_href]Download[end] [is view "markup"] [if-any annotate_href]Blame[end] [end] [is view "annotate"] View [end] [if-any orig_path]
Original Path: [orig_path] [end] [if-any mime_type]
File MIME type: [mime_type] [end] [if-any size]
File size: [size] byte(s) [end] [if-any log]
[log]
[end]
[if-any prefer_markup][define hide_binary_garbage]0[end][end] [if-any image_src_href][define hide_binary_garbage]0[end][end] [is hide_binary_garbage "1"]

This file's contents are not viewable. Please download this version of the file in order to view it.

[else] [define last_rev]0[end] [define rowclass]vc_row_even[end] [if-any lines]
[for lines] [is lines.rev last_rev] [else] [is lines.rev rev] [define rowclass]vc_row_special[end] [else] [is rowclass "vc_row_even"] [define rowclass]vc_row_odd[end] [else] [define rowclass]vc_row_even[end] [end] [end] [end] [is annotation "annotated"] [end] [define last_rev][lines.rev][end] [end]
[lines.line_number][is lines.rev last_rev] [else][lines.author][end] [is lines.rev last_rev] [else][if-any lines.diff_href][end][lines.rev][if-any lines.diff_href][end][end][lines.text]
[else] [if-any image_src_href]
[end] [end] [end] [include "include/props.ezt"] [include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/viewsvn/templates/error.ezt0000644000175000017500000000101710403112407024301 0ustar cmpilatocmpilato ViewVC Exception

An Exception Has Occurred

[if-any msg]

[msg]

[end] [if-any status]

HTTP Response Status

[status]


[end]

Python Traceback

[stacktrace]

viewvc-1.1.22/templates-contrib/viewsvn/templates/include/0000755000175000017500000000000012265242273024065 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/viewsvn/templates/include/file_header.ezt0000644000175000017500000000263211170155650027037 0ustar cmpilatocmpilato

Parent Directory Parent Directory [is view "markup"] [if-any log_href] Revision Log Revision Log [end] [end] [is view "annotate"] [if-any log_href] Revision Log Revision Log [end] [end] [is view "diff"] [if-any log_href] Revision Log Revision Log [end] View Patch Patch [end] [is pathtype "file"] [else] View Directory Listing Directory Listing [end]

viewvc-1.1.22/templates-contrib/viewsvn/templates/include/log_header.ezt0000644000175000017500000000244311040367645026706 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Log of /[where][end] [define help_href][docroot]/help_log.html[end] [# end] [include "header.ezt" "log"] [include "file_header.ezt"]
[is pathtype "file"] [if-any view_href] [end] [if-any tag_view_href] [end] [end]
Links to HEAD: View [if-any download_href]Download[end] [if-any annotate_href]Blame[end]
Links to [pathrev]: View [if-any tag_download_href]Download[end] [if-any tag_annotate_href]Blame[end]
Sticky [is roottype "cvs"]Tag[else]Revision[end]: [include "pathrev_form.ezt"]
[include "paging.ezt"] viewvc-1.1.22/templates-contrib/viewsvn/templates/include/pathrev_form.ezt0000644000175000017500000000154511040367645027313 0ustar cmpilatocmpilato
[for pathrev_hidden_values][end]
[if-any pathrev]
[for pathrev_clear_hidden_values][end] [if-any lastrev] [is pathrev lastrev][else][end] (Current path doesn't exist after revision [lastrev]) [else] [end]
[end] viewvc-1.1.22/templates-contrib/viewsvn/templates/include/footer.ezt0000644000175000017500000000154411677132210026106 0ustar cmpilatocmpilato[# standard footer used by all ViewVC pages ]
[if-any cfg.general.address]
[cfg.general.address]
[else] [end]
Subversion  TortoiseSVN  ViewVC
  [if-any rss_href]RSS 2.0 feed[else] [end]
viewvc-1.1.22/templates-contrib/viewsvn/templates/include/dir_footer.ezt0000644000175000017500000000171211043271104026732 0ustar cmpilatocmpilato[if-any where]
[# Enable this section and replace protocoll://svnparentlocation with your SVN parent URL location (e.g. https://server/svn) to enable direct TortoiseSVN checkouts. This works only if all your repositories have the same location, e.g. if you use the SVNParentPath directive in mod_dav_svn ] [if-any tarball_href] Download as GNU tarball [end]  
[end] [include "props.ezt"] [include "footer.ezt"] viewvc-1.1.22/templates-contrib/viewsvn/templates/include/props.ezt0000644000175000017500000000115311063544311025745 0ustar cmpilatocmpilato[if-any properties]

Properties

[for properties] [if-any properties.undisplayable] [else] [end] [end]
Name Value
[properties.name]Property value is undisplayable.[properties.value]
[end] viewvc-1.1.22/templates-contrib/viewsvn/templates/include/dir_header.ezt0000644000175000017500000000272411201220304026660 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Index of /[where][end] [define help_href][docroot]/help_[if-any where]dir[else]root[end]view.html[end] [# end] [include "header.ezt" "directory"] [if-any where][else] [end] [if-any search_re] [end] [if-any queryform_href] [end]
Directory revision: [tree_rev][if-any youngest_rev] (of [youngest_rev])[end]
Sticky Revision: [include "pathrev_form.ezt"]
Current search:[search_re]
Query: Query revision history
[is picklist_len "0"] [else] [is picklist_len "1"] [else]
[for dir_paging_hidden_values][end]
[end] [end]


viewvc-1.1.22/templates-contrib/viewsvn/templates/include/paging.ezt0000644000175000017500000000152311201220304026033 0ustar cmpilatocmpilato [is picklist_len "0"] [else] [is picklist_len "1"] [else]
[for log_paging_hidden_values][end]
[end] [end] viewvc-1.1.22/templates-contrib/viewsvn/templates/include/log_footer.ezt0000644000175000017500000000014710617121637026751 0ustar cmpilatocmpilato[include "paging.ezt"] [is pathtype "file"] [include "diff_form.ezt"] [end] [include "footer.ezt"] viewvc-1.1.22/templates-contrib/viewsvn/templates/include/header.ezt0000644000175000017500000000374611756774520026064 0ustar cmpilatocmpilato [if-any rootname][[][rootname]][else]Subversion[end] [page_title] [# set your favicon here ] [if-any rss_href][end] [is roottype "svn"][else][is page_title "Repository Listing"][else]

Do not use this template set if you plan to view cvs repositories through ViewVC! This template-set is heavily trimmed for Subversion needs. Special CVS features are untested, possibly disabled or broken.

[end][end]
[# insert your logo and link to your organisation here ]
[if-any roots_href][[]Repository Listing] /[else]/[end] [if-any nav_path] [for nav_path] [if-index nav_path last][else] [if-any nav_path.href][end] [end] [if-index nav_path first] [[][nav_path.name]] [else] [nav_path.name] [end] [if-index nav_path last][else] [if-any nav_path.href][end] [end] [if-index nav_path last][else]/[end] [end] [end]
  [if-any username]Logged in as: [username][end]

[page_title]

viewvc-1.1.22/templates-contrib/viewsvn/templates/include/diff_form.ezt0000644000175000017500000000211611040367645026545 0ustar cmpilatocmpilato

This form allows you to request diffs between any two revisions of this file. For each of the two "sides" of the diff, enter a numeric revision.

  [for diff_select_hidden_values][end] Diffs between and
  Type of Diff should be a
viewvc-1.1.22/templates-contrib/viewsvn/templates/rss.ezt0000644000175000017500000000137011424633522023773 0ustar cmpilatocmpilato [rss_link_href] [rootname] checkins[if-any where] (in [where])[end] Subversion commits to the[if-any where] [where] directory of the[end] [rootname] repository [for commits] [if-any commits.rev][commits.rev]: [end][[commits.author]] [format "xml"][commits.short_log][end] [if-any commits.rss_url][commits.rss_url][end] [commits.author] [if-any commits.rss_date][commits.rss_date][else](unknown date)[end] <pre>[format "xml"][format "html"][commits.log][end][end]</pre> [end] viewvc-1.1.22/templates-contrib/viewsvn/templates/query_results.ezt0000644000175000017500000000707611677106750026133 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Query results on /[where][end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt"]

[english_query]

[# ] [if-any row_limit_reached]

WARNING: These query results have been artificially limited by an administrative threshold value and do not represent the entirety of the data set which matches the query. Consider modifying your query to be more specific, using your version control tool's query capabilities, or asking your administrator to raise the database response size threshold.

[end]

Modify query

Show commands which could be used to back out these changes

+[plus_count]/-[minus_count] lines changed.

[if-any commits] [if-any show_branch] [end] [# uncommment, if you want a separate Description column: (also see below) ] [for commits] [for commits.files] [if-any show_branch] [end] [end] [if-any commits.limited_files] [end] [end]
Revision FileBranch+/- Date AuthorDescription
[define rev_href][if-any commits.files.prefer_markup][commits.files.view_href][else][if-any commits.files.download_href][commits.files.download_href][end][end][end] [if-any commits.files.rev][if-any rev_href][end][commits.files.rev][if-any rev_href][end][else] [end] [commits.files.dir]/ [commits.files.file] [if-any commits.files.branch][commits.files.branch][else] [end] [# only show a diff link for changes ] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [commits.files.plus]/[commits.files.minus] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [if-any commits.files.date][commits.files.date][else] [end] [if-any commits.files.author][commits.files.author][else] [end]
  Only first [commits.num_files] files shown. Show all files or adjust limit.
  Log:
[commits.log]
[end] [include "include/footer.ezt"]viewvc-1.1.22/templates-contrib/viewsvn/templates/roots.ezt0000644000175000017500000000231011677110026024324 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Repository Listing[end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "directory"] [is cfg.options.show_roots_lastmod "1"] [end] [for roots] [is cfg.options.show_roots_lastmod "1"] [end] [end]
NameRevision Age Author Last log entry
[roots.name] [roots.name]  [if-any roots.log_href][roots.rev][else][roots.rev][end]  [roots.ago]  [roots.author]  [roots.short_log]
[include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/viewsvn/templates/query_form.ezt0000644000175000017500000001310211277560233025354 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Query on /[where][end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "query"]

Browse Directory  Browse Directory

[for query_hidden_values][end]
Subdirectory:
(you can list multiple directories separated by commas)
File:
Who:
Sort By:
Date:
hours
and
(use the form yyyy-mm-dd hh:mm:ss)
Limit: Show at most changed files per commit.
(use 0 to show all files)
[include "include/footer.ezt"] viewvc-1.1.22/templates-contrib/viewsvn/templates/log.ezt0000644000175000017500000000330111043271104023730 0ustar cmpilatocmpilato[include "include/log_header.ezt"] [define first_revision][end] [define last_revision][end] [for entries] [if-index entries first][define first_revision][entries.rev][end][end] [if-index entries last][define last_revision][entries.rev][end][end]

Revision [entries.rev]  [is pathtype "file"] [if-any entries.view_href]View[end] [else] Directory Listing [end] [if-any entries.download_href]Download[end] [if-any entries.annotate_href]Blame[end]
[if-index entries last]Added[else]Modified[end] [entries.date] ([entries.ago] ago) by [entries.author] [if-any entries.orig_path]
Original Path: [entries.orig_path] [end] [if-any entries.size]
File length: [entries.size] byte(s) [end] [if-any entries.copy_path]
Copied from: [entries.copy_path] revision [entries.copy_rev] [end] [is pathtype "file"] [if-any entries.prev]
Diff to previous [entries.prev] [end] [end]
[entries.log]
[end] [include "include/log_footer.ezt"] viewvc-1.1.22/templates-contrib/viewsvn/extras/0000755000175000017500000000000012265242273021752 5ustar cmpilatocmpilatoviewvc-1.1.22/templates-contrib/viewsvn/extras/svnindex.xsl0000644000175000017500000001252112102660663024336 0ustar cmpilatocmpilato /viewvc/ <xsl:if test="string-length(index/@name) != 0"> <xsl:value-of select="index/@name"/> <xsl:text>: </xsl:text> </xsl:if> <xsl:value-of select="index/@path"/>
Revision
Parent Directory [ .. Parent Directory ]
{@name} {@name} {@name} / ?revision= /
{@name} / ?revision= &view=markup
viewvc-1.1.22/templates-contrib/viewsvn/extras/svnindex.css0000644000175000017500000000212410624001606024307 0ustar cmpilatocmpilato html, body { color: #000000; background-color: #ffffff; font-family: arial,helvetica,sans-serif; font-size: 12px; } body{ margin: 0; padding: 0; } a { text-decoration:none;} a:hover { text-decoration:underline;} a:link { color: #0000ff; } a:visited { color: #880088; } a:active { color: #0000ff; } .img a:hover { text-decoration:none;} img { border: none; } .footer { margin-top: 8em; padding: 0.5em 1em 0.5em; clear: both; font-size: smaller; font-style: italic; } .svn { margin: 3em; } .rev { margin-right: 3px; margin-bottom: 15px; padding-left: 3px; text-align: left; font-size: 24px; font-weight: bold; color: #000066 } .path { margin: 3px; padding: 3px; background: #e0e0ff; font-weight: bold; } .updir { margin: 3px; padding: 3px; margin-left: 3em; background: #eeeeee; } .file { margin: 3px; padding: 3px; margin-left: 3em; background: #eeeeee; } .dir { margin: 3px; padding: 3px; margin-left: 3em; background: #eeeeee; } viewvc-1.1.22/lib/0000755000175000017500000000000012265242270014052 5ustar cmpilatocmpilatoviewvc-1.1.22/lib/compat.py0000644000175000017500000001215712071623704015715 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # compat.py: compatibility functions for operation across Python 1.5.x to 2.2.x # # ----------------------------------------------------------------------- import urllib import string import time import calendar import re import os import rfc822 import tempfile import errno # # urllib.urlencode() is new to Python 1.5.2 # try: urlencode = urllib.urlencode except AttributeError: def urlencode(dict): "Encode a dictionary as application/x-url-form-encoded." if not dict: return '' quote = urllib.quote_plus keyvalue = [ ] for key, value in dict.items(): keyvalue.append(quote(key) + '=' + quote(str(value))) return string.join(keyvalue, '&') # # time.strptime() is new to Python 1.5.2 # if hasattr(time, 'strptime'): def cvs_strptime(timestr): 'Parse a CVS-style date/time value.' return time.strptime(timestr, '%Y/%m/%d %H:%M:%S')[:-1] + (0,) else: _re_rev_date = re.compile('([0-9]{4})/([0-9][0-9])/([0-9][0-9]) ' '([0-9][0-9]):([0-9][0-9]):([0-9][0-9])') def cvs_strptime(timestr): 'Parse a CVS-style date/time value.' match = _re_rev_date.match(timestr) if match: return tuple(map(int, match.groups())) + (0, 1, 0) else: raise ValueError('date is not in cvs format') # # os.makedirs() is new to Python 1.5.2 # try: makedirs = os.makedirs except AttributeError: def makedirs(path, mode=0777): head, tail = os.path.split(path) if head and tail and not os.path.exists(head): makedirs(head, mode) os.mkdir(path, mode) # # rfc822.formatdate() is new to Python 1.6 # try: formatdate = rfc822.formatdate except AttributeError: def formatdate(timeval): if timeval is None: timeval = time.time() timeval = time.gmtime(timeval) return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][timeval[6]], timeval[2], ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][timeval[1]-1], timeval[0], timeval[3], timeval[4], timeval[5]) # # calendar.timegm() is new to Python 2.x and # calendar.leapdays() was wrong in Python 1.5.2 # try: timegm = calendar.timegm except AttributeError: def leapdays(year1, year2): """Return number of leap years in range [year1, year2). Assume year1 <= year2.""" year1 = year1 - 1 year2 = year2 - 1 return (year2/4 - year1/4) - (year2/100 - year1/100) + (year2/400 - year1/400) EPOCH = 1970 def timegm(tuple): """Unrelated but handy function to calculate Unix timestamp from GMT.""" year, month, day, hour, minute, second = tuple[:6] # assert year >= EPOCH # assert 1 <= month <= 12 days = 365*(year-EPOCH) + leapdays(EPOCH, year) for i in range(1, month): days = days + calendar.mdays[i] if month > 2 and calendar.isleap(year): days = days + 1 days = days + day - 1 hours = days*24 + hour minutes = hours*60 + minute seconds = minutes*60 + second return seconds # # tempfile.mkdtemp() is new to Python 2.3 # try: mkdtemp = tempfile.mkdtemp except AttributeError: def mkdtemp(suffix="", prefix="tmp", dir=None): # mktemp() only took a single suffix argument until Python 2.3. # We'll do the best we can. oldtmpdir = os.environ.get('TMPDIR') try: for i in range(10): if dir: os.environ['TMPDIR'] = dir dir = tempfile.mktemp(suffix) if prefix: parent, base = os.path.split(dir) dir = os.path.join(parent, prefix + base) try: os.mkdir(dir, 0700) return dir except OSError, e: if e.errno == errno.EEXIST: continue # try again raise finally: if oldtmpdir: os.environ['TMPDIR'] = oldtmpdir elif os.environ.has_key('TMPDIR'): del(os.environ['TMPDIR']) raise IOError, (errno.EEXIST, "No usable temporary directory name found") # # the following stuff is *ONLY* needed for standalone.py. # For that reason I've encapsulated it into a function. # def for_standalone(): import SocketServer if not hasattr(SocketServer.TCPServer, "close_request"): # # method close_request() was missing until Python 2.1 # class TCPServer(SocketServer.TCPServer): def process_request(self, request, client_address): """Call finish_request. Overridden by ForkingMixIn and ThreadingMixIn. """ self.finish_request(request, client_address) self.close_request(request) def close_request(self, request): """Called to clean up an individual request.""" request.close() SocketServer.TCPServer = TCPServer viewvc-1.1.22/lib/debug.py0000644000175000017500000001342412071623704015516 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # Note: a t_start/t_end pair consumes about 0.00005 seconds on a P3/700. # the lambda form (when debugging is disabled) should be even faster. # import sys # Set to non-zero to track and print processing times SHOW_TIMES = 0 # Set to non-zero to display child process info SHOW_CHILD_PROCESSES = 0 # Set to a server-side path to force the tarball view to generate the # tarball as a file on the server, instead of transmitting the data # back to the browser. This enables easy display of error # considitions in the browser, as well as tarball inspection on the # server. NOTE: The file will be a TAR archive, *not* gzip-compressed. TARFILE_PATH = '' if SHOW_TIMES: import time _timers = { } _times = { } def t_start(which): _timers[which] = time.time() def t_end(which): t = time.time() - _timers[which] if _times.has_key(which): _times[which] = _times[which] + t else: _times[which] = t def t_dump(out): out.write('
') names = _times.keys() names.sort() for name in names: out.write('%s: %.6fs
\n' % (name, _times[name])) out.write('
') else: t_start = t_end = t_dump = lambda *args: None class ViewVCException: def __init__(self, msg, status=None): self.msg = msg self.status = status def __str__(self): if self.status: return '%s: %s' % (self.status, self.msg) return "ViewVC Unrecoverable Error: %s" % self.msg def PrintException(server, exc_data): status = exc_data['status'] msg = exc_data['msg'] tb = exc_data['stacktrace'] server.header(status=status) server.write("

An Exception Has Occurred

\n") s = '' if msg: s = '

%s

' % server.escape(msg) if status: s = s + ('

HTTP Response Status

\n

\n%s


\n' % status) server.write(s) server.write("

Python Traceback

\n

")
  server.write(server.escape(tb))
  server.write("

\n") def GetExceptionData(): # capture the exception before doing anything else exc_type, exc, exc_tb = sys.exc_info() exc_dict = { 'status' : None, 'msg' : None, 'stacktrace' : None, } try: import traceback, string if isinstance(exc, ViewVCException): exc_dict['msg'] = exc.msg exc_dict['status'] = exc.status tb = string.join(traceback.format_exception(exc_type, exc, exc_tb), '') exc_dict['stacktrace'] = tb finally: # prevent circular reference. sys.exc_info documentation warns # "Assigning the traceback return value to a local variable in a function # that is handling an exception will cause a circular reference..." # This is all based on 'exc_tb', and we're now done with it. Toss it. del exc_tb return exc_dict if SHOW_CHILD_PROCESSES: class Process: def __init__(self, command, inStream, outStream, errStream): self.command = command self.debugIn = inStream self.debugOut = outStream self.debugErr = errStream import sapi if not sapi.server is None: if not sapi.server.pageGlobals.has_key('processes'): sapi.server.pageGlobals['processes'] = [self] else: sapi.server.pageGlobals['processes'].append(self) def DumpChildren(server): import os if not server.pageGlobals.has_key('processes'): return server.header() lastOut = None i = 0 for k in server.pageGlobals['processes']: i = i + 1 server.write("\n") server.write("" % i) server.write("\n \n\n") server.write("\n \n\n") if k.debugOut is k.debugErr: server.write("\n \n\n") else: server.write("\n \n\n") server.write("\n \n\n") server.write("
Child Process%i
Command Line
")
      server.write(server.escape(k.command))
      server.write("
Standard In: ") if k.debugIn is lastOut and not lastOut is None: server.write("Output from process %i" % (i - 1)) elif k.debugIn: server.write("
")
        server.write(server.escape(k.debugIn.getvalue()))
        server.write("
") server.write("
Standard Out & Error:
")
        if k.debugOut:
          server.write(server.escape(k.debugOut.getvalue()))
        server.write("
Standard Out:
")
        if k.debugOut:
          server.write(server.escape(k.debugOut.getvalue()))
        server.write("
Standard Error:
")
        if k.debugErr:
          server.write(server.escape(k.debugErr.getvalue()))
        server.write("
\n") server.flush() lastOut = k.debugOut server.write("\n") server.write("") for k, v in os.environ.items(): server.write("\n \n \n") server.write("
Environment Variables
")
      server.write(server.escape(k))
      server.write("
")
      server.write(server.escape(v))
      server.write("
") else: def DumpChildren(server): pass viewvc-1.1.22/lib/win32popen.py0000644000175000017500000001776012071623704016443 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # Utilities for controlling processes and pipes on win32 # # ----------------------------------------------------------------------- import os, sys, traceback, string, thread try: import win32api except ImportError, e: raise ImportError, str(e) + """ Did you install the Python for Windows Extensions? http://sourceforge.net/projects/pywin32/ """ import win32process, win32pipe, win32con import win32event, win32file, winerror import pywintypes, msvcrt # Buffer size for spooling SPOOL_BYTES = 4096 # File object to write error messages SPOOL_ERROR = sys.stderr #SPOOL_ERROR = open("m:/temp/error.txt", "wt") def CommandLine(command, args): """Convert an executable path and a sequence of arguments into a command line that can be passed to CreateProcess""" cmd = "\"" + string.replace(command, "\"", "\"\"") + "\"" for arg in args: cmd = cmd + " \"" + string.replace(arg, "\"", "\"\"") + "\"" return cmd def CreateProcess(cmd, hStdInput, hStdOutput, hStdError): """Creates a new process which uses the specified handles for its standard input, output, and error. The handles must be inheritable. 0 can be passed as a special handle indicating that the process should inherit the current process's input, output, or error streams, and None can be passed to discard the child process's output or to prevent it from reading any input.""" # initialize new process's startup info si = win32process.STARTUPINFO() si.dwFlags = win32process.STARTF_USESTDHANDLES if hStdInput == 0: si.hStdInput = win32api.GetStdHandle(win32api.STD_INPUT_HANDLE) else: si.hStdInput = hStdInput if hStdOutput == 0: si.hStdOutput = win32api.GetStdHandle(win32api.STD_OUTPUT_HANDLE) else: si.hStdOutput = hStdOutput if hStdError == 0: si.hStdError = win32api.GetStdHandle(win32api.STD_ERROR_HANDLE) else: si.hStdError = hStdError # create the process phandle, pid, thandle, tid = win32process.CreateProcess \ ( None, # appName cmd, # commandLine None, # processAttributes None, # threadAttributes 1, # bInheritHandles win32con.NORMAL_PRIORITY_CLASS, # dwCreationFlags None, # newEnvironment None, # currentDirectory si # startupinfo ) if hStdInput and hasattr(hStdInput, 'Close'): hStdInput.Close() if hStdOutput and hasattr(hStdOutput, 'Close'): hStdOutput.Close() if hStdError and hasattr(hStdError, 'Close'): hStdError.Close() return phandle, pid, thandle, tid def CreatePipe(readInheritable, writeInheritable): """Create a new pipe specifying whether the read and write ends are inheritable and whether they should be created for blocking or nonblocking I/O.""" r, w = win32pipe.CreatePipe(None, SPOOL_BYTES) if readInheritable: r = MakeInheritedHandle(r) if writeInheritable: w = MakeInheritedHandle(w) return r, w def File2FileObject(pipe, mode): """Make a C stdio file object out of a win32 file handle""" if string.find(mode, 'r') >= 0: wmode = os.O_RDONLY elif string.find(mode, 'w') >= 0: wmode = os.O_WRONLY if string.find(mode, 'b') >= 0: wmode = wmode | os.O_BINARY if string.find(mode, 't') >= 0: wmode = wmode | os.O_TEXT return os.fdopen(msvcrt.open_osfhandle(pipe.Detach(),wmode),mode) def FileObject2File(fileObject): """Get the win32 file handle from a C stdio file object""" return win32file._get_osfhandle(fileObject.fileno()) def DuplicateHandle(handle): """Duplicates a win32 handle.""" proc = win32api.GetCurrentProcess() return win32api.DuplicateHandle(proc,handle,proc,0,0,win32con.DUPLICATE_SAME_ACCESS) def MakePrivateHandle(handle, replace = 1): """Turn an inherited handle into a non inherited one. This avoids the handle duplication that occurs on CreateProcess calls which can create uncloseable pipes.""" ### Could change implementation to use SetHandleInformation()... flags = win32con.DUPLICATE_SAME_ACCESS proc = win32api.GetCurrentProcess() if replace: flags = flags | win32con.DUPLICATE_CLOSE_SOURCE newhandle = win32api.DuplicateHandle(proc,handle,proc,0,0,flags) if replace: handle.Detach() # handle was already deleted by the last call return newhandle def MakeInheritedHandle(handle, replace = 1): """Turn a private handle into an inherited one.""" ### Could change implementation to use SetHandleInformation()... flags = win32con.DUPLICATE_SAME_ACCESS proc = win32api.GetCurrentProcess() if replace: flags = flags | win32con.DUPLICATE_CLOSE_SOURCE newhandle = win32api.DuplicateHandle(proc,handle,proc,0,1,flags) if replace: handle.Detach() # handle was deleted by the last call return newhandle def MakeSpyPipe(readInheritable, writeInheritable, outFiles = None, doneEvent = None): """Return read and write handles to a pipe that asynchronously writes all of its input to the files in the outFiles sequence. doneEvent can be None, or a a win32 event handle that will be set when the write end of pipe is closed. """ if outFiles is None: return CreatePipe(readInheritable, writeInheritable) r, writeHandle = CreatePipe(0, writeInheritable) if readInheritable is None: readHandle, w = None, None else: readHandle, w = CreatePipe(readInheritable, 0) thread.start_new_thread(SpoolWorker, (r, w, outFiles, doneEvent)) return readHandle, writeHandle def SpoolWorker(srcHandle, destHandle, outFiles, doneEvent): """Thread entry point for implementation of MakeSpyPipe""" try: buffer = win32file.AllocateReadBuffer(SPOOL_BYTES) while 1: try: #print >> SPOOL_ERROR, "Calling ReadFile..."; SPOOL_ERROR.flush() hr, data = win32file.ReadFile(srcHandle, buffer) #print >> SPOOL_ERROR, "ReadFile returned '%s', '%s'" % (str(hr), str(data)); SPOOL_ERROR.flush() if hr != 0: raise "win32file.ReadFile returned %i, '%s'" % (hr, data) elif len(data) == 0: break except pywintypes.error, e: #print >> SPOOL_ERROR, "ReadFile threw '%s'" % str(e); SPOOL_ERROR.flush() if e.args[0] == winerror.ERROR_BROKEN_PIPE: break else: raise e #print >> SPOOL_ERROR, "Writing to %i file objects..." % len(outFiles); SPOOL_ERROR.flush() for f in outFiles: f.write(data) #print >> SPOOL_ERROR, "Done writing to file objects."; SPOOL_ERROR.flush() #print >> SPOOL_ERROR, "Writing to destination %s" % str(destHandle); SPOOL_ERROR.flush() if destHandle: #print >> SPOOL_ERROR, "Calling WriteFile..."; SPOOL_ERROR.flush() hr, bytes = win32file.WriteFile(destHandle, data) #print >> SPOOL_ERROR, "WriteFile() passed %i bytes and returned %i, %i" % (len(data), hr, bytes); SPOOL_ERROR.flush() if hr != 0 or bytes != len(data): raise "win32file.WriteFile() passed %i bytes and returned %i, %i" % (len(data), hr, bytes) srcHandle.Close() if doneEvent: win32event.SetEvent(doneEvent) if destHandle: destHandle.Close() except: info = sys.exc_info() SPOOL_ERROR.writelines(apply(traceback.format_exception, info), '') SPOOL_ERROR.flush() del info def NullFile(inheritable): """Create a null file handle.""" if inheritable: sa = pywintypes.SECURITY_ATTRIBUTES() sa.bInheritHandle = 1 else: sa = None return win32file.CreateFile("nul", win32file.GENERIC_READ | win32file.GENERIC_WRITE, win32file.FILE_SHARE_READ | win32file.FILE_SHARE_WRITE, sa, win32file.OPEN_EXISTING, 0, None) viewvc-1.1.22/lib/popen.py0000644000175000017500000001270512071623704015552 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # popen.py: a replacement for os.popen() # # This implementation of popen() provides a cmd + args calling sequence, # rather than a system() type of convention. The shell facilities are not # available, but that implies we can avoid worrying about shell hacks in # the arguments. # # ----------------------------------------------------------------------- import os import sys import sapi import threading import string if sys.platform == "win32": import win32popen import win32event import win32process import debug import StringIO def popen(cmd, args, mode, capture_err=1): if sys.platform == "win32": command = win32popen.CommandLine(cmd, args) if string.find(mode, 'r') >= 0: hStdIn = None if debug.SHOW_CHILD_PROCESSES: dbgIn, dbgOut = None, StringIO.StringIO() handle, hStdOut = win32popen.MakeSpyPipe(0, 1, (dbgOut,)) if capture_err: hStdErr = hStdOut dbgErr = dbgOut else: dbgErr = StringIO.StringIO() x, hStdErr = win32popen.MakeSpyPipe(None, 1, (dbgErr,)) else: handle, hStdOut = win32popen.CreatePipe(0, 1) if capture_err: hStdErr = hStdOut else: hStdErr = win32popen.NullFile(1) else: if debug.SHOW_CHILD_PROCESSES: dbgIn, dbgOut, dbgErr = StringIO.StringIO(), StringIO.StringIO(), StringIO.StringIO() hStdIn, handle = win32popen.MakeSpyPipe(1, 0, (dbgIn,)) x, hStdOut = win32popen.MakeSpyPipe(None, 1, (dbgOut,)) x, hStdErr = win32popen.MakeSpyPipe(None, 1, (dbgErr,)) else: hStdIn, handle = win32popen.CreatePipe(0, 1) hStdOut = None hStdErr = None phandle, pid, thandle, tid = win32popen.CreateProcess(command, hStdIn, hStdOut, hStdErr) if debug.SHOW_CHILD_PROCESSES: debug.Process(command, dbgIn, dbgOut, dbgErr) return _pipe(win32popen.File2FileObject(handle, mode), phandle) # flush the stdio buffers since we are about to change the FD under them sys.stdout.flush() sys.stderr.flush() r, w = os.pipe() pid = os.fork() if pid: # in the parent # close the descriptor that we don't need and return the other one. if string.find(mode, 'r') >= 0: os.close(w) return _pipe(os.fdopen(r, mode), pid) os.close(r) return _pipe(os.fdopen(w, mode), pid) # in the child # we'll need /dev/null for the discarded I/O null = os.open('/dev/null', os.O_RDWR) if string.find(mode, 'r') >= 0: # hook stdout/stderr to the "write" channel os.dup2(w, 1) # "close" stdin; the child shouldn't use it ### this isn't quite right... we may want the child to read from stdin os.dup2(null, 0) # what to do with errors? if capture_err: os.dup2(w, 2) else: os.dup2(null, 2) else: # hook stdin to the "read" channel os.dup2(r, 0) # "close" stdout/stderr; the child shouldn't use them ### this isn't quite right... we may want the child to write to these os.dup2(null, 1) os.dup2(null, 2) # don't need these FDs any more os.close(null) os.close(r) os.close(w) # the stdin/stdout/stderr are all set up. exec the target try: os.execvp(cmd, (cmd,) + tuple(args)) except: # aid debugging, if the os.execvp above fails for some reason: print "

exec failed:

", cmd, string.join(args), "
" raise # crap. shouldn't be here. sys.exit(127) class _pipe: "Wrapper for a file which can wait() on a child process at close time." def __init__(self, file, child_pid, done_event = None, thread = None): self.file = file self.child_pid = child_pid if sys.platform == "win32": if done_event: self.wait_for = (child_pid, done_event) else: self.wait_for = (child_pid,) else: self.thread = thread def eof(self): ### should be calling file.eof() here instead of file.close(), there ### may be data in the pipe or buffer after the process exits if sys.platform == "win32": r = win32event.WaitForMultipleObjects(self.wait_for, 1, 0) if r == win32event.WAIT_OBJECT_0: self.file.close() self.file = None return win32process.GetExitCodeProcess(self.child_pid) return None if self.thread and self.thread.isAlive(): return None pid, status = os.waitpid(self.child_pid, os.WNOHANG) if pid: self.file.close() self.file = None return status return None def close(self): if self.file: self.file.close() self.file = None if sys.platform == "win32": win32event.WaitForMultipleObjects(self.wait_for, 1, win32event.INFINITE) return win32process.GetExitCodeProcess(self.child_pid) else: if self.thread: self.thread.join() if type(self.child_pid) == type([]): for pid in self.child_pid: exit = os.waitpid(pid, 0)[1] return exit else: return os.waitpid(self.child_pid, 0)[1] return None def __getattr__(self, name): return getattr(self.file, name) def __del__(self): self.close() viewvc-1.1.22/lib/query.py0000644000175000017500000003430712071623704015600 0ustar cmpilatocmpilato#!/usr/bin/env python # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # CGI script to process and display queries to CVSdb # # This script is part of the ViewVC package. More information can be # found at http://viewvc.org # # ----------------------------------------------------------------------- import os import sys import string import time import cvsdb import viewvc import ezt import debug import urllib import fnmatch class FormData: def __init__(self, form): self.valid = 0 self.repository = "" self.branch = "" self.directory = "" self.file = "" self.who = "" self.sortby = "" self.date = "" self.hours = 0 self.decode_thyself(form) def decode_thyself(self, form): try: self.repository = string.strip(form["repository"].value) except KeyError: pass except TypeError: pass else: self.valid = 1 try: self.branch = string.strip(form["branch"].value) except KeyError: pass except TypeError: pass else: self.valid = 1 try: self.directory = string.strip(form["directory"].value) except KeyError: pass except TypeError: pass else: self.valid = 1 try: self.file = string.strip(form["file"].value) except KeyError: pass except TypeError: pass else: self.valid = 1 try: self.who = string.strip(form["who"].value) except KeyError: pass except TypeError: pass else: self.valid = 1 try: self.sortby = string.strip(form["sortby"].value) except KeyError: pass except TypeError: pass try: self.date = string.strip(form["date"].value) except KeyError: pass except TypeError: pass try: self.hours = int(form["hours"].value) except KeyError: pass except TypeError: pass except ValueError: pass else: self.valid = 1 ## returns a tuple-list (mod-str, string) def listparse_string(str): return_list = [] cmd = "" temp = "" escaped = 0 state = "eat leading whitespace" for c in str: ## handle escaped charactors if not escaped and c == "\\": escaped = 1 continue ## strip leading white space if state == "eat leading whitespace": if c in string.whitespace: continue else: state = "get command or data" ## parse to '"' or "," if state == "get command or data": ## just add escaped charactors if escaped: escaped = 0 temp = temp + c continue ## the data is in quotes after the command elif c == "\"": cmd = temp temp = "" state = "get quoted data" continue ## this tells us there was no quoted data, therefore no ## command; add the command and start over elif c == ",": ## strip ending whitespace on un-quoted data temp = string.rstrip(temp) return_list.append( ("", temp) ) temp = "" state = "eat leading whitespace" continue ## record the data else: temp = temp + c continue ## parse until ending '"' if state == "get quoted data": ## just add escaped charactors if escaped: escaped = 0 temp = temp + c continue ## look for ending '"' elif c == "\"": return_list.append( (cmd, temp) ) cmd = "" temp = "" state = "eat comma after quotes" continue ## record the data else: temp = temp + c continue ## parse until "," if state == "eat comma after quotes": if c in string.whitespace: continue elif c == ",": state = "eat leading whitespace" continue else: print "format error" sys.exit(1) if cmd or temp: return_list.append((cmd, temp)) return return_list def decode_command(cmd): if cmd == "r": return "regex" elif cmd == "l": return "like" else: return "exact" def form_to_cvsdb_query(cfg, form_data): query = cvsdb.CreateCheckinQuery() query.SetLimit(cfg.cvsdb.row_limit) if form_data.repository: for cmd, str in listparse_string(form_data.repository): cmd = decode_command(cmd) query.SetRepository(str, cmd) if form_data.branch: for cmd, str in listparse_string(form_data.branch): cmd = decode_command(cmd) query.SetBranch(str, cmd) if form_data.directory: for cmd, str in listparse_string(form_data.directory): cmd = decode_command(cmd) query.SetDirectory(str, cmd) if form_data.file: for cmd, str in listparse_string(form_data.file): cmd = decode_command(cmd) query.SetFile(str, cmd) if form_data.who: for cmd, str in listparse_string(form_data.who): cmd = decode_command(cmd) query.SetAuthor(str, cmd) if form_data.sortby == "author": query.SetSortMethod("author") elif form_data.sortby == "file": query.SetSortMethod("file") else: query.SetSortMethod("date") if form_data.date: if form_data.date == "hours" and form_data.hours: query.SetFromDateHoursAgo(form_data.hours) elif form_data.date == "day": query.SetFromDateDaysAgo(1) elif form_data.date == "week": query.SetFromDateDaysAgo(7) elif form_data.date == "month": query.SetFromDateDaysAgo(31) return query def prev_rev(rev): '''Returns a string representing the previous revision of the argument.''' r = string.split(rev, '.') # decrement final revision component r[-1] = str(int(r[-1]) - 1) # prune if we pass the beginning of the branch if len(r) > 2 and r[-1] == '0': r = r[:-2] return string.join(r, '.') def is_forbidden(cfg, cvsroot_name, module): '''Return 1 if MODULE in CVSROOT_NAME is forbidden; return 0 otherwise.''' # CVSROOT_NAME might be None here if the data comes from an # unconfigured root. This interfaces doesn't care that the root # isn't configured, but if that's the case, it will consult only # the base and per-vhost configuration for authorizer and # authorizer parameters. if cvsroot_name: authorizer, params = cfg.get_authorizer_and_params_hack(cvsroot_name) else: authorizer = cfg.options.authorizer params = cfg.get_authorizer_params() # If CVSROOT_NAME isn't configured to use an authorizer, nothing # is forbidden. If it's configured to use something other than # the 'forbidden' authorizer, complain. Otherwise, check for # forbiddenness per the PARAMS as expected. if not authorizer: return 0 if authorizer != 'forbidden': raise Exception("The 'forbidden' authorizer is the only one supported " "by this interface. The '%s' root is configured to " "use a different one." % (cvsroot_name)) forbidden = params.get('forbidden', '') forbidden = map(string.strip, filter(None, string.split(forbidden, ','))) default = 0 for pat in forbidden: if pat[0] == '!': default = 1 if fnmatch.fnmatchcase(module, pat[1:]): return 0 elif fnmatch.fnmatchcase(module, pat): return 1 return default def build_commit(server, cfg, desc, files, cvsroots, viewvc_link): ob = _item(num_files=len(files), files=[]) ob.log = desc and string.replace(server.escape(desc), '\n', '
') or '' for commit in files: repository = commit.GetRepository() directory = commit.GetDirectory() cvsroot_name = cvsroots.get(repository) ## find the module name (if any) try: module = filter(None, string.split(directory, '/'))[0] except IndexError: module = None ## skip commits we aren't supposed to show if module and ((module == 'CVSROOT' and cfg.options.hide_cvsroot) \ or is_forbidden(cfg, cvsroot_name, module)): continue ctime = commit.GetTime() if not ctime: ctime = " " else: if (cfg.options.use_localtime): ctime = time.strftime("%y/%m/%d %H:%M %Z", time.localtime(ctime)) else: ctime = time.strftime("%y/%m/%d %H:%M", time.gmtime(ctime)) \ + ' UTC' ## make the file link try: file = (directory and directory + "/") + commit.GetFile() except: raise Exception, str([directory, commit.GetFile()]) ## If we couldn't find the cvsroot path configured in the ## viewvc.conf file, or we don't have a VIEWVC_LINK, then ## don't make the link. if cvsroot_name and viewvc_link: flink = '[%s] %s' % ( cvsroot_name, viewvc_link, urllib.quote(file), cvsroot_name, file) if commit.GetType() == commit.CHANGE: dlink = '%s/%s?root=%s&view=diff&r1=%s&r2=%s' % ( viewvc_link, urllib.quote(file), cvsroot_name, prev_rev(commit.GetRevision()), commit.GetRevision()) else: dlink = None else: flink = '[%s] %s' % (repository, file) dlink = None ob.files.append(_item(date=ctime, author=commit.GetAuthor(), link=flink, rev=commit.GetRevision(), branch=commit.GetBranch(), plus=int(commit.GetPlusCount()), minus=int(commit.GetMinusCount()), type=commit.GetTypeString(), difflink=dlink, )) return ob def run_query(server, cfg, form_data, viewvc_link): query = form_to_cvsdb_query(cfg, form_data) db = cvsdb.ConnectDatabaseReadOnly(cfg) db.RunQuery(query) commit_list = query.GetCommitList() if not commit_list: return [ ], 0 row_limit_reached = query.GetLimitReached() commits = [ ] files = [ ] cvsroots = {} viewvc.expand_root_parents(cfg) rootitems = cfg.general.svn_roots.items() + cfg.general.cvs_roots.items() for key, value in rootitems: cvsroots[cvsdb.CleanRepository(value)] = key current_desc = commit_list[0].GetDescription() for commit in commit_list: desc = commit.GetDescription() if current_desc == desc: files.append(commit) continue commits.append(build_commit(server, cfg, current_desc, files, cvsroots, viewvc_link)) files = [ commit ] current_desc = desc ## add the last file group to the commit list commits.append(build_commit(server, cfg, current_desc, files, cvsroots, viewvc_link)) # Strip out commits that don't have any files attached to them. The # files probably aren't present because they've been blocked via # forbiddenness. def _only_with_files(commit): return len(commit.files) > 0 commits = filter(_only_with_files, commits) return commits, row_limit_reached def main(server, cfg, viewvc_link): try: form = server.FieldStorage() form_data = FormData(form) if form_data.valid: commits, row_limit_reached = run_query(server, cfg, form_data, viewvc_link) query = None else: commits = [ ] row_limit_reached = 0 query = 'skipped' docroot = cfg.options.docroot if docroot is None and viewvc_link: docroot = viewvc_link + '/' + viewvc.docroot_magic_path data = ezt.TemplateData({ 'cfg' : cfg, 'address' : cfg.general.address, 'vsn' : viewvc.__version__, 'repository' : server.escape(form_data.repository), 'branch' : server.escape(form_data.branch), 'directory' : server.escape(form_data.directory), 'file' : server.escape(form_data.file), 'who' : server.escape(form_data.who), 'docroot' : docroot, 'sortby' : form_data.sortby, 'date' : form_data.date, 'query' : query, 'row_limit_reached' : ezt.boolean(row_limit_reached), 'commits' : commits, 'num_commits' : len(commits), 'rss_href' : None, 'hours' : form_data.hours and form_data.hours or 2, }) # generate the page server.header() template = viewvc.get_view_template(cfg, "query") template.generate(server.file(), data) except SystemExit, e: pass except: exc_info = debug.GetExceptionData() server.header(status=exc_info['status']) debug.PrintException(server, exc_info) class _item: def __init__(self, **kw): vars(self).update(kw) viewvc-1.1.22/lib/sapi.py0000644000175000017500000003134012071623704015361 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # generic server api - currently supports normal cgi, mod_python, and # active server pages # # ----------------------------------------------------------------------- import types import string import os import sys import re import cgi # global server object. It will be either a CgiServer, a WsgiServer, # or a proxy to an AspServer or ModPythonServer object. server = None # Simple HTML string escaping. Note that we always escape the # double-quote character -- ViewVC shouldn't ever need to preserve # that character as-is, and sometimes needs to embed escaped values # into HTML attributes. def escape(s): s = str(s) s = string.replace(s, '&', '&') s = string.replace(s, '>', '>') s = string.replace(s, '<', '<') s = string.replace(s, '"', """) return s class Server: def __init__(self): self.pageGlobals = {} def self(self): return self def escape(self, s): return escape(s) def close(self): pass class ThreadedServer(Server): def __init__(self): Server.__init__(self) self.inheritableOut = 0 global server if not isinstance(server, ThreadedServerProxy): server = ThreadedServerProxy() if not isinstance(sys.stdout, File): sys.stdout = File(server) server.registerThread(self) def file(self): return File(self) def close(self): server.unregisterThread() class ThreadedServerProxy: """In a multithreaded server environment, ThreadedServerProxy stores the different server objects being used to display pages and transparently forwards access to them based on the current thread id.""" def __init__(self): self.__dict__['servers'] = { } global thread import thread def registerThread(self, server): self.__dict__['servers'][thread.get_ident()] = server def unregisterThread(self): del self.__dict__['servers'][thread.get_ident()] def self(self): """This function bypasses the getattr and setattr trickery and returns the actual server object.""" return self.__dict__['servers'][thread.get_ident()] def __getattr__(self, key): return getattr(self.self(), key) def __setattr__(self, key, value): setattr(self.self(), key, value) def __delattr__(self, key): delattr(self.self(), key) class File: def __init__(self, server): self.closed = 0 self.mode = 'w' self.name = "" self.softspace = 0 self.server = server def write(self, s): self.server.write(s) def writelines(self, list): for s in list: self.server.write(s) def flush(self): self.server.flush() def truncate(self, size): pass def close(self): pass class CgiServer(Server): def __init__(self, inheritableOut = 1): Server.__init__(self) self.headerSent = 0 self.headers = [] self.inheritableOut = inheritableOut self.iis = os.environ.get('SERVER_SOFTWARE', '')[:13] == 'Microsoft-IIS' if sys.platform == "win32" and inheritableOut: import msvcrt msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) global server server = self def addheader(self, name, value): self.headers.append((name, value)) def header(self, content_type='text/html; charset=UTF-8', status=None): if not self.headerSent: self.headerSent = 1 extraheaders = '' for (name, value) in self.headers: extraheaders = extraheaders + '%s: %s\r\n' % (name, value) # The only way ViewVC pages and error messages are visible under # IIS is if a 200 error code is returned. Otherwise IIS instead # sends the static error page corresponding to the code number. if status is None or (status[:3] != '304' and self.iis): status = '' else: status = 'Status: %s\r\n' % status sys.stdout.write('%sContent-Type: %s\r\n%s\r\n' % (status, content_type, extraheaders)) def redirect(self, url): if self.iis: url = fix_iis_url(self, url) self.addheader('Location', url) self.header(status='301 Moved') sys.stdout.write('This document is located here.\n' % url) def getenv(self, name, value=None): ret = os.environ.get(name, value) if self.iis and name == 'PATH_INFO' and ret: ret = fix_iis_path_info(self, ret) return ret def params(self): return cgi.parse() def FieldStorage(fp=None, headers=None, outerboundary="", environ=os.environ, keep_blank_values=0, strict_parsing=0): return cgi.FieldStorage(fp, headers, outerboundary, environ, keep_blank_values, strict_parsing) def write(self, s): sys.stdout.write(s) def flush(self): sys.stdout.flush() def file(self): return sys.stdout class WsgiServer(Server): def __init__(self, environ, start_response): Server.__init__(self) self._environ = environ self._start_response = start_response; self._headers = [] self._wsgi_write = None self.headerSent = False global server server = self global cgi import cgi def addheader(self, name, value): self._headers.append((name, value)) def header(self, content_type='text/html; charset=UTF-8', status=None): if not status: status = "200 OK" if not self.headerSent: self.headerSent = True self._headers.insert(0, ("Content-Type", content_type),) self._wsgi_write = self._start_response("%s" % status, self._headers) def redirect(self, url): """Redirect client to url. This discards any data that has been queued to be sent to the user. But there should never by any anyway. """ self.addheader('Location', url) self.header(status='301 Moved') self._wsgi_write('This document is located here.' % url) def getenv(self, name, value=None): return self._environ.get(name, value) def params(self): return cgi.parse(environ=self._environ, fp=self._environ["wsgi.input"]) def FieldStorage(self, fp=None, headers=None, outerboundary="", environ=os.environ, keep_blank_values=0, strict_parsing=0): return cgi.FieldStorage(self._environ["wsgi.input"], headers, outerboundary, self._environ, keep_blank_values, strict_parsing) def write(self, s): self._wsgi_write(s) def flush(self): pass def file(self): return File(self) class AspServer(ThreadedServer): def __init__(self, Server, Request, Response, Application): ThreadedServer.__init__(self) self.headerSent = 0 self.server = Server self.request = Request self.response = Response self.application = Application def addheader(self, name, value): self.response.AddHeader(name, value) def header(self, content_type=None, status=None): # Normally, setting self.response.ContentType after headers have already # been sent simply results in an AttributeError exception, but sometimes # it leads to a fatal ASP error. For this reason I'm keeping the # self.headerSent member and only checking for the exception as a # secondary measure if not self.headerSent: try: self.headerSent = 1 if content_type is None: self.response.ContentType = 'text/html; charset=UTF-8' else: self.response.ContentType = content_type if status is not None: self.response.Status = status except AttributeError: pass def redirect(self, url): self.response.Redirect(url) def getenv(self, name, value = None): ret = self.request.ServerVariables(name)() if not type(ret) is types.UnicodeType: return value ret = str(ret) if name == 'PATH_INFO': ret = fix_iis_path_info(self, ret) return ret def params(self): p = {} for i in self.request.Form: p[str(i)] = map(str, self.request.Form[i]) for i in self.request.QueryString: p[str(i)] = map(str, self.request.QueryString[i]) return p def FieldStorage(self, fp=None, headers=None, outerboundary="", environ=os.environ, keep_blank_values=0, strict_parsing=0): # Code based on a very helpful usenet post by "Max M" (maxm@mxm.dk) # Subject "Re: Help! IIS and Python" # http://groups.google.com/groups?selm=3C7C0AB6.2090307%40mxm.dk from StringIO import StringIO from cgi import FieldStorage environ = {} for i in self.request.ServerVariables: environ[str(i)] = str(self.request.ServerVariables(i)()) # this would be bad for uploaded files, could use a lot of memory binaryContent, size = self.request.BinaryRead(int(environ['CONTENT_LENGTH'])) fp = StringIO(str(binaryContent)) fs = FieldStorage(fp, None, "", environ, keep_blank_values, strict_parsing) fp.close() return fs def write(self, s): t = type(s) if t is types.StringType: s = buffer(s) elif not t is types.BufferType: s = buffer(str(s)) self.response.BinaryWrite(s) def flush(self): self.response.Flush() _re_status = re.compile("\\d+") class ModPythonServer(ThreadedServer): def __init__(self, request): ThreadedServer.__init__(self) self.request = request self.headerSent = 0 def addheader(self, name, value): self.request.headers_out.add(name, value) def header(self, content_type=None, status=None): if content_type is None: self.request.content_type = 'text/html; charset=UTF-8' else: self.request.content_type = content_type self.headerSent = 1 if status is not None: m = _re_status.match(status) if not m is None: self.request.status = int(m.group()) def redirect(self, url): import mod_python.apache self.request.headers_out['Location'] = url self.request.status = mod_python.apache.HTTP_MOVED_TEMPORARILY self.request.write("You are being redirected to %s" % (url, url)) def getenv(self, name, value = None): try: return self.request.subprocess_env[name] except KeyError: return value def params(self): import mod_python.util if self.request.args is None: return {} else: return mod_python.util.parse_qs(self.request.args) def FieldStorage(self, fp=None, headers=None, outerboundary="", environ=os.environ, keep_blank_values=0, strict_parsing=0): import mod_python.util return mod_python.util.FieldStorage(self.request, keep_blank_values, strict_parsing) def write(self, s): self.request.write(s) def flush(self): pass def fix_iis_url(server, url): """When a CGI application under IIS outputs a "Location" header with a url beginning with a forward slash, IIS tries to optimise the redirect by not returning any output from the original CGI script at all and instead just returning the new page in its place. Because of this, the browser does not know it is getting a different page than it requested. As a result, The address bar that appears in the browser window shows the wrong location and if the new page is in a different folder than the old one, any relative links on it will be broken. This function can be used to circumvent the IIS "optimization" of local redirects. If it is passed a location that begins with a forward slash it will return a URL constructed with the information in CGI environment. If it is passed a URL or any location that doens't begin with a forward slash it will return just argument unaltered. """ if url[0] == '/': if server.getenv('HTTPS') == 'on': dport = "443" prefix = "https://" else: dport = "80" prefix = "http://" prefix = prefix + server.getenv('HTTP_HOST') if server.getenv('SERVER_PORT') != dport: prefix = prefix + ":" + server.getenv('SERVER_PORT') return prefix + url return url def fix_iis_path_info(server, path_info): """Fix the PATH_INFO value in IIS""" # If the viewvc cgi's are in the /viewvc/ folder on the web server and a # request looks like # # /viewvc/viewvc.cgi/myproject/?someoption # # The CGI environment variables on IIS will look like this: # # SCRIPT_NAME = /viewvc/viewvc.cgi # PATH_INFO = /viewvc/viewvc.cgi/myproject/ # # Whereas on Apache they look like: # # SCRIPT_NAME = /viewvc/viewvc.cgi # PATH_INFO = /myproject/ # # This function converts the IIS PATH_INFO into the nonredundant form # expected by ViewVC return path_info[len(server.getenv('SCRIPT_NAME', '')):] viewvc-1.1.22/lib/accept.py0000644000175000017500000001710012071623704015662 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # accept.py: parse/handle the various Accept headers from the client # # ----------------------------------------------------------------------- import re import string def language(hdr): "Parse an Accept-Language header." # parse the header, storing results in a _LanguageSelector object return _parse(hdr, _LanguageSelector()) # ----------------------------------------------------------------------- _re_token = re.compile(r'\s*([^\s;,"]+|"[^"]*")+\s*') _re_param = re.compile(r';\s*([^;,"]+|"[^"]*")+\s*') _re_split_param = re.compile(r'([^\s=])\s*=\s*(.*)') def _parse(hdr, result): # quick exit for empty or not-supplied header if not hdr: return result pos = 0 while pos < len(hdr): name = _re_token.match(hdr, pos) if not name: raise AcceptLanguageParseError() a = result.item_class(string.lower(name.group(1))) pos = name.end() while 1: # are we looking at a parameter? match = _re_param.match(hdr, pos) if not match: break param = match.group(1) pos = match.end() # split up the pieces of the parameter match = _re_split_param.match(param) if not match: # the "=" was probably missing continue pname = string.lower(match.group(1)) if pname == 'q' or pname == 'qs': try: a.quality = float(match.group(2)) except ValueError: # bad float literal pass elif pname == 'level': try: a.level = float(match.group(2)) except ValueError: # bad float literal pass elif pname == 'charset': a.charset = string.lower(match.group(2)) result.append(a) if hdr[pos:pos+1] == ',': pos = pos + 1 return result class _AcceptItem: def __init__(self, name): self.name = name self.quality = 1.0 self.level = 0.0 self.charset = '' def __str__(self): s = self.name if self.quality != 1.0: s = '%s;q=%.3f' % (s, self.quality) if self.level != 0.0: s = '%s;level=%.3f' % (s, self.level) if self.charset: s = '%s;charset=%s' % (s, self.charset) return s class _LanguageRange(_AcceptItem): def matches(self, tag): "Match the tag against self. Returns the qvalue, or None if non-matching." if tag == self.name: return self.quality # are we a prefix of the available language-tag name = self.name + '-' if tag[:len(name)] == name: return self.quality return None class _LanguageSelector: """Instances select an available language based on the user's request. Languages found in the user's request are added to this object with the append() method (they should be instances of _LanguageRange). After the languages have been added, then the caller can use select_from() to determine which user-request language(s) best matches the set of available languages. Strictly speaking, this class is pretty close for more than just language matching. It has been implemented to enable q-value based matching between requests and availability. Some minor tweaks may be necessary, but simply using a new 'item_class' should be sufficient to allow the _parse() function to construct a selector which holds the appropriate item implementations (e.g. _LanguageRange is the concrete _AcceptItem class that handles matching of language tags). """ item_class = _LanguageRange def __init__(self): self.requested = [ ] def select_from(self, avail): """Select one of the available choices based on the request. Note: if there isn't a match, then the first available choice is considered the default. Also, if a number of matches are equally relevant, then the first-requested will be used. avail is a list of language-tag strings of available languages """ # tuples of (qvalue, language-tag) matches = [ ] # try matching all pairs of desired vs available, recording the # resulting qvalues. we also need to record the longest language-range # that matches since the most specific range "wins" for tag in avail: longest = 0 final = 0.0 # check this tag against the requests from the user for want in self.requested: qvalue = want.matches(tag) #print 'have %s. want %s. qvalue=%s' % (tag, want.name, qvalue) if qvalue is not None and len(want.name) > longest: # we have a match and it is longer than any we may have had. # the final qvalue should be from this tag. final = qvalue longest = len(want.name) # a non-zero qvalue is a potential match if final: matches.append((final, tag)) # if there are no matches, then return the default language tag if not matches: return avail[0] # get the highest qvalue and its corresponding tag matches.sort() qvalue, tag = matches[-1] # if the qvalue is zero, then we have no valid matches. return the # default language tag. if not qvalue: return avail[0] # if there are two or more matches, and the second-highest has a # qvalue equal to the best, then we have multiple "best" options. # select the one that occurs first in self.requested if len(matches) >= 2 and matches[-2][0] == qvalue: # remove non-best matches while matches[0][0] != qvalue: del matches[0] #print "non-deterministic choice", matches # sequence through self.requested, in order for want in self.requested: # try to find this one in our best matches for qvalue, tag in matches: if want.matches(tag): # this requested item is one of the "best" options ### note: this request item could match *other* "best" options, ### so returning *this* one is rather non-deterministic. ### theoretically, we could go further here, and do another ### search based on the ordering in 'avail'. however, note ### that this generally means that we are picking from multiple ### *SUB* languages, so I'm all right with the non-determinism ### at this point. stupid client should send a qvalue if they ### want to refine. return tag # NOTREACHED # return the best match return tag def append(self, item): self.requested.append(item) class AcceptLanguageParseError(Exception): pass def _test(): s = language('en') assert s.select_from(['en']) == 'en' assert s.select_from(['en', 'de']) == 'en' assert s.select_from(['de', 'en']) == 'en' # Netscape 4.x and early version of Mozilla may not send a q value s = language('en, ja') assert s.select_from(['en', 'ja']) == 'en' s = language('fr, de;q=0.9, en-gb;q=0.7, en;q=0.6, en-gb-foo;q=0.8') assert s.select_from(['en']) == 'en' assert s.select_from(['en-gb-foo']) == 'en-gb-foo' assert s.select_from(['de', 'fr']) == 'fr' assert s.select_from(['de', 'en-gb']) == 'de' assert s.select_from(['en-gb', 'en-gb-foo']) == 'en-gb-foo' assert s.select_from(['en-bar']) == 'en-bar' assert s.select_from(['en-gb-bar', 'en-gb-foo']) == 'en-gb-foo' # non-deterministic. en-gb;q=0.7 matches both avail tags. #assert s.select_from(['en-gb-bar', 'en-gb']) == 'en-gb' viewvc-1.1.22/lib/cvsdb.py0000644000175000017500000007607112113165412015532 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- import os import sys import string import time import fnmatch import re import vclib import dbi ## Current commits database schema version number. ## ## Version 0 was the original Bonsai-compatible version. ## ## Version 1 added the 'metadata' table (which holds the 'version' key) ## and renamed all the 'repository'-related stuff to be 'root'- ## CURRENT_SCHEMA_VERSION = 1 ## error error = "cvsdb error" ## CheckinDatabase provides all interfaces needed to the SQL database ## back-end; it needs to be subclassed, and have its "Connect" method ## defined to actually be complete; it should run well off of any DBI 2.0 ## complient database interface class CheckinDatabase: def __init__(self, host, port, user, passwd, database): self._host = host self._port = port self._user = user self._passwd = passwd self._database = database self._version = None ## database lookup caches self._get_cache = {} self._get_id_cache = {} self._desc_id_cache = {} def Connect(self): self.db = dbi.connect( self._host, self._port, self._user, self._passwd, self._database) cursor = self.db.cursor() cursor.execute("SET AUTOCOMMIT=1") table_list = self.GetTableList() if 'metadata' in table_list: version = self.GetMetadataValue("version") if version is None: self._version = 0 else: self._version = int(version) else: self._version = 0 if self._version > CURRENT_SCHEMA_VERSION: raise DatabaseVersionError("Database version %d is newer than the " "last version supported by this " "software." % (self._version)) def sql_get_id(self, table, column, value, auto_set): sql = "SELECT id FROM %s WHERE %s=%%s" % (table, column) sql_args = (value, ) cursor = self.db.cursor() cursor.execute(sql, sql_args) try: (id, ) = cursor.fetchone() except TypeError: if not auto_set: return None else: return str(int(id)) ## insert the new identifier sql = "INSERT INTO %s(%s) VALUES(%%s)" % (table, column) sql_args = (value, ) cursor.execute(sql, sql_args) return self.sql_get_id(table, column, value, 0) def get_id(self, table, column, value, auto_set): ## attempt to retrieve from cache try: return self._get_id_cache[table][column][value] except KeyError: pass id = self.sql_get_id(table, column, value, auto_set) if id == None: return None ## add to cache try: temp = self._get_id_cache[table] except KeyError: temp = self._get_id_cache[table] = {} try: temp2 = temp[column] except KeyError: temp2 = temp[column] = {} temp2[value] = id return id def sql_get(self, table, column, id): sql = "SELECT %s FROM %s WHERE id=%%s" % (column, table) sql_args = (id, ) cursor = self.db.cursor() cursor.execute(sql, sql_args) try: (value, ) = cursor.fetchone() except TypeError: return None return value def get(self, table, column, id): ## attempt to retrieve from cache try: return self._get_cache[table][column][id] except KeyError: pass value = self.sql_get(table, column, id) if value == None: return None ## add to cache try: temp = self._get_cache[table] except KeyError: temp = self._get_cache[table] = {} try: temp2 = temp[column] except KeyError: temp2 = temp[column] = {} temp2[id] = value return value def get_list(self, table, field_index): sql = "SELECT * FROM %s" % (table) cursor = self.db.cursor() cursor.execute(sql) list = [] while 1: row = cursor.fetchone() if row == None: break list.append(row[field_index]) return list def GetCommitsTable(self): return self._version >= 1 and 'commits' or 'checkins' def GetTableList(self): sql = "SHOW TABLES" cursor = self.db.cursor() cursor.execute(sql) list = [] while 1: row = cursor.fetchone() if row == None: break list.append(row[0]) return list def GetMetadataValue(self, name): sql = "SELECT value FROM metadata WHERE name=%s" sql_args = (name) cursor = self.db.cursor() cursor.execute(sql, sql_args) try: (value,) = cursor.fetchone() except TypeError: return None return value def SetMetadataValue(self, name, value): assert(self._version > 0) sql = "REPLACE INTO metadata (name, value) VALUES (%s, %s)" sql_args = (name, value) cursor = self.db.cursor() try: cursor.execute(sql, sql_args) except Exception, e: raise Exception("Error setting metadata: '%s'\n" "\tname = %s\n" "\tvalue = %s\n" % (str(e), name, value)) def GetBranchID(self, branch, auto_set = 1): return self.get_id("branches", "branch", branch, auto_set) def GetBranch(self, id): return self.get("branches", "branch", id) def GetDirectoryID(self, dir, auto_set = 1): return self.get_id("dirs", "dir", dir, auto_set) def GetDirectory(self, id): return self.get("dirs", "dir", id) def GetFileID(self, file, auto_set = 1): return self.get_id("files", "file", file, auto_set) def GetFile(self, id): return self.get("files", "file", id) def GetAuthorID(self, author, auto_set = 1): return self.get_id("people", "who", author, auto_set) def GetAuthor(self, id): return self.get("people", "who", id) def GetRepositoryID(self, repository, auto_set = 1): return self.get_id("repositories", "repository", repository, auto_set) def GetRepository(self, id): return self.get("repositories", "repository", id) def SQLGetDescriptionID(self, description, auto_set = 1): ## lame string hash, blame Netscape -JMP hash = len(description) sql = "SELECT id FROM descs WHERE hash=%s AND description=%s" sql_args = (hash, description) cursor = self.db.cursor() cursor.execute(sql, sql_args) try: (id, ) = cursor.fetchone() except TypeError: if not auto_set: return None else: return str(int(id)) sql = "INSERT INTO descs (hash,description) values (%s,%s)" sql_args = (hash, description) cursor.execute(sql, sql_args) return self.GetDescriptionID(description, 0) def GetDescriptionID(self, description, auto_set = 1): ## attempt to retrieve from cache hash = len(description) try: return self._desc_id_cache[hash][description] except KeyError: pass id = self.SQLGetDescriptionID(description, auto_set) if id == None: return None ## add to cache try: temp = self._desc_id_cache[hash] except KeyError: temp = self._desc_id_cache[hash] = {} temp[description] = id return id def GetDescription(self, id): return self.get("descs", "description", id) def GetRepositoryList(self): return self.get_list("repositories", 1) def GetBranchList(self): return self.get_list("branches", 1) def GetAuthorList(self): return self.get_list("people", 1) def AddCommitList(self, commit_list): for commit in commit_list: self.AddCommit(commit) def AddCommit(self, commit): ci_when = dbi.DateTimeFromTicks(commit.GetTime() or 0.0) ci_type = commit.GetTypeString() who_id = self.GetAuthorID(commit.GetAuthor()) repository_id = self.GetRepositoryID(commit.GetRepository()) directory_id = self.GetDirectoryID(commit.GetDirectory()) file_id = self.GetFileID(commit.GetFile()) revision = commit.GetRevision() sticky_tag = "NULL" branch_id = self.GetBranchID(commit.GetBranch()) plus_count = commit.GetPlusCount() or '0' minus_count = commit.GetMinusCount() or '0' description_id = self.GetDescriptionID(commit.GetDescription()) sql = "REPLACE INTO %s" % (self.GetCommitsTable()) sql = sql + \ " (type,ci_when,whoid,repositoryid,dirid,fileid,revision,"\ " stickytag,branchid,addedlines,removedlines,descid)"\ "VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)" sql_args = (ci_type, ci_when, who_id, repository_id, directory_id, file_id, revision, sticky_tag, branch_id, plus_count, minus_count, description_id) cursor = self.db.cursor() try: cursor.execute(sql, sql_args) except Exception, e: raise Exception("Error adding commit: '%s'\n" "Values were:\n" "\ttype = %s\n" "\tci_when = %s\n" "\twhoid = %s\n" "\trepositoryid = %s\n" "\tdirid = %s\n" "\tfileid = %s\n" "\trevision = %s\n" "\tstickytag = %s\n" "\tbranchid = %s\n" "\taddedlines = %s\n" "\tremovedlines = %s\n" "\tdescid = %s\n" % ((str(e), ) + sql_args)) def SQLQueryListString(self, field, query_entry_list): sqlList = [] for query_entry in query_entry_list: data = query_entry.data ## figure out the correct match type if query_entry.match == "exact": match = "=" elif query_entry.match == "like": match = " LIKE " elif query_entry.match == "glob": match = " REGEXP " # Use fnmatch to translate the glob into a regular # expression. Sadly, we have to account for the fact # that in Python 2.6, fnmatch.translate() started # sticking '\Z(?ms)' at the end of the regular # expression instead of just '$', and doesn't prepend # the '^'. data = fnmatch.translate(data) if data[0] != '^': data = '^' + data if data[-7:] == '\Z(?ms)': data = data[:-7] + '$' elif query_entry.match == "regex": match = " REGEXP " elif query_entry.match == "notregex": match = " NOT REGEXP " sqlList.append("%s%s%s" % (field, match, self.db.literal(data))) return "(%s)" % (string.join(sqlList, " OR ")) def CreateSQLQueryString(self, query, detect_leftover=0): commits_table = self.GetCommitsTable() tableList = [(commits_table, None)] condList = [] if len(query.repository_list): tableList.append(("repositories", "(%s.repositoryid=repositories.id)" % (commits_table))) temp = self.SQLQueryListString("repositories.repository", query.repository_list) condList.append(temp) if len(query.branch_list): tableList.append(("branches", "(%s.branchid=branches.id)" % (commits_table))) temp = self.SQLQueryListString("branches.branch", query.branch_list) condList.append(temp) if len(query.directory_list): tableList.append(("dirs", "(%s.dirid=dirs.id)" % (commits_table))) temp = self.SQLQueryListString("dirs.dir", query.directory_list) condList.append(temp) if len(query.file_list): tableList.append(("files", "(%s.fileid=files.id)" % (commits_table))) temp = self.SQLQueryListString("files.file", query.file_list) condList.append(temp) if len(query.author_list): tableList.append(("people", "(%s.whoid=people.id)" % (commits_table))) temp = self.SQLQueryListString("people.who", query.author_list) condList.append(temp) if len(query.comment_list): tableList.append(("descs", "(%s.descid=descs.id)" % (commits_table))) temp = self.SQLQueryListString("descs.description", query.comment_list) condList.append(temp) if query.from_date: temp = "(%s.ci_when>=\"%s\")" \ % (commits_table, str(query.from_date)) condList.append(temp) if query.to_date: temp = "(%s.ci_when<=\"%s\")" \ % (commits_table, str(query.to_date)) condList.append(temp) if query.sort == "date": order_by = "ORDER BY %s.ci_when DESC,descid" % (commits_table) elif query.sort == "author": tableList.append(("people", "(%s.whoid=people.id)" % (commits_table))) order_by = "ORDER BY people.who,descid" elif query.sort == "file": tableList.append(("files", "(%s.fileid=files.id)" % (commits_table))) order_by = "ORDER BY files.file,descid" ## exclude duplicates from the table list, and split out join ## conditions from table names. In future, the join conditions ## might be handled by INNER JOIN statements instead of WHERE ## clauses, but MySQL 3.22 apparently doesn't support them well. tables = [] joinConds = [] for (table, cond) in tableList: if table not in tables: tables.append(table) if cond is not None: joinConds.append(cond) tables = string.join(tables, ",") conditions = string.join(joinConds + condList, " AND ") conditions = conditions and "WHERE %s" % conditions ## apply the query's row limit, if any (so we avoid really ## slamming a server with a large database) limit = "" if query.limit: if detect_leftover: limit = "LIMIT %s" % (str(query.limit + 1)) else: limit = "LIMIT %s" % (str(query.limit)) sql = "SELECT %s.* FROM %s %s %s %s" \ % (commits_table, tables, conditions, order_by, limit) return sql def RunQuery(self, query): sql = self.CreateSQLQueryString(query, 1) cursor = self.db.cursor() cursor.execute(sql) query.SetExecuted() row_count = 0 while 1: row = cursor.fetchone() if not row: break row_count = row_count + 1 if query.limit and (row_count > query.limit): query.SetLimitReached() break (dbType, dbCI_When, dbAuthorID, dbRepositoryID, dbDirID, dbFileID, dbRevision, dbStickyTag, dbBranchID, dbAddedLines, dbRemovedLines, dbDescID) = row commit = LazyCommit(self) if dbType == 'Add': commit.SetTypeAdd() elif dbType == 'Remove': commit.SetTypeRemove() else: commit.SetTypeChange() commit.SetTime(dbi.TicksFromDateTime(dbCI_When)) commit.SetFileID(dbFileID) commit.SetDirectoryID(dbDirID) commit.SetRevision(dbRevision) commit.SetRepositoryID(dbRepositoryID) commit.SetAuthorID(dbAuthorID) commit.SetBranchID(dbBranchID) commit.SetPlusCount(dbAddedLines) commit.SetMinusCount(dbRemovedLines) commit.SetDescriptionID(dbDescID) query.AddCommit(commit) def CheckCommit(self, commit): repository_id = self.GetRepositoryID(commit.GetRepository(), 0) if repository_id == None: return None dir_id = self.GetDirectoryID(commit.GetDirectory(), 0) if dir_id == None: return None file_id = self.GetFileID(commit.GetFile(), 0) if file_id == None: return None sql = "SELECT type, ci_when, whoid, repositoryid, dirid, fileid, " \ "revision, stickytag, branchid, addedlines, removedlines, " \ "descid "\ " FROM %s WHERE "\ " repositoryid=%%s "\ " AND dirid=%%s"\ " AND fileid=%%s"\ " AND revision=%%s"\ % (self.GetCommitsTable()) sql_args = (repository_id, dir_id, file_id, commit.GetRevision()) cursor = self.db.cursor() cursor.execute(sql, sql_args) try: (ci_type, ci_when, who_id, repository_id, dir_id, file_id, revision, sticky_tag, branch_id, plus_count, minus_count, description_id) = cursor.fetchone() except TypeError: return None return commit def sql_delete(self, table, key, value, keep_fkey = None): sql = "DELETE FROM %s WHERE %s=%%s" % (table, key) sql_args = (value, ) if keep_fkey: sql += " AND %s NOT IN (SELECT %s FROM %s WHERE %s = %%s)" \ % (key, keep_fkey, self.GetCommitsTable(), keep_fkey) sql_args = (value, value) cursor = self.db.cursor() cursor.execute(sql, sql_args) def sql_purge(self, table, key, fkey, ftable): sql = "DELETE FROM %s WHERE %s NOT IN (SELECT %s FROM %s)" \ % (table, key, fkey, ftable) cursor = self.db.cursor() cursor.execute(sql) def PurgeRepository(self, repository): rep_id = self.GetRepositoryID(repository, auto_set=0) if not rep_id: raise UnknownRepositoryError("Unknown repository '%s'" % (repository)) if (self._version >= 1): self.sql_delete('repositories', 'id', rep_id) self.sql_purge('commits', 'repositoryid', 'id', 'repositories') self.sql_purge('files', 'id', 'fileid', 'commits') self.sql_purge('dirs', 'id', 'dirid', 'commits') self.sql_purge('branches', 'id', 'branchid', 'commits') self.sql_purge('descs', 'id', 'descid', 'commits') self.sql_purge('people', 'id', 'whoid', 'commits') else: sql = "SELECT type, ci_when, whoid, repositoryid, dirid, " \ "fileid, revision, stickytag, branchid, addedlines, " \ "removedlines, descid "\ " FROM checkins WHERE repositoryid=%s" sql_args = (rep_id, ) cursor = self.db.cursor() cursor.execute(sql, sql_args) checkins = [] while 1: try: (ci_type, ci_when, who_id, repository_id, dir_id, file_id, revision, sticky_tag, branch_id, plus_count, minus_count, description_id) = \ cursor.fetchone() except TypeError: break checkins.append([file_id, dir_id, branch_id, description_id, who_id]) #self.sql_delete('repositories', 'id', rep_id) self.sql_delete('checkins', 'repositoryid', rep_id) for checkin in checkins: self.sql_delete('files', 'id', checkin[0], 'fileid') self.sql_delete('dirs', 'id', checkin[1], 'dirid') self.sql_delete('branches', 'id', checkin[2], 'branchid') self.sql_delete('descs', 'id', checkin[3], 'descid') self.sql_delete('people', 'id', checkin[4], 'whoid') # Reset all internal id caches. We could be choosier here, # but let's just be as safe as possible. self._get_cache = {} self._get_id_cache = {} self._desc_id_cache = {} class DatabaseVersionError(Exception): pass class UnknownRepositoryError(Exception): pass ## the Commit class holds data on one commit, the representation is as ## close as possible to how it should be committed and retrieved to the ## database engine class Commit: ## static constants for type of commit CHANGE = 0 ADD = 1 REMOVE = 2 def __init__(self): self.__directory = '' self.__file = '' self.__repository = '' self.__revision = '' self.__author = '' self.__branch = '' self.__pluscount = '' self.__minuscount = '' self.__description = '' self.__gmt_time = 0.0 self.__type = Commit.CHANGE def SetRepository(self, repository): self.__repository = repository def GetRepository(self): return self.__repository def SetDirectory(self, dir): self.__directory = dir def GetDirectory(self): return self.__directory def SetFile(self, file): self.__file = file def GetFile(self): return self.__file def SetRevision(self, revision): self.__revision = revision def GetRevision(self): return self.__revision def SetTime(self, gmt_time): if gmt_time is None: ### We're just going to assume that a datestamp of The Epoch ### ain't real. self.__gmt_time = 0.0 else: self.__gmt_time = float(gmt_time) def GetTime(self): return self.__gmt_time and self.__gmt_time or None def SetAuthor(self, author): self.__author = author def GetAuthor(self): return self.__author def SetBranch(self, branch): self.__branch = branch or '' def GetBranch(self): return self.__branch def SetPlusCount(self, pluscount): self.__pluscount = pluscount def GetPlusCount(self): return self.__pluscount def SetMinusCount(self, minuscount): self.__minuscount = minuscount def GetMinusCount(self): return self.__minuscount def SetDescription(self, description): self.__description = description def GetDescription(self): return self.__description def SetTypeChange(self): self.__type = Commit.CHANGE def SetTypeAdd(self): self.__type = Commit.ADD def SetTypeRemove(self): self.__type = Commit.REMOVE def GetType(self): return self.__type def GetTypeString(self): if self.__type == Commit.CHANGE: return 'Change' elif self.__type == Commit.ADD: return 'Add' elif self.__type == Commit.REMOVE: return 'Remove' ## LazyCommit overrides a few methods of Commit to only retrieve ## it's properties as they are needed class LazyCommit(Commit): def __init__(self, db): Commit.__init__(self) self.__db = db def SetFileID(self, dbFileID): self.__dbFileID = dbFileID def GetFileID(self): return self.__dbFileID def GetFile(self): return self.__db.GetFile(self.__dbFileID) def SetDirectoryID(self, dbDirID): self.__dbDirID = dbDirID def GetDirectoryID(self): return self.__dbDirID def GetDirectory(self): return self.__db.GetDirectory(self.__dbDirID) def SetRepositoryID(self, dbRepositoryID): self.__dbRepositoryID = dbRepositoryID def GetRepositoryID(self): return self.__dbRepositoryID def GetRepository(self): return self.__db.GetRepository(self.__dbRepositoryID) def SetAuthorID(self, dbAuthorID): self.__dbAuthorID = dbAuthorID def GetAuthorID(self): return self.__dbAuthorID def GetAuthor(self): return self.__db.GetAuthor(self.__dbAuthorID) def SetBranchID(self, dbBranchID): self.__dbBranchID = dbBranchID def GetBranchID(self): return self.__dbBranchID def GetBranch(self): return self.__db.GetBranch(self.__dbBranchID) def SetDescriptionID(self, dbDescID): self.__dbDescID = dbDescID def GetDescriptionID(self): return self.__dbDescID def GetDescription(self): return self.__db.GetDescription(self.__dbDescID) ## QueryEntry holds data on one match-type in the SQL database ## match is: "exact", "like", or "regex" class QueryEntry: def __init__(self, data, match): self.data = data self.match = match ## CheckinDatabaseQuery is an object which contains the search ## parameters for a query to the Checkin Database and -- after the ## query is executed -- the data returned by the query. class CheckinDatabaseQuery: def __init__(self): ## sorting self.sort = "date" ## repository to query self.repository_list = [] self.branch_list = [] self.directory_list = [] self.file_list = [] self.author_list = [] self.comment_list = [] ## date range in DBI 2.0 timedate objects self.from_date = None self.to_date = None ## limit on number of rows to return self.limit = None self.limit_reached = 0 ## list of commits -- filled in by CVS query self.commit_list = [] ## commit_cb provides a callback for commits as they ## are added self.commit_cb = None ## has this query been run? self.executed = 0 def SetRepository(self, repository, match = "exact"): self.repository_list.append(QueryEntry(repository, match)) def SetBranch(self, branch, match = "exact"): self.branch_list.append(QueryEntry(branch, match)) def SetDirectory(self, directory, match = "exact"): self.directory_list.append(QueryEntry(directory, match)) def SetFile(self, file, match = "exact"): self.file_list.append(QueryEntry(file, match)) def SetAuthor(self, author, match = "exact"): self.author_list.append(QueryEntry(author, match)) def SetComment(self, comment, match = "exact"): self.comment_list.append(QueryEntry(comment, match)) def SetSortMethod(self, sort): self.sort = sort def SetFromDateObject(self, ticks): self.from_date = dbi.DateTimeFromTicks(ticks) def SetToDateObject(self, ticks): self.to_date = dbi.DateTimeFromTicks(ticks) def SetFromDateHoursAgo(self, hours_ago): ticks = time.time() - (3600 * hours_ago) self.from_date = dbi.DateTimeFromTicks(ticks) def SetFromDateDaysAgo(self, days_ago): ticks = time.time() - (86400 * days_ago) self.from_date = dbi.DateTimeFromTicks(ticks) def SetToDateDaysAgo(self, days_ago): ticks = time.time() - (86400 * days_ago) self.to_date = dbi.DateTimeFromTicks(ticks) def SetLimit(self, limit): self.limit = limit; def AddCommit(self, commit): self.commit_list.append(commit) def SetExecuted(self): self.executed = 1 def SetLimitReached(self): self.limit_reached = 1 def GetLimitReached(self): assert self.executed return self.limit_reached def GetCommitList(self): assert self.executed return self.commit_list ## ## entrypoints ## def CreateCommit(): return Commit() def CreateCheckinQuery(): return CheckinDatabaseQuery() def ConnectDatabase(cfg, readonly=0): if readonly: user = cfg.cvsdb.readonly_user passwd = cfg.cvsdb.readonly_passwd else: user = cfg.cvsdb.user passwd = cfg.cvsdb.passwd db = CheckinDatabase(cfg.cvsdb.host, cfg.cvsdb.port, user, passwd, cfg.cvsdb.database_name) db.Connect() return db def ConnectDatabaseReadOnly(cfg): return ConnectDatabase(cfg, 1) def GetCommitListFromRCSFile(repository, path_parts, revision=None): commit_list = [] directory = string.join(path_parts[:-1], "/") file = path_parts[-1] revs = repository.itemlog(path_parts, revision, vclib.SORTBY_DEFAULT, 0, 0, {"cvs_pass_rev": 1}) for rev in revs: commit = CreateCommit() commit.SetRepository(repository.rootpath) commit.SetDirectory(directory) commit.SetFile(file) commit.SetRevision(rev.string) commit.SetAuthor(rev.author) commit.SetDescription(rev.log) commit.SetTime(rev.date) if rev.changed: # extract the plus/minus and drop the sign plus, minus = string.split(rev.changed) commit.SetPlusCount(plus[1:]) commit.SetMinusCount(minus[1:]) if rev.dead: commit.SetTypeRemove() else: commit.SetTypeChange() else: commit.SetTypeAdd() commit_list.append(commit) # if revision is on a branch which has at least one tag if len(rev.number) > 2 and rev.branches: commit.SetBranch(rev.branches[0].name) return commit_list def GetUnrecordedCommitList(repository, path_parts, db): commit_list = GetCommitListFromRCSFile(repository, path_parts) unrecorded_commit_list = [] for commit in commit_list: result = db.CheckCommit(commit) if not result: unrecorded_commit_list.append(commit) return unrecorded_commit_list _re_likechars = re.compile(r"([_%\\])") def EscapeLike(literal): """Escape literal string for use in a MySQL LIKE pattern""" return re.sub(_re_likechars, r"\\\1", literal) def FindRepository(db, path): """Find repository path in database given path to subdirectory Returns normalized repository path and relative directory path""" path = os.path.normpath(path) dirs = [] while path: rep = os.path.normcase(path) if db.GetRepositoryID(rep, 0) is None: path, pdir = os.path.split(path) if not pdir: return None, None dirs.append(pdir) else: break dirs.reverse() return rep, dirs def CleanRepository(path): """Return normalized top-level repository path""" return os.path.normcase(os.path.normpath(path)) viewvc-1.1.22/lib/viewvc.py0000644000175000017500000053461412265242240015741 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # viewvc: View CVS/SVN repositories via a web browser # # ----------------------------------------------------------------------- __version__ = '1.1.22' # this comes from our library; measure the startup time import debug debug.t_start('startup') debug.t_start('imports') # standard modules that we know are in the path or builtin import sys import os import fnmatch import gzip import mimetypes import re import rfc822 import stat import string import struct import tempfile import time import types import urllib # These modules come from our library (the stub has set up the path) import accept import compat import config import ezt import popen import sapi import vcauth import vclib import vclib.ccvs import vclib.svn try: import idiff except (SyntaxError, ImportError): idiff = None debug.t_end('imports') ######################################################################### checkout_magic_path = '*checkout*' # According to RFC 1738 the '~' character is unsafe in URLs. # But for compatibility with URLs bookmarked with old releases of ViewCVS: oldstyle_checkout_magic_path = '~checkout~' docroot_magic_path = '*docroot*' viewcvs_mime_type = 'text/vnd.viewcvs-markup' alt_mime_type = 'text/x-cvsweb-markup' view_roots_magic = '*viewroots*' # Put here the variables we need in order to hold our state - they # will be added (with their current value) to (almost) any link/query # string you construct. _sticky_vars = [ 'hideattic', 'sortby', 'sortdir', 'logsort', 'diff_format', 'search', 'limit_changes', ] # for reading/writing between a couple descriptors CHUNK_SIZE = 8192 # for rcsdiff processing of header _RCSDIFF_IS_BINARY = 'binary-diff' _RCSDIFF_ERROR = 'error' # special characters that don't need to be URL encoded _URL_SAFE_CHARS = "/*~" class Request: def __init__(self, server, cfg): self.server = server self.cfg = cfg self.script_name = _normalize_path(server.getenv('SCRIPT_NAME', '')) self.browser = server.getenv('HTTP_USER_AGENT', 'unknown') # process the Accept-Language: header, and load the key/value # files, given the selected language hal = server.getenv('HTTP_ACCEPT_LANGUAGE','') try: self.lang_selector = accept.language(hal) except accept.AcceptLanguageParseError: self.lang_selector = accept.language('en') self.language = self.lang_selector.select_from(cfg.general.languages) self.kv = cfg.load_kv_files(self.language) # check for an authenticated username self.username = server.getenv('REMOTE_USER') # if we allow compressed output, see if the client does too self.gzip_compress_level = 0 if cfg.options.allow_compress: http_accept_encoding = os.environ.get("HTTP_ACCEPT_ENCODING", "") if "gzip" in filter(None, map(lambda x: string.strip(x), string.split(http_accept_encoding, ","))): self.gzip_compress_level = 9 # make this configurable? def run_viewvc(self): cfg = self.cfg # This function first parses the query string and sets the following # variables. Then it executes the request. self.view_func = None # function to call to process the request self.repos = None # object representing current repository self.rootname = None # name of current root (as used in viewvc.conf) self.roottype = None # current root type ('svn' or 'cvs') self.rootpath = None # physical path to current root self.pathtype = None # type of path, either vclib.FILE or vclib.DIR self.where = None # path to file or directory in current root self.query_dict = {} # validated and cleaned up query options self.path_parts = None # for convenience, equals where.split('/') self.pathrev = None # current path revision or tag self.auth = None # authorizer module in use # redirect if we're loading from a valid but irregular URL # These redirects aren't neccessary to make ViewVC work, it functions # just fine without them, but they make it easier for server admins to # implement access restrictions based on URL needs_redirect = 0 # Process the query params for name, values in self.server.params().items(): # we only care about the first value value = values[0] # patch up old queries that use 'cvsroot' to look like they used 'root' if name == 'cvsroot': name = 'root' needs_redirect = 1 # same for 'only_with_tag' and 'pathrev' if name == 'only_with_tag': name = 'pathrev' needs_redirect = 1 # redirect view=rev to view=revision, too if name == 'view' and value == 'rev': value = 'revision' needs_redirect = 1 # validate the parameter _validate_param(name, value) # if we're here, then the parameter is okay self.query_dict[name] = value # Resolve the view parameter into a handler function. self.view_func = _views.get(self.query_dict.get('view', None), self.view_func) # Process PATH_INFO component of query string path_info = self.server.getenv('PATH_INFO', '') # clean it up. this removes duplicate '/' characters and any that may # exist at the front or end of the path. ### we might want to redirect to the cleaned up URL path_parts = _path_parts(path_info) if path_parts: # handle magic path prefixes if path_parts[0] == docroot_magic_path: # if this is just a simple hunk of doc, then serve it up self.where = _path_join(path_parts[1:]) return view_doc(self) elif path_parts[0] in (checkout_magic_path, oldstyle_checkout_magic_path): path_parts.pop(0) self.view_func = view_checkout if not cfg.options.checkout_magic: needs_redirect = 1 # handle tarball magic suffixes if self.view_func is download_tarball: if (self.query_dict.get('parent')): del path_parts[-1] elif path_parts[-1][-7:] == ".tar.gz": path_parts[-1] = path_parts[-1][:-7] # Figure out root name self.rootname = self.query_dict.get('root') if self.rootname == view_roots_magic: del self.query_dict['root'] self.rootname = "" needs_redirect = 1 elif self.rootname is None: if cfg.options.root_as_url_component: if path_parts: self.rootname = path_parts.pop(0) else: self.rootname = "" elif self.view_func != view_roots: self.rootname = cfg.general.default_root elif cfg.options.root_as_url_component: needs_redirect = 1 self.where = _path_join(path_parts) self.path_parts = path_parts if self.rootname: roottype, rootpath = locate_root(cfg, self.rootname) if roottype: # Overlay root-specific options. cfg.overlay_root_options(self.rootname) # Setup an Authorizer for this rootname and username debug.t_start('setup-authorizer') self.auth = setup_authorizer(cfg, self.username) debug.t_end('setup-authorizer') # Create the repository object debug.t_start('select-repos') try: if roottype == 'cvs': self.rootpath = vclib.ccvs.canonicalize_rootpath(rootpath) self.repos = vclib.ccvs.CVSRepository(self.rootname, self.rootpath, self.auth, cfg.utilities, cfg.options.use_rcsparse) # required so that spawned rcs programs correctly expand # $CVSHeader$ os.environ['CVSROOT'] = self.rootpath elif roottype == 'svn': self.rootpath = vclib.svn.canonicalize_rootpath(rootpath) self.repos = vclib.svn.SubversionRepository(self.rootname, self.rootpath, self.auth, cfg.utilities, cfg.options.svn_config_dir) else: raise vclib.ReposNotFound() except vclib.ReposNotFound: pass debug.t_end('select-repos') if self.repos is None: raise debug.ViewVCException( 'The root "%s" is unknown. If you believe the value is ' 'correct, then please double-check your configuration.' % self.rootname, "404 Not Found") if self.repos: debug.t_start('select-repos') self.repos.open() debug.t_end('select-repos') type = self.repos.roottype() if type == vclib.SVN: self.roottype = 'svn' elif type == vclib.CVS: self.roottype = 'cvs' else: raise debug.ViewVCException( 'The root "%s" has an unknown type ("%s"). Expected "cvs" or "svn".' % (self.rootname, type), "500 Internal Server Error") # If this is using an old-style 'rev' parameter, redirect to new hotness. # Subversion URLs will now use 'pathrev'; CVS ones use 'revision'. if self.repos and self.query_dict.has_key('rev'): if self.roottype == 'svn' \ and not self.query_dict.has_key('pathrev') \ and not self.view_func == view_revision: self.query_dict['pathrev'] = self.query_dict['rev'] del self.query_dict['rev'] else: # elif not self.query_dict.has_key('revision'): ? self.query_dict['revision'] = self.query_dict['rev'] del self.query_dict['rev'] needs_redirect = 1 if self.repos and self.view_func is not redirect_pathrev: # If this is an intended-to-be-hidden CVSROOT path, complain. if cfg.options.hide_cvsroot \ and is_cvsroot_path(self.roottype, path_parts): raise debug.ViewVCException("Unknown location: /%s" % self.where, "404 Not Found") # Make sure path exists self.pathrev = pathrev = self.query_dict.get('pathrev') self.pathtype = _repos_pathtype(self.repos, path_parts, pathrev) if self.pathtype is None: # Path doesn't exist, see if it could be an old-style ViewVC URL # with a fake suffix. result = _strip_suffix('.diff', path_parts, pathrev, vclib.FILE, \ self.repos, view_diff) or \ _strip_suffix('.tar.gz', path_parts, pathrev, vclib.DIR, \ self.repos, download_tarball) or \ _strip_suffix('root.tar.gz', path_parts, pathrev, vclib.DIR,\ self.repos, download_tarball) or \ _strip_suffix(self.rootname + '-root.tar.gz', \ path_parts, pathrev, vclib.DIR, \ self.repos, download_tarball) or \ _strip_suffix('root', \ path_parts, pathrev, vclib.DIR, \ self.repos, download_tarball) or \ _strip_suffix(self.rootname + '-root', \ path_parts, pathrev, vclib.DIR, \ self.repos, download_tarball) if result: self.path_parts, self.pathtype, self.view_func = result self.where = _path_join(self.path_parts) needs_redirect = 1 else: raise debug.ViewVCException("Unknown location: /%s" % self.where, "404 Not Found") # If we have an old ViewCVS Attic URL which is still valid, redirect if self.roottype == 'cvs': attic_parts = None if (self.pathtype == vclib.FILE and len(self.path_parts) > 1 and self.path_parts[-2] == 'Attic'): attic_parts = self.path_parts[:-2] + self.path_parts[-1:] elif (self.pathtype == vclib.DIR and len(self.path_parts) > 0 and self.path_parts[-1] == 'Attic'): attic_parts = self.path_parts[:-1] if attic_parts: self.path_parts = attic_parts self.where = _path_join(attic_parts) needs_redirect = 1 if self.view_func is None: # view parameter is not set, try looking at pathtype and the # other parameters if not self.rootname: self.view_func = view_roots elif self.pathtype == vclib.DIR: # ViewCVS 0.9.2 used to put ?tarball=1 at the end of tarball urls if self.query_dict.has_key('tarball'): self.view_func = download_tarball else: self.view_func = view_directory elif self.pathtype == vclib.FILE: if self.query_dict.has_key('r1') and self.query_dict.has_key('r2'): self.view_func = view_diff elif self.query_dict.has_key('annotate'): self.view_func = view_annotate elif self.query_dict.has_key('graph'): if not self.query_dict.has_key('makeimage'): self.view_func = view_cvsgraph else: self.view_func = view_cvsgraph_image elif self.query_dict.has_key('revision') \ or cfg.options.default_file_view != "log": if cfg.options.default_file_view == "markup" \ or self.query_dict.get('content-type', None) \ in (viewcvs_mime_type, alt_mime_type): self.view_func = view_markup else: self.view_func = view_checkout else: self.view_func = view_log # If we've chosen the roots or revision view, our effective # location is not really "inside" the repository, so we have no # path and therefore no path parts or type, either. if self.view_func is view_revision or self.view_func is view_roots: self.where = '' self.path_parts = [] self.pathtype = None # if we have a directory and the request didn't end in "/", then redirect # so that it does. if (self.pathtype == vclib.DIR and path_info[-1:] != '/' and self.view_func is not download_tarball and self.view_func is not redirect_pathrev): needs_redirect = 1 # startup is done now. debug.t_end('startup') # If we need to redirect, do so. Otherwise, handle our requested view. if needs_redirect: self.server.redirect(self.get_url()) else: debug.t_start('view-func') self.view_func(self) debug.t_end('view-func') def get_url(self, escape=0, partial=0, prefix=0, **args): """Constructs a link to another ViewVC page just like the get_link function except that it returns a single URL instead of a URL split into components. If PREFIX is set, include the protocol and server name portions of the URL.""" url, params = apply(self.get_link, (), args) qs = compat.urlencode(params) if qs: result = urllib.quote(url, _URL_SAFE_CHARS) + '?' + qs else: result = urllib.quote(url, _URL_SAFE_CHARS) if partial: result = result + (qs and '&' or '?') if escape: result = self.server.escape(result) if prefix: result = '%s://%s%s' % \ (self.server.getenv("HTTPS") == "on" and "https" or "http", self.server.getenv("HTTP_HOST"), result) return result def get_form(self, **args): """Constructs a link to another ViewVC page just like the get_link function except that it returns a base URL suitable for use as an HTML form action, and an iterable object with .name and .value attributes representing stuff that should be in tags with the link parameters.""" url, params = apply(self.get_link, (), args) action = self.server.escape(urllib.quote(url, _URL_SAFE_CHARS)) hidden_values = [] for name, value in params.items(): hidden_values.append(_item(name=self.server.escape(name), value=self.server.escape(value))) return action, hidden_values def get_link(self, view_func=None, where=None, pathtype=None, params=None): """Constructs a link pointing to another ViewVC page. All arguments correspond to members of the Request object. If they are set to None they take values from the current page. Return value is a base URL and a dictionary of parameters""" cfg = self.cfg if view_func is None: view_func = self.view_func if params is None: params = self.query_dict.copy() else: params = params.copy() # must specify both where and pathtype or neither assert (where is None) == (pathtype is None) # if we are asking for the revision info view, we don't need any # path information if (view_func is view_revision or view_func is view_roots or view_func is redirect_pathrev): where = pathtype = None elif where is None: where = self.where pathtype = self.pathtype # no need to add sticky variables for views with no links sticky_vars = not (view_func is view_checkout or view_func is download_tarball) # The logic used to construct the URL is an inverse of the # logic used to interpret URLs in Request.run_viewvc url = self.script_name # add checkout magic if neccessary if view_func is view_checkout and cfg.options.checkout_magic: url = url + '/' + checkout_magic_path # add root to url rootname = None if view_func is not view_roots: if cfg.options.root_as_url_component: # remove root from parameter list if present try: rootname = params['root'] except KeyError: rootname = self.rootname else: del params['root'] # add root path component if rootname is not None: url = url + '/' + rootname else: # add root to parameter list try: rootname = params['root'] except KeyError: rootname = params['root'] = self.rootname # no need to specify default root if rootname == cfg.general.default_root: del params['root'] # add 'pathrev' value to parameter list if (self.pathrev is not None and not params.has_key('pathrev') and view_func is not view_revision and rootname == self.rootname): params['pathrev'] = self.pathrev # add path if where: url = url + '/' + where # add trailing slash for a directory if pathtype == vclib.DIR: url = url + '/' # normalize top level URLs for use in Location headers and A tags elif not url: url = '/' # no need to explicitly specify directory view for a directory if view_func is view_directory and pathtype == vclib.DIR: view_func = None # no need to explicitly specify roots view when in root_as_url # mode or there's no default root if view_func is view_roots and (cfg.options.root_as_url_component or not cfg.general.default_root): view_func = None # no need to explicitly specify annotate view when # there's an annotate parameter if view_func is view_annotate and params.get('annotate') is not None: view_func = None # no need to explicitly specify diff view when # there's r1 and r2 parameters if (view_func is view_diff and params.get('r1') is not None and params.get('r2') is not None): view_func = None # no need to explicitly specify checkout view when it's the default # view or when checkout_magic is enabled if view_func is view_checkout: if ((cfg.options.default_file_view == "co" and pathtype == vclib.FILE) or cfg.options.checkout_magic): view_func = None # no need to explicitly specify markup view when it's the default view if view_func is view_markup: if (cfg.options.default_file_view == "markup" \ and pathtype == vclib.FILE): view_func = None # set the view parameter view_code = _view_codes.get(view_func) if view_code and not (params.has_key('view') and params['view'] is None): params['view'] = view_code # add sticky values to parameter list if sticky_vars: for name in _sticky_vars: value = self.query_dict.get(name) if value is not None and not params.has_key(name): params[name] = value # remove null values from parameter list for name, value in params.items(): if value is None: del params[name] return url, params def _path_parts(path): """Split up a repository path into a list of path components""" # clean it up. this removes duplicate '/' characters and any that may # exist at the front or end of the path. return filter(None, string.split(path, '/')) def _normalize_path(path): """Collapse leading slashes in the script name You only get multiple slashes in the script name when users accidentally type urls like http://abc.com//viewvc.cgi/, but we correct for it because we output the script name in links and web browsers interpret //viewvc.cgi/ as http://viewvc.cgi/ """ i = 0 for c in path: if c != '/': break i = i + 1 if i: return path[i-1:] return path def _validate_param(name, value): """Validate whether the given value is acceptable for the param name. If the value is not allowed, then an error response is generated, and this function throws an exception. Otherwise, it simply returns None. """ # First things first -- check that we have a legal parameter name. try: validator = _legal_params[name] except KeyError: raise debug.ViewVCException( 'An illegal parameter name was provided.', '400 Bad Request') # Is there a validator? Is it a regex or a function? Validate if # we can, returning without incident on valid input. if validator is None: return elif hasattr(validator, 'match'): if validator.match(value): return else: if validator(value): return # If we get here, the input value isn't valid. raise debug.ViewVCException( 'An illegal value was provided for the "%s" parameter.' % (name), '400 Bad Request') def _validate_regex(value): ### we need to watch the flow of these parameters through the system ### to ensure they don't hit the page unescaped. otherwise, these ### parameters could constitute a CSS attack. try: re.compile(value) return True except: return None def _validate_view(value): # Return true iff VALUE is one of our allowed views. return _views.has_key(value) def _validate_mimetype(value): # For security purposes, we only allow mimetypes from a predefined set # thereof. return value in (viewcvs_mime_type, alt_mime_type, 'text/plain') # obvious things here. note that we don't need uppercase for alpha. _re_validate_alpha = re.compile('^[a-z]+$') _re_validate_number = re.compile('^[0-9]+$') _re_validate_boolint = re.compile('^[01]$') # when comparing two revs, we sometimes construct REV:SYMBOL, so ':' is needed _re_validate_revnum = re.compile('^[-_.a-zA-Z0-9:~\\[\\]/]*$') # date time values _re_validate_datetime = re.compile(r'^(\d\d\d\d-\d\d-\d\d(\s+\d\d:\d\d' '(:\d\d)?)?)?$') # the legal query parameters and their validation functions _legal_params = { 'root' : None, 'view' : _validate_view, 'search' : _validate_regex, 'p1' : None, 'p2' : None, 'hideattic' : _re_validate_boolint, 'limit_changes' : _re_validate_number, 'sortby' : _re_validate_alpha, 'sortdir' : _re_validate_alpha, 'logsort' : _re_validate_alpha, 'diff_format' : _re_validate_alpha, 'pathrev' : _re_validate_revnum, 'dir_pagestart' : _re_validate_number, 'log_pagestart' : _re_validate_number, 'annotate' : _re_validate_revnum, 'graph' : _re_validate_revnum, 'makeimage' : _re_validate_boolint, 'r1' : _re_validate_revnum, 'tr1' : _re_validate_revnum, 'r2' : _re_validate_revnum, 'tr2' : _re_validate_revnum, 'revision' : _re_validate_revnum, 'content-type' : _validate_mimetype, # for query 'file_match' : _re_validate_alpha, 'branch_match' : _re_validate_alpha, 'who_match' : _re_validate_alpha, 'comment_match' : _re_validate_alpha, 'dir' : None, 'file' : None, 'branch' : None, 'who' : None, 'comment' : None, 'querysort' : _re_validate_alpha, 'date' : _re_validate_alpha, 'hours' : _re_validate_number, 'mindate' : _re_validate_datetime, 'maxdate' : _re_validate_datetime, 'format' : _re_validate_alpha, # for redirect_pathrev 'orig_path' : None, 'orig_pathtype' : None, 'orig_pathrev' : None, 'orig_view' : None, # deprecated 'parent' : _re_validate_boolint, 'rev' : _re_validate_revnum, 'tarball' : _re_validate_boolint, 'hidecvsroot' : _re_validate_boolint, } def _path_join(path_parts): return string.join(path_parts, '/') def _strip_suffix(suffix, path_parts, rev, pathtype, repos, view_func): """strip the suffix from a repository path if the resulting path is of the specified type, otherwise return None""" if not path_parts: return None l = len(suffix) if path_parts[-1][-l:] == suffix: path_parts = path_parts[:] if len(path_parts[-1]) == l: del path_parts[-1] else: path_parts[-1] = path_parts[-1][:-l] t = _repos_pathtype(repos, path_parts, rev) if pathtype == t: return path_parts, t, view_func return None def _repos_pathtype(repos, path_parts, rev): """Return the type of a repository path, or None if the path doesn't exist""" try: return repos.itemtype(path_parts, rev) except vclib.ItemNotFound: return None def _orig_path(request, rev_param='revision', path_param=None): "Get original path of requested file at old revision before copies or moves" # The 'pathrev' variable is interpreted by nearly all ViewVC views to # provide a browsable snapshot of a repository at some point in its history. # 'pathrev' is a tag name for CVS repositories and a revision number for # Subversion repositories. It's automatically propagated between pages by # logic in the Request.get_link() function which adds it to links like a # sticky variable. When 'pathrev' is set, directory listings only include # entries that exist in the specified revision or tag. Similarly, log pages # will only show revisions preceding the point in history specified by # 'pathrev.' Markup, checkout, and annotate pages show the 'pathrev' # revision of files by default when no other revision is specified. # # In Subversion repositories, paths are always considered to refer to the # pathrev revision. For example, if there is a "circle.jpg" in revision 3, # which is renamed and modified as "square.jpg" in revision 4, the original # circle image is visible at the following URLs: # # *checkout*/circle.jpg?pathrev=3 # *checkout*/square.jpg?revision=3 # *checkout*/square.jpg?revision=3&pathrev=4 # # Note that the following: # # *checkout*/circle.jpg?rev=3 # # now gets redirected to one of the following URLs: # # *checkout*/circle.jpg?pathrev=3 (for Subversion) # *checkout*/circle.jpg?revision=3 (for CVS) # rev = request.query_dict.get(rev_param, request.pathrev) path = request.query_dict.get(path_param, request.where) if rev is not None and hasattr(request.repos, '_getrev'): try: pathrev = request.repos._getrev(request.pathrev) rev = request.repos._getrev(rev) except vclib.InvalidRevision: raise debug.ViewVCException('Invalid revision', '404 Not Found') return _path_parts(request.repos.get_location(path, pathrev, rev)), rev return _path_parts(path), rev def setup_authorizer(cfg, username, rootname=None): """Setup the authorizer. If ROOTNAME is provided, assume that per-root options have not been overlayed. Otherwise, assume they have (and fetch the authorizer for the configured root).""" if rootname is None: authorizer = cfg.options.authorizer params = cfg.get_authorizer_params() else: authorizer, params = cfg.get_authorizer_and_params_hack(rootname) # No configured authorizer? No problem. if not authorizer: return None # First, try to load a module with the configured name. import imp fp = None try: try: fp, path, desc = imp.find_module("%s" % (authorizer), vcauth.__path__) my_auth = imp.load_module('viewvc', fp, path, desc) except ImportError: raise debug.ViewVCException( 'Invalid authorizer (%s) specified for root "%s"' \ % (authorizer, rootname), '500 Internal Server Error') finally: if fp: fp.close() # Finally, instantiate our Authorizer. return my_auth.ViewVCAuthorizer(username, params) def check_freshness(request, mtime=None, etag=None, weak=0): cfg = request.cfg # See if we are supposed to disable etags (for debugging, usually) if not cfg.options.generate_etags: return 0 request_etag = request_mtime = None if etag is not None: if weak: etag = 'W/"%s"' % etag else: etag = '"%s"' % etag request_etag = request.server.getenv('HTTP_IF_NONE_MATCH') if mtime is not None: try: request_mtime = request.server.getenv('HTTP_IF_MODIFIED_SINCE') request_mtime = rfc822.mktime_tz(rfc822.parsedate_tz(request_mtime)) except: request_mtime = None # if we have an etag, use that for freshness checking. # if not available, then we use the last-modified time. # if not available, then the document isn't fresh. if etag is not None: isfresh = (request_etag == etag) elif mtime is not None: isfresh = (request_mtime >= mtime) else: isfresh = 0 # require revalidation after the configured amount of time if cfg and cfg.options.http_expiration_time >= 0: expiration = compat.formatdate(time.time() + cfg.options.http_expiration_time) request.server.addheader('Expires', expiration) request.server.addheader('Cache-Control', 'max-age=%d' % cfg.options.http_expiration_time) if isfresh: request.server.header(status='304 Not Modified') else: if etag is not None: request.server.addheader('ETag', etag) if mtime is not None: request.server.addheader('Last-Modified', compat.formatdate(mtime)) return isfresh def get_view_template(cfg, view_name, language="en"): # See if the configuration specifies a template for this view. If # not, use the default template path for this view. tname = vars(cfg.templates).get(view_name) or view_name + ".ezt" # Template paths are relative to the configurated template_dir (if # any, "templates" otherwise), so build the template path as such. tname = os.path.join(cfg.options.template_dir or "templates", tname) # Allow per-language template selection. tname = string.replace(tname, '%lang%', language) # Finally, construct the whole template path. tname = cfg.path(tname) debug.t_start('ezt-parse') template = ezt.Template(tname) debug.t_end('ezt-parse') return template def get_writeready_server_file(request, content_type=None, encoding=None, content_length=None, allow_compress=True): """Return a file handle to a response body stream, after outputting any queued special headers (on REQUEST.server) and (optionally) a 'Content-Type' header whose value is CONTENT_TYPE and character set is ENCODING. If CONTENT_LENGTH is provided and compression is not in use, also generate a 'Content-Length' header for this response. Callers my use ALLOW_COMPRESS to disable compression where it would otherwise be allowed. (Such as when transmitting an already-compressed response.) After this function is called, it is too late to add new headers to the response.""" if allow_compress and request.gzip_compress_level: request.server.addheader('Content-Encoding', 'gzip') elif content_length is not None: request.server.addheader('Content-Length', content_length) if content_type and encoding: request.server.header("%s; charset=%s" % (content_type, encoding)) elif content_type: request.server.header(content_type) else: request.server.header() if allow_compress and request.gzip_compress_level: fp = gzip.GzipFile('', 'wb', request.gzip_compress_level, request.server.file()) else: fp = request.server.file() return fp def generate_page(request, view_name, data, content_type=None): server_fp = get_writeready_server_file(request, content_type) template = get_view_template(request.cfg, view_name, request.language) template.generate(server_fp, data) def nav_path(request): """Return current path as list of items with "name" and "href" members The href members are view_directory links for directories and view_log links for files, but are set to None when the link would point to the current view""" if not request.repos: return [] is_dir = request.pathtype == vclib.DIR # add root item items = [] root_item = _item(name=request.server.escape(request.repos.name), href=None) if request.path_parts or request.view_func is not view_directory: root_item.href = request.get_url(view_func=view_directory, where='', pathtype=vclib.DIR, params={}, escape=1) items.append(root_item) # add path part items path_parts = [] for part in request.path_parts: path_parts.append(part) is_last = len(path_parts) == len(request.path_parts) item = _item(name=part, href=None) if not is_last or (is_dir and request.view_func is not view_directory): item.href = request.get_url(view_func=view_directory, where=_path_join(path_parts), pathtype=vclib.DIR, params={}, escape=1) elif not is_dir and request.view_func is not view_log: item.href = request.get_url(view_func=view_log, where=_path_join(path_parts), pathtype=vclib.FILE, params={}, escape=1) items.append(item) return items def prep_tags(request, tags): url, params = request.get_link(params={'pathrev': None}) params = compat.urlencode(params) if params: url = urllib.quote(url, _URL_SAFE_CHARS) + '?' + params + '&pathrev=' else: url = urllib.quote(url, _URL_SAFE_CHARS) + '?pathrev=' url = request.server.escape(url) links = [ ] for tag in tags: links.append(_item(name=tag.name, href=url+tag.name)) links.sort(lambda a, b: cmp(a.name, b.name)) return links def guess_mime(filename): return mimetypes.guess_type(filename)[0] def is_viewable_image(mime_type): return mime_type and mime_type in ('image/gif', 'image/jpeg', 'image/png') def is_text(mime_type): return not mime_type or mime_type[:5] == 'text/' def is_cvsroot_path(roottype, path_parts): return roottype == 'cvs' and path_parts and path_parts[0] == 'CVSROOT' def is_plain_text(mime_type): return not mime_type or mime_type == 'text/plain' def default_view(mime_type, cfg): "Determine whether file should be viewed through markup page or sent raw" # If the mime type is text/anything or a supported image format we view # through the markup page. If the mime type is something else, we send # it directly to the browser. That way users can see things like flash # animations, pdfs, word documents, multimedia, etc, which wouldn't be # very useful marked up. If the mime type is totally unknown (happens when # we encounter an unrecognized file extension) we also view it through # the markup page since that's better than sending it text/plain. if ('markup' in cfg.options.allowed_views and (is_viewable_image(mime_type) or is_text(mime_type))): return view_markup return view_checkout def is_binary_file_mime_type(mime_type, cfg): """Return True iff MIME_TYPE is set and matches one of the binary file mime type patterns in CFG.""" if mime_type: for pattern in cfg.options.binary_mime_types: if fnmatch.fnmatch(mime_type, pattern): return True return False def get_file_view_info(request, where, rev=None, mime_type=None, pathrev=-1): """Return an object holding common hrefs and a viewability flag used for various views of FILENAME at revision REV whose MIME type is MIME_TYPE. The object's members include: view_href download_href download_text_href annotate_href revision_href prefer_markup """ rev = rev and str(rev) or None mime_type = mime_type or guess_mime(where) if pathrev == -1: # cheesy default value, since we need to preserve None pathrev = request.pathrev view_href = None download_href = None download_text_href = None annotate_href = None revision_href = None if 'markup' in request.cfg.options.allowed_views: view_href = request.get_url(view_func=view_markup, where=where, pathtype=vclib.FILE, params={'revision': rev, 'pathrev': pathrev}, escape=1) if 'co' in request.cfg.options.allowed_views: download_href = request.get_url(view_func=view_checkout, where=where, pathtype=vclib.FILE, params={'revision': rev, 'pathrev': pathrev}, escape=1) if not is_plain_text(mime_type): download_text_href = request.get_url(view_func=view_checkout, where=where, pathtype=vclib.FILE, params={'content-type': 'text/plain', 'revision': rev, 'pathrev': pathrev}, escape=1) if 'annotate' in request.cfg.options.allowed_views: annotate_href = request.get_url(view_func=view_annotate, where=where, pathtype=vclib.FILE, params={'annotate': rev, 'pathrev': pathrev}, escape=1) if request.roottype == 'svn': revision_href = request.get_url(view_func=view_revision, params={'revision': rev}, escape=1) is_binary_file = is_binary_file_mime_type(mime_type, request.cfg) if is_binary_file: download_text_href = annotate_href = view_href = None prefer_markup = False else: prefer_markup = default_view(mime_type, request.cfg) == view_markup return _item(view_href=view_href, download_href=download_href, download_text_href=download_text_href, annotate_href=annotate_href, revision_href=revision_href, prefer_markup=ezt.boolean(prefer_markup)) # Matches URLs _re_rewrite_url = re.compile('((http|https|ftp|file|svn|svn\+ssh)' '(://[-a-zA-Z0-9%.~:_/]+)((\?|\&)' '([-a-zA-Z0-9%.~:_]+)=([-a-zA-Z0-9%.~:_])+)*' '(#([-a-zA-Z0-9%.~:_]+)?)?)') # Matches email addresses _re_rewrite_email = re.compile('([-a-zA-Z0-9_.\+]+)@' '(([-a-zA-Z0-9]+\.)+[A-Za-z]{2,4})') # Matches revision references _re_rewrite_svnrevref = re.compile(r'\b(r|rev #?|revision #?)([0-9]+)\b') class ViewVCHtmlFormatterTokens: def __init__(self, tokens): self.tokens = tokens def get_result(self, maxlen=0): """Format the tokens per the registered set of formatters, and limited to MAXLEN visible characters (or unlimited if MAXLEN is 0). Return a 3-tuple containing the formatted result string, the number of visible characters in the result string, and a boolean flag indicating whether or not S was truncated.""" out = '' out_len = 0 for token in self.tokens: chunk, chunk_len = token.converter(token.match, token.userdata, maxlen) out = out + chunk out_len = out_len + chunk_len if maxlen: maxlen = maxlen - chunk_len if maxlen <= 0: return out, out_len, 1 return out, out_len, 0 class ViewVCHtmlFormatter: """Format a string as HTML-encoded output with customizable markup rules, for example turning strings that look like URLs into anchor links. NOTE: While there might appear to be some unused portions of this interface, there is a good chance that there are consumers outside of ViewVC itself that make use of these things. """ def __init__(self): self._formatters = [] def format_url(self, mobj, userdata, maxlen=0): """Return a 2-tuple containing: - the text represented by MatchObject MOBJ, formatted as linkified URL, with no more than MAXLEN characters in the non-HTML-tag bits. If MAXLEN is 0, there is no maximum. - the number of non-HTML-tag characters returned. """ s = mobj.group(0) trunc_s = maxlen and s[:maxlen] or s return '%s' % (sapi.escape(s), sapi.escape(trunc_s)), \ len(trunc_s) def format_email(self, mobj, userdata, maxlen=0): """Return a 2-tuple containing: - the text represented by MatchObject MOBJ, formatted as linkified email address, with no more than MAXLEN characters in the non-HTML-tag bits. If MAXLEN is 0, there is no maximum. - the number of non-HTML-tag characters returned. """ s = mobj.group(0) trunc_s = maxlen and s[:maxlen] or s return '%s' % (urllib.quote(s), self._entity_encode(trunc_s)), \ len(trunc_s) def format_email_obfuscated(self, mobj, userdata, maxlen=0): """Return a 2-tuple containing: - the text represented by MatchObject MOBJ, formatted as an entity-encoded email address, with no more than MAXLEN characters in the non-HTML-tag bits. If MAXLEN is 0, there is no maximum. - the number of non-HTML-tag characters returned. """ s = mobj.group(0) trunc_s = maxlen and s[:maxlen] or s return self._entity_encode(trunc_s), len(trunc_s) def format_email_truncated(self, mobj, userdata, maxlen=0): """Return a 2-tuple containing: - the text represented by MatchObject MOBJ, formatted as an HTML-escaped truncated email address of no more than MAXLEN characters. If MAXLEN is 0, there is no maximum. - the number of characters returned. """ s = mobj.group(1) s_len = len(s) if (maxlen == 0) or (s_len < (maxlen - 1)): return self._entity_encode(s) + '@…', s_len + 2 elif s_len < maxlen: return self._entity_encode(s) + '@', s_len + 1 else: trunc_s = mobj.group(1)[:maxlen] return self._entity_encode(trunc_s), len(trunc_s) def format_svnrevref(self, mobj, userdata, maxlen=0): """Return a 2-tuple containing: - the text represented by MatchObject MOBJ, formatted as an linkified URL to a ViewVC Subversion revision view, with no more than MAXLEN characters in the non-HTML-tag portions. If MAXLEN is 0, there is no maximum. - the number of characters returned. USERDATA is a function that accepts a revision reference and returns a URL to that revision. """ s = mobj.group(0) revref = mobj.group(2) trunc_s = maxlen and s[:maxlen] or s revref_url = userdata(revref) return '%s' % (sapi.escape(revref_url), sapi.escape(trunc_s)), \ len(trunc_s) def format_custom_url(self, mobj, userdata, maxlen=0): """Return a 2-tuple containing: - the text represented by MatchObject MOBJ, formatted as an linkified URL created by substituting match groups 0-9 into USERDATA (which is a format string that uses \N to represent the substitution locations) and with no more than MAXLEN characters in the non-HTML-tag portions. If MAXLEN is 0, there is no maximum. - the number of characters returned. """ format = userdata text = mobj.group(0) url = format for i in range(9): try: repl = mobj.group(i) except: repl = '' url = url.replace('\%d' % (i), repl) trunc_s = maxlen and text[:maxlen] or text return '%s' % (sapi.escape(url), sapi.escape(trunc_s)), \ len(trunc_s) def format_text(self, s, unused, maxlen=0): """Return a 2-tuple containing: - the text S, HTML-escaped, containing no more than MAXLEN characters. If MAXLEN is 0, there is no maximum. - the number of characters returned. """ trunc_s = maxlen and s[:maxlen] or s return sapi.escape(trunc_s), len(trunc_s) def add_formatter(self, regexp, conv, userdata=None): """Register a formatter which finds instances of strings matching REGEXP, and using the function CONV and USERDATA to format them. CONV is a function which accepts three parameters: - the MatchObject which holds the string portion to be formatted, - the USERDATA object, - the maximum number of characters from that string to use for human-readable output (or 0 to indicate no maximum). """ if type(regexp) == type(''): regexp = re.compile(regexp) self._formatters.append([regexp, conv, userdata]) def get_result(self, s, maxlen=0): """Format S per the set of formatters registered with this object, and limited to MAXLEN visible characters (or unlimited if MAXLEN is 0). Return a 3-tuple containing the formatted result string, the number of visible characters in the result string, and a boolean flag indicating whether or not S was truncated. """ return self.tokenize_text(s).get_result(maxlen) def tokenize_text(self, s): """Return a ViewVCHtmlFormatterTokens object containing the tokens created when parsing the string S. Callers can use that object's get_result() function to retrieve HTML-formatted text. """ tokens = [] # We could just have a "while s:" here instead of "for line: while # line:", but for really large log messages with heavy # tokenization, the cost in both performance and memory # consumption of the approach taken was atrocious. for line in string.split(string.replace(s, '\r\n', '\n'), '\n'): line = line + '\n' while line: best_match = best_conv = best_userdata = None for test in self._formatters: match = test[0].search(line) # If we find and match and (a) its our first one, or (b) it # matches text earlier than our previous best match, or (c) it # matches text at the same location as our previous best match # but extends to cover more text than that match, then this is # our new best match. # # Implied here is that when multiple formatters match exactly # the same text, the first formatter in the registration list wins. if match \ and ((best_match is None) \ or (match.start() < best_match.start()) or ((match.start() == best_match.start()) \ and (match.end() > best_match.end()))): best_match = match best_conv = test[1] best_userdata = test[2] # If we found a match... if best_match: # ... add any non-matching stuff first, then the matching bit. start = best_match.start() end = best_match.end() if start > 0: tokens.append(_item(match=line[:start], converter=self.format_text, userdata=None)) tokens.append(_item(match=best_match, converter=best_conv, userdata=best_userdata)) line = line[end:] else: # Otherwise, just add the rest of the string. tokens.append(_item(match=line, converter=self.format_text, userdata=None)) line = '' return ViewVCHtmlFormatterTokens(tokens) def _entity_encode(self, s): return string.join(map(lambda x: '&#%d;' % (ord(x)), s), '') class LogFormatter: def __init__(self, request, log): self.request = request self.log = log or '' self.tokens = None self.cache = {} # (maxlen, htmlize) => resulting_log def get(self, maxlen=0, htmlize=1): cfg = self.request.cfg # Prefer the cache. if self.cache.has_key((maxlen, htmlize)): return self.cache[(maxlen, htmlize)] # If we are HTML-izing... if htmlize: # ...and we don't yet have ViewVCHtmlFormatter() object tokens... if not self.tokens: # ... then get them. lf = ViewVCHtmlFormatter() # Rewrite URLs. lf.add_formatter(_re_rewrite_url, lf.format_url) # Rewrite Subversion revision references. if self.request.roottype == 'svn': def revision_to_url(rev): return self.request.get_url(view_func=view_revision, params={'revision': rev}, escape=1) lf.add_formatter(_re_rewrite_svnrevref, lf.format_svnrevref, revision_to_url) # Rewrite email addresses. if cfg.options.mangle_email_addresses == 2: lf.add_formatter(_re_rewrite_email, lf.format_email_truncated) elif cfg.options.mangle_email_addresses == 1: lf.add_formatter(_re_rewrite_email, lf.format_email_obfuscated) else: lf.add_formatter(_re_rewrite_email, lf.format_email) # Add custom rewrite handling per configuration. for rule in cfg.options.custom_log_formatting: rule = rule.replace('\\:', '\x01') regexp, format = map(lambda x: x.strip(), rule.split(':', 1)) regexp = regexp.replace('\x01', ':') format = format.replace('\x01', ':') lf.add_formatter(re.compile(regexp), lf.format_custom_url, format) # Tokenize the log message. self.tokens = lf.tokenize_text(self.log) # Use our formatter to ... you know ... format. log, log_len, truncated = self.tokens.get_result(maxlen) result_log = log + (truncated and '…' or '') # But if we're not HTML-izing... else: # ...then do much more simplistic transformations as necessary. log = self.log if cfg.options.mangle_email_addresses == 2: log = re.sub(_re_rewrite_email, r'\1@...', log) result_log = maxlen and log[:maxlen] or log # In either case, populate the cache and return the results. self.cache[(maxlen, htmlize)] = result_log return result_log _time_desc = { 1 : 'second', 60 : 'minute', 3600 : 'hour', 86400 : 'day', 604800 : 'week', 2628000 : 'month', 31536000 : 'year', } def get_time_text(request, interval, num): "Get some time text, possibly internationalized." ### some languages have even harder pluralization rules. we'll have to ### deal with those on demand if num == 0: return '' text = _time_desc[interval] if num == 1: attr = text + '_singular' fmt = '%d ' + text else: attr = text + '_plural' fmt = '%d ' + text + 's' try: fmt = getattr(request.kv.i18n.time, attr) except AttributeError: pass return fmt % num def little_time(request): try: return request.kv.i18n.time.little_time except AttributeError: return 'very little time' def html_time(request, secs, extended=0): secs = long(time.time()) - secs if secs < 2: return little_time(request) breaks = _time_desc.keys() breaks.sort() i = 0 while i < len(breaks): if secs < 2 * breaks[i]: break i = i + 1 value = breaks[i - 1] s = get_time_text(request, value, secs / value) if extended and i > 1: secs = secs % value value = breaks[i - 2] ext = get_time_text(request, value, secs / value) if ext: ### this is not i18n compatible. pass on it for now s = s + ', ' + ext return s def common_template_data(request, revision=None, mime_type=None): """Return a ezt.TemplateData instance with data dictionary items common to most ViewVC views.""" cfg = request.cfg # Initialize data dictionary members (sorted alphanumerically) data = ezt.TemplateData({ 'annotate_href' : None, 'cfg' : cfg, 'docroot' : cfg.options.docroot is None \ and request.script_name + '/' + docroot_magic_path \ or cfg.options.docroot, 'download_href' : None, 'download_text_href' : None, 'graph_href': None, 'kv' : request.kv, 'lockinfo' : None, 'log_href' : None, 'nav_path' : nav_path(request), 'pathtype' : None, 'prefer_markup' : ezt.boolean(0), 'queryform_href' : None, 'rev' : None, 'revision_href' : None, 'rootname' : request.rootname \ and request.server.escape(request.rootname) or None, 'rootpath' : request.rootpath, 'roots_href' : None, 'roottype' : request.roottype, 'rss_href' : None, 'tarball_href' : None, 'up_href' : None, 'username' : request.username, 'view' : _view_codes[request.view_func], 'view_href' : None, 'vsn' : __version__, 'where' : request.server.escape(request.where), }) rev = revision if not rev: rev = request.query_dict.get('annotate') if not rev: rev = request.query_dict.get('revision') if not rev and request.roottype == 'svn': rev = request.query_dict.get('pathrev') try: data['rev'] = hasattr(request.repos, '_getrev') \ and request.repos._getrev(rev) or rev except vclib.InvalidRevision: raise debug.ViewVCException('Invalid revision', '404 Not Found') if request.pathtype == vclib.DIR: data['pathtype'] = 'dir' elif request.pathtype == vclib.FILE: data['pathtype'] = 'file' if request.path_parts: dir = _path_join(request.path_parts[:-1]) data['up_href'] = request.get_url(view_func=view_directory, where=dir, pathtype=vclib.DIR, params={}, escape=1) if 'roots' in cfg.options.allowed_views: data['roots_href'] = request.get_url(view_func=view_roots, escape=1, params={}) if request.pathtype == vclib.FILE: fvi = get_file_view_info(request, request.where, data['rev'], mime_type) data['view_href'] = fvi.view_href data['download_href'] = fvi.download_href data['download_text_href'] = fvi.download_text_href data['annotate_href'] = fvi.annotate_href data['revision_href'] = fvi.revision_href data['prefer_markup'] = fvi.prefer_markup data['log_href'] = request.get_url(view_func=view_log, params={}, escape=1) if request.roottype == 'cvs' and cfg.options.use_cvsgraph: data['graph_href'] = request.get_url(view_func=view_cvsgraph, params={}, escape=1) file_data = request.repos.listdir(request.path_parts[:-1], request.pathrev, {}) def _only_this_file(item): return item.name == request.path_parts[-1] entries = filter(_only_this_file, file_data) if len(entries) == 1: request.repos.dirlogs(request.path_parts[:-1], request.pathrev, entries, {}) data['lockinfo'] = entries[0].lockinfo elif request.pathtype == vclib.DIR: data['view_href'] = request.get_url(view_func=view_directory, params={}, escape=1) if 'tar' in cfg.options.allowed_views: data['tarball_href'] = request.get_url(view_func=download_tarball, params={}, escape=1) if request.roottype == 'svn': data['revision_href'] = request.get_url(view_func=view_revision, params={'revision': data['rev']}, escape=1) data['log_href'] = request.get_url(view_func=view_log, params={}, escape=1) if is_querydb_nonempty_for_root(request): if request.pathtype == vclib.DIR: params = {} if request.roottype == 'cvs' and request.pathrev: params['branch'] = request.pathrev data['queryform_href'] = request.get_url(view_func=view_queryform, params=params, escape=1) data['rss_href'] = request.get_url(view_func=view_query, params={'date': 'month', 'format': 'rss'}, escape=1) elif request.pathtype == vclib.FILE: parts = _path_parts(request.where) where = _path_join(parts[:-1]) data['rss_href'] = request.get_url(view_func=view_query, where=where, pathtype=request.pathtype, params={'date': 'month', 'format': 'rss', 'file': parts[-1], 'file_match': 'exact'}, escape=1) return data def retry_read(src, reqlen=CHUNK_SIZE): while 1: chunk = src.read(CHUNK_SIZE) if not chunk: # need to check for eof methods because the cStringIO file objects # returned by ccvs don't provide them if hasattr(src, 'eof') and src.eof() is None: time.sleep(1) continue return chunk def copy_stream(src, dst, htmlize=0): while 1: chunk = retry_read(src) if not chunk: break if htmlize: chunk = sapi.escape(chunk) dst.write(chunk) class MarkupPipeWrapper: """An EZT callback that outputs a filepointer, plus some optional pre- and post- text.""" def __init__(self, fp, pretext=None, posttext=None, htmlize=0): self.fp = fp self.pretext = pretext self.posttext = posttext self.htmlize = htmlize def __call__(self, ctx): if self.pretext: ctx.fp.write(self.pretext) copy_stream(self.fp, ctx.fp, self.htmlize) self.fp.close() if self.posttext: ctx.fp.write(self.posttext) _re_rewrite_escaped_url = re.compile('((http|https|ftp|file|svn|svn\+ssh)' '(://[-a-zA-Z0-9%.~:_/]+)' '((\?|\&amp;|\&|\&)' '([-a-zA-Z0-9%.~:_]+)=([-a-zA-Z0-9%.~:_])+)*' '(#([-a-zA-Z0-9%.~:_]+)?)?)') def markup_escaped_urls(s): # Return a copy of S with all URL references -- which are expected # to be already HTML-escaped -- wrapped in . def _url_repl(match_obj): url = match_obj.group(0) unescaped_url = string.replace(url, "&amp;", "&") return "%s" % (unescaped_url, url) return re.sub(_re_rewrite_escaped_url, _url_repl, s) def detect_encoding(text_block): """Return the encoding used by TEXT_BLOCK as detected by the chardet Python module. (Currently, this is used only when syntax highlighting is not enabled/available; otherwise, Pygments does this work for us.)""" # Does the TEXT_BLOCK start with a BOM? for bom, encoding in [('\xef\xbb\xbf', 'utf-8'), ('\xff\xfe', 'utf-16'), ('\xfe\xff', 'utf-16be'), ('\xff\xfe\0\0', 'utf-32'), ('\0\0\xfe\xff', 'utf-32be'), ]: if text_block[:len(bom)] == bom: return encoding # If no recognized BOM, see if chardet can help us. try: import chardet # If chardet can confidently claimed a match, we'll use its # findings. (And if that match is 'ascii' -- which is a subset of # utf-8 -- we'll just call it 'utf-8' and score a zero transform.) resp = chardet.detect(text_block) if resp.get('confidence') == 1.0: encoding = resp.get('encoding') if encoding is "ascii": encoding = "utf-8" return encoding except: pass # By default ... we have no idea. return None def transcode_text(text, encoding=None): """If ENCODING is provided and not 'utf-8', transcode TEXT from ENCODING to UTF-8.""" if not encoding or encoding == 'utf-8': return text try: return unicode(text, encoding, 'replace').encode('utf-8', 'replace') except: pass return text def markup_stream(request, cfg, blame_data, file_lines, filename, mime_type, encoding, colorize): """Return the contents of a versioned file as a list of vclib.Annotation objects, each representing one line of the file's contents. Use BLAME_DATA as the annotation information for the file if provided. Use FILE_LINES as the lines of file content text themselves. MIME_TYPE is the MIME content type of the file; ENCODING is its character encoding. If COLORIZE is true, attempt to apply syntax coloration to the file contents, and use the HTML-marked-up results as the text in the return vclib.Annotation objects.""" # Nothing to mark up? So be it. if not file_lines: return [] # Determine if we should (and can) use Pygments to highlight our # output. Reasons not to include a) being told not to by the # configuration, b) not being able to import the Pygments modules, # and c) Pygments not having a lexer for our file's format. pygments_lexer = None if colorize: from pygments import highlight from pygments.formatters import HtmlFormatter from pygments.lexers import ClassNotFound, \ get_lexer_by_name, \ get_lexer_for_mimetype, \ get_lexer_for_filename, \ guess_lexer if not encoding: encoding = 'guess' if cfg.options.detect_encoding: try: import chardet encoding = 'chardet' except (SyntaxError, ImportError): pass # First, see if there's a Pygments lexer associated with MIME_TYPE. if mime_type: try: pygments_lexer = get_lexer_for_mimetype(mime_type, encoding=encoding, tabsize=cfg.options.tabsize, stripnl=False) except ClassNotFound: pygments_lexer = None # If we've no lexer thus far, try to find one based on the FILENAME. if not pygments_lexer: try: pygments_lexer = get_lexer_for_filename(filename, encoding=encoding, tabsize=cfg.options.tabsize, stripnl=False) except ClassNotFound: pygments_lexer = None # Still no lexer? If we've reason to believe this is a text # file, try to guess the lexer based on the file's content. if not pygments_lexer and is_text(mime_type) and file_lines: try: pygments_lexer = guess_lexer(file_lines[0]) except ClassNotFound: pygments_lexer = None # If we aren't highlighting, just return an amalgamation of the # BLAME_DATA (if any) and the FILE_LINES. if not pygments_lexer: # If allowed by configuration, try to detect the source encoding # for this file. We'll assemble a block of data from the file # contents to do so... 1024 bytes should be enough. if not encoding and cfg.options.detect_encoding: block_size = 0 text_block = '' for i in range(len(file_lines)): text_block = text_block + file_lines[i] if len(text_block) >= 1024: break encoding = detect_encoding(text_block) # Built output data comprised of marked-up and possibly-transcoded # source text lines wrapped in (possibly dummy) vclib.Annotation # objects. lines = [] file_lines = transcode_text(string.join(file_lines, ''), encoding) file_lines = string.rstrip(file_lines, '\n') file_lines = string.split(file_lines, '\n') for i in range(len(file_lines)): line = file_lines[i] if cfg.options.tabsize > 0: line = string.expandtabs(line, cfg.options.tabsize) line = markup_escaped_urls(sapi.escape(line)) if blame_data: blame_item = blame_data[i] blame_item.text = line else: blame_item = vclib.Annotation(line, i + 1, None, None, None, None) blame_item.diff_href = None lines.append(blame_item) return lines # If we get here, we're highlighting something. class PygmentsSink: def __init__(self, blame_data): if blame_data: self.has_blame_data = 1 self.blame_data = blame_data else: self.has_blame_data = 0 self.blame_data = [] self.line_no = 0 def write(self, buf): ### FIXME: Don't bank on write() being called once per line buf = markup_escaped_urls(string.rstrip(buf, '\n\r')) if self.has_blame_data: self.blame_data[self.line_no].text = buf else: item = vclib.Annotation(buf, self.line_no + 1, None, None, None, None) item.diff_href = None self.blame_data.append(item) self.line_no = self.line_no + 1 ps = PygmentsSink(blame_data) highlight(string.join(file_lines, ''), pygments_lexer, HtmlFormatter(nowrap=True, classprefix="pygments-", encoding='utf-8'), ps) return ps.blame_data def make_time_string(date, cfg): """Returns formatted date string in either local time or UTC. The passed in 'date' variable is seconds since epoch. """ if date is None: return None if cfg.options.use_localtime: tm = time.localtime(date) else: tm = time.gmtime(date) if cfg.options.iso8601_timestamps: if cfg.options.use_localtime: if tm[8] and time.daylight: tz = time.altzone else: tz = time.timezone tz = float(tz) / 3600.0 tz = string.replace(str.format('{0:+06.2f}', tz), '.', ':') else: tz = 'Z' return time.strftime('%Y-%m-%dT%H:%M:%S', tm) + tz else: return time.asctime(tm) + ' ' + \ (cfg.options.use_localtime and time.tzname[tm[8]] or 'UTC') def make_rss_time_string(date, cfg): """Returns formatted date string in UTC, formatted for RSS. The passed in 'date' variable is seconds since epoch. """ if date is None: return None return time.strftime("%a, %d %b %Y %H:%M:%S", time.gmtime(date)) + ' UTC' def make_comma_sep_list_string(items): return string.join(map(lambda x: x.name, items), ', ') def get_itemprops(request, path_parts, rev): itemprops = request.repos.itemprops(path_parts, rev) propnames = itemprops.keys() propnames.sort() props = [] for name in propnames: lf = LogFormatter(request, itemprops[name]) value = lf.get(maxlen=0, htmlize=1) undisplayable = ezt.boolean(0) # skip non-utf8 property names try: unicode(name, 'utf8') except: continue # note non-utf8 property values try: unicode(value, 'utf8') except: value = None undisplayable = ezt.boolean(1) props.append(_item(name=name, value=value, undisplayable=undisplayable)) return props def parse_mime_type(mime_type): mime_parts = map(lambda x: x.strip(), string.split(mime_type, ';')) type_subtype = mime_parts[0].lower() parameters = {} for part in mime_parts[1:]: name, value = string.split(part, '=', 1) parameters[name] = value return type_subtype, parameters def calculate_mime_type(request, path_parts, rev): """Return a 2-tuple carrying the MIME content type and character encoding for the file represented by PATH_PARTS in REV. Use REQUEST for repository access as necessary.""" if not path_parts: return None, None mime_type = encoding = None if request.roottype == 'svn' \ and (not request.cfg.options.svn_ignore_mimetype): try: itemprops = request.repos.itemprops(path_parts, rev) mime_type = itemprops.get('svn:mime-type') if mime_type: mime_type, parameters = parse_mime_type(mime_type) return mime_type, parameters.get('charset') except: pass return guess_mime(path_parts[-1]), None def assert_viewable_filesize(cfg, filesize): if cfg.options.max_filesize_kbytes \ and filesize != -1 \ and filesize > (1024 * cfg.options.max_filesize_kbytes): raise debug.ViewVCException('Display of files larger than %d KB ' 'disallowed by configuration' % (cfg.options.max_filesize_kbytes), '403 Forbidden') def markup_or_annotate(request, is_annotate): cfg = request.cfg path, rev = _orig_path(request, is_annotate and 'annotate' or 'revision') lines = fp = image_src_href = None annotation = 'none' revision = None mime_type, encoding = calculate_mime_type(request, path, rev) # Is this display blocked by 'binary_mime_types' configuration? if is_binary_file_mime_type(mime_type, cfg): raise debug.ViewVCException('Display of binary file content disabled ' 'by configuration', '403 Forbidden') # Is this a viewable image type? if is_viewable_image(mime_type) \ and 'co' in cfg.options.allowed_views: fp, revision = request.repos.openfile(path, rev, {}) fp.close() if check_freshness(request, None, revision, weak=1): return if is_annotate: annotation = 'binary' image_src_href = request.get_url(view_func=view_checkout, params={'revision': rev}, escape=1) # Not a viewable image. else: filesize = request.repos.filesize(path, rev) # If configuration disallows display of large files, try to honor # that request. assert_viewable_filesize(cfg, filesize) # If this was an annotation request, try to annotate this file. # If something goes wrong, that's okay -- we'll gracefully revert # to a plain markup display. blame_data = None if is_annotate: try: blame_source, revision = request.repos.annotate(path, rev, False) if check_freshness(request, None, revision, weak=1): return # Create BLAME_DATA list from BLAME_SOURCE, adding diff_href # items to each relevant "line". blame_data = [] for item in blame_source: item.diff_href = None if item.prev_rev: item.diff_href = request.get_url(view_func=view_diff, params={'r1': item.prev_rev, 'r2': item.rev}, escape=1, partial=1) blame_data.append(item) annotation = 'annotated' except vclib.NonTextualFileContents: annotation = 'binary' except: annotation = 'error' # Grab the file contents. fp, revision = request.repos.openfile(path, rev, {'cvs_oldkeywords' : 1}) if check_freshness(request, None, revision, weak=1): fp.close() return # If we're limiting by filesize but couldn't pull off the cheap # check above, we'll try to do so line by line here (while # building our file_lines array). if cfg.options.max_filesize_kbytes and filesize == -1: file_lines = [] filesize = 0 while 1: line = fp.readline() if not line: break filesize = filesize + len(line) assert_viewable_filesize(cfg, filesize) file_lines.append(line) else: file_lines = fp.readlines() fp.close() # Do we have a differing number of file content lines and # annotation items? That's no good. Call it an error and don't # bother attempting the annotation display. if blame_data and (len(file_lines) != len(blame_data)): annotation = 'error' blame_data = None # Try to markup the file contents/annotation. If we get an error # and we were colorizing the stream, try once more without the # colorization enabled. colorize = cfg.options.enable_syntax_coloration try: lines = markup_stream(request, cfg, blame_data, file_lines, path[-1], mime_type, encoding, colorize) except: if colorize: lines = markup_stream(request, cfg, blame_data, file_lines, path[-1], mime_type, encoding, False) else: raise debug.ViewVCException('Error displaying file contents', '500 Internal Server Error') data = common_template_data(request, revision, mime_type) data.merge(ezt.TemplateData({ 'mime_type' : mime_type, 'log' : None, 'date' : None, 'ago' : None, 'author' : None, 'branches' : None, 'tags' : None, 'branch_points' : None, 'changed' : None, 'size' : None, 'state' : None, 'vendor_branch' : None, 'prev' : None, 'orig_path' : None, 'orig_href' : None, 'image_src_href' : image_src_href, 'lines' : lines, 'properties' : get_itemprops(request, path, rev), 'annotation' : annotation, })) if cfg.options.show_log_in_markup: options = { 'svn_latest_log': 1, ### FIXME: Use of this magical value is uncool. 'svn_cross_copies': 1, } revs = request.repos.itemlog(path, revision, vclib.SORTBY_REV, 0, 1, options) entry = revs[-1] lf = LogFormatter(request, entry.log) data['date'] = make_time_string(entry.date, cfg) data['author'] = entry.author data['changed'] = entry.changed data['log'] = lf.get(maxlen=0, htmlize=1) data['size'] = entry.size if entry.date is not None: data['ago'] = html_time(request, entry.date, 1) if request.roottype == 'cvs': branch = entry.branch_number prev = entry.prev or entry.parent data['state'] = entry.dead and 'dead' data['prev'] = prev and prev.string data['vendor_branch'] = ezt.boolean(branch and branch[2] % 2 == 1) ### TODO: Should this be using prep_tags() instead? data['branches'] = make_comma_sep_list_string(entry.branches) data['tags'] = make_comma_sep_list_string(entry.tags) data['branch_points']= make_comma_sep_list_string(entry.branch_points) if path != request.path_parts: orig_path = _path_join(path) data['orig_path'] = orig_path data['orig_href'] = request.get_url(view_func=view_log, where=orig_path, pathtype=vclib.FILE, params={'pathrev': revision}, escape=1) generate_page(request, "file", data) def view_markup(request): if 'markup' not in request.cfg.options.allowed_views: raise debug.ViewVCException('Markup view is disabled', '403 Forbidden') if request.pathtype != vclib.FILE: raise debug.ViewVCException('Unsupported feature: markup view on ' 'directory', '400 Bad Request') markup_or_annotate(request, 0) def view_annotate(request): if 'annotate' not in request.cfg.options.allowed_views: raise debug.ViewVCException('Annotation view is disabled', '403 Forbidden') if request.pathtype != vclib.FILE: raise debug.ViewVCException('Unsupported feature: annotate view on ' 'directory', '400 Bad Request') markup_or_annotate(request, 1) def revcmp(rev1, rev2): rev1 = map(int, string.split(rev1, '.')) rev2 = map(int, string.split(rev2, '.')) return cmp(rev1, rev2) def sort_file_data(file_data, roottype, sortdir, sortby, group_dirs): # convert sortdir into a sign bit s = sortdir == "down" and -1 or 1 # in cvs, revision numbers can't be compared meaningfully between # files, so try to do the right thing and compare dates instead if roottype == "cvs" and sortby == "rev": sortby = "date" def file_sort_sortby(file1, file2, sortby): # sort according to sortby if sortby == 'rev': return s * revcmp(file1.rev, file2.rev) elif sortby == 'date': return s * cmp(file2.date, file1.date) # latest date is first elif sortby == 'log': return s * cmp(file1.log, file2.log) elif sortby == 'author': return s * cmp(file1.author, file2.author) return s * cmp(file1.name, file2.name) def file_sort_cmp(file1, file2, sortby=sortby, group_dirs=group_dirs, s=s): # if we're grouping directories together, sorting is pretty # simple. a directory sorts "higher" than a non-directory, and # two directories are sorted as normal. if group_dirs: if file1.kind == vclib.DIR: if file2.kind == vclib.DIR: # two directories, no special handling. return file_sort_sortby(file1, file2, sortby) else: # file1 is a directory, it sorts first. return -1 elif file2.kind == vclib.DIR: # file2 is a directory, it sorts first. return 1 # we should have data on these. if not, then it is because we requested # a specific tag and that tag is not present on the file. if file1.rev is not None and file2.rev is not None: return file_sort_sortby(file1, file2, sortby) elif file1.rev is not None: return -1 elif file2.rev is not None: return 1 # sort by file name return s * cmp(file1.name, file2.name) file_data.sort(file_sort_cmp) def icmp(x, y): """case insensitive comparison""" return cmp(string.lower(x), string.lower(y)) def view_roots(request): if 'roots' not in request.cfg.options.allowed_views: raise debug.ViewVCException('Root listing view is disabled', '403 Forbidden') # add in the roots for the selection roots = [] expand_root_parents(request.cfg) allroots = list_roots(request) if len(allroots): rootnames = allroots.keys() rootnames.sort(icmp) for rootname in rootnames: root_path, root_type, lastmod = allroots[rootname] href = request.get_url(view_func=view_directory, where='', pathtype=vclib.DIR, params={'root': rootname}, escape=1) if root_type == vclib.SVN: log_href = request.get_url(view_func=view_log, where='', pathtype=vclib.DIR, params={'root': rootname}, escape=1) else: log_href = None roots.append(_item(name=request.server.escape(rootname), type=root_type, path=root_path, author=lastmod and lastmod.author or None, ago=lastmod and lastmod.ago or None, date=lastmod and lastmod.date or None, log=lastmod and lastmod.log or None, short_log=lastmod and lastmod.short_log or None, rev=lastmod and lastmod.rev or None, href=href, log_href=log_href)) data = common_template_data(request) data.merge(ezt.TemplateData({ 'roots' : roots, })) generate_page(request, "roots", data) def view_directory(request): cfg = request.cfg # For Subversion repositories, the revision acts as a weak validator for # the directory listing (to take into account template changes or # revision property changes). if request.roottype == 'svn': try: rev = request.repos._getrev(request.pathrev) except vclib.InvalidRevision: raise debug.ViewVCException('Invalid revision', '404 Not Found') tree_rev = request.repos.created_rev(request.where, rev) if check_freshness(request, None, str(tree_rev), weak=1): return # List current directory options = {} if request.roottype == 'cvs': hideattic = int(request.query_dict.get('hideattic', cfg.options.hide_attic)) options["cvs_subdirs"] = (cfg.options.show_subdir_lastmod and cfg.options.show_logs) file_data = request.repos.listdir(request.path_parts, request.pathrev, options) # sort with directories first, and using the "sortby" criteria sortby = request.query_dict.get('sortby', cfg.options.sort_by) or 'file' sortdir = request.query_dict.get('sortdir', 'up') # when paging and sorting by filename, we can greatly improve # performance by "cheating" -- first, we sort (we already have the # names), then we just fetch dirlogs for the needed entries. # however, when sorting by other properties or not paging, we've no # choice but to fetch dirlogs for everything. debug.t_start("dirlogs") if cfg.options.dir_pagesize and sortby == 'file': dirlogs_first = int(request.query_dict.get('dir_pagestart', 0)) if dirlogs_first > len(file_data): dirlogs_first = 0 dirlogs_last = dirlogs_first + cfg.options.dir_pagesize for file in file_data: file.rev = None file.date = None file.log = None file.author = None file.size = None file.lockinfo = None file.dead = None sort_file_data(file_data, request.roottype, sortdir, sortby, cfg.options.sort_group_dirs) # request dirlogs only for the slice of files in "this page" request.repos.dirlogs(request.path_parts, request.pathrev, file_data[dirlogs_first:dirlogs_last], options) else: request.repos.dirlogs(request.path_parts, request.pathrev, file_data, options) sort_file_data(file_data, request.roottype, sortdir, sortby, cfg.options.sort_group_dirs) debug.t_end("dirlogs") # If a regex is specified, build a compiled form thereof for filtering searchstr = None search_re = request.query_dict.get('search', '') if cfg.options.use_re_search and search_re: searchstr = re.compile(search_re) # loop through entries creating rows and changing these values rows = [ ] num_displayed = 0 num_dead = 0 # set some values to be used inside loop where = request.where where_prefix = where and where + '/' for file in file_data: row = _item(author=None, log=None, short_log=None, state=None, size=None, log_file=None, log_rev=None, graph_href=None, mime_type=None, date=None, ago=None, view_href=None, log_href=None, revision_href=None, annotate_href=None, download_href=None, download_text_href=None, prefer_markup=ezt.boolean(0)) if request.roottype == 'cvs' and file.absent: continue if cfg.options.hide_errorful_entries and file.errors: continue row.rev = file.rev row.author = file.author row.state = (request.roottype == 'cvs' and file.dead) and 'dead' or '' if file.date is not None: row.date = make_time_string(file.date, cfg) row.ago = html_time(request, file.date) if cfg.options.show_logs: debug.t_start("dirview_logformat") lf = LogFormatter(request, file.log) row.log = lf.get(maxlen=0, htmlize=1) row.short_log = lf.get(maxlen=cfg.options.short_log_len, htmlize=1) debug.t_end("dirview_logformat") row.lockinfo = file.lockinfo row.anchor = request.server.escape(file.name) row.name = request.server.escape(file.name) row.pathtype = (file.kind == vclib.FILE and 'file') or \ (file.kind == vclib.DIR and 'dir') row.errors = file.errors if file.kind == vclib.DIR: if cfg.options.hide_cvsroot \ and is_cvsroot_path(request.roottype, request.path_parts + [file.name]): continue row.view_href = request.get_url(view_func=view_directory, where=where_prefix+file.name, pathtype=vclib.DIR, params={}, escape=1) if request.roottype == 'svn': row.revision_href = request.get_url(view_func=view_revision, params={'revision': file.rev}, escape=1) if request.roottype == 'cvs' and file.rev is not None: row.rev = None if cfg.options.show_logs: row.log_file = file.newest_file row.log_rev = file.rev if request.roottype == 'svn': row.log_href = request.get_url(view_func=view_log, where=where_prefix + file.name, pathtype=vclib.DIR, params={}, escape=1) elif file.kind == vclib.FILE: if searchstr is not None: if request.roottype == 'cvs' and (file.errors or file.dead): continue if not search_file(request.repos, request.path_parts + [file.name], request.pathrev, searchstr): continue if request.roottype == 'cvs' and file.dead: num_dead = num_dead + 1 if hideattic: continue num_displayed = num_displayed + 1 file_where = where_prefix + file.name if request.roottype == 'svn': row.size = file.size row.mime_type, encoding = calculate_mime_type(request, _path_parts(file_where), file.rev) fvi = get_file_view_info(request, file_where, file.rev, row.mime_type) row.view_href = fvi.view_href row.download_href = fvi.download_href row.download_text_href = fvi.download_text_href row.annotate_href = fvi.annotate_href row.revision_href = fvi.revision_href row.prefer_markup = fvi.prefer_markup row.log_href = request.get_url(view_func=view_log, where=file_where, pathtype=vclib.FILE, params={}, escape=1) if cfg.options.use_cvsgraph and request.roottype == 'cvs': row.graph_href = request.get_url(view_func=view_cvsgraph, where=file_where, pathtype=vclib.FILE, params={}, escape=1) rows.append(row) # Prepare the data that will be passed to the template, based on the # common template data. data = common_template_data(request) data.merge(ezt.TemplateData({ 'entries' : rows, 'sortby' : sortby, 'sortdir' : sortdir, 'search_re' : request.server.escape(search_re), 'dir_pagestart' : None, 'sortby_file_href' : request.get_url(params={'sortby': 'file', 'sortdir': None}, escape=1), 'sortby_rev_href' : request.get_url(params={'sortby': 'rev', 'sortdir': None}, escape=1), 'sortby_date_href' : request.get_url(params={'sortby': 'date', 'sortdir': None}, escape=1), 'sortby_author_href' : request.get_url(params={'sortby': 'author', 'sortdir': None}, escape=1), 'sortby_log_href' : request.get_url(params={'sortby': 'log', 'sortdir': None}, escape=1), 'files_shown' : num_displayed, 'num_dead' : num_dead, 'youngest_rev' : None, 'youngest_rev_href' : None, 'selection_form' : None, 'attic_showing' : None, 'show_attic_href' : None, 'hide_attic_href' : None, 'branch_tags': None, 'plain_tags': None, 'properties': get_itemprops(request, request.path_parts, request.pathrev), 'tree_rev' : None, 'tree_rev_href' : None, 'dir_paging_action' : None, 'dir_paging_hidden_values' : [], 'search_re_action' : None, 'search_re_hidden_values' : [], # Populated by paging()/paging_sws() 'picklist' : [], 'picklist_len' : 0, # Populated by pathrev_form() 'pathrev_action' : None, 'pathrev_hidden_values' : [], 'pathrev_clear_action' : None, 'pathrev_clear_hidden_values' : [], 'pathrev' : None, 'lastrev' : None, })) # clicking on sort column reverses sort order if sortdir == 'down': revsortdir = None # 'up' else: revsortdir = 'down' if sortby in ['file', 'rev', 'date', 'log', 'author']: data['sortby_%s_href' % sortby] = request.get_url(params={'sortdir': revsortdir}, escape=1) # CVS doesn't support sorting by rev if request.roottype == "cvs": data['sortby_rev_href'] = None # set cvs-specific fields if request.roottype == 'cvs': plain_tags = options['cvs_tags'] plain_tags.sort(icmp) plain_tags.reverse() data['plain_tags']= plain_tags branch_tags = options['cvs_branches'] branch_tags.sort(icmp) branch_tags.reverse() data['branch_tags']= branch_tags data['attic_showing'] = ezt.boolean(not hideattic) data['show_attic_href'] = request.get_url(params={'hideattic': 0}, escape=1) data['hide_attic_href'] = request.get_url(params={'hideattic': 1}, escape=1) # set svn-specific fields elif request.roottype == 'svn': data['tree_rev'] = tree_rev data['tree_rev_href'] = request.get_url(view_func=view_revision, params={'revision': tree_rev}, escape=1) data['youngest_rev'] = request.repos.get_youngest_revision() data['youngest_rev_href'] = request.get_url(view_func=view_revision, params={}, escape=1) if cfg.options.dir_pagesize: data['dir_paging_action'], data['dir_paging_hidden_values'] = \ request.get_form(params={'dir_pagestart': None}) pathrev_form(request, data) if cfg.options.use_re_search: data['search_re_action'], data['search_re_hidden_values'] = \ request.get_form(params={'search': None}) if cfg.options.dir_pagesize: data['dir_pagestart'] = int(request.query_dict.get('dir_pagestart',0)) data['entries'] = paging(data, 'entries', data['dir_pagestart'], 'name', cfg.options.dir_pagesize) generate_page(request, "directory", data) def paging(data, key, pagestart, local_name, pagesize): # Implement paging # Create the picklist picklist = data['picklist'] = [] for i in range(0, len(data[key]), pagesize): pick = _item(start=None, end=None, count=None, more=ezt.boolean(0)) pick.start = getattr(data[key][i], local_name) pick.count = i pick.page = (i / pagesize) + 1 try: pick.end = getattr(data[key][i+pagesize-1], local_name) except IndexError: pick.end = getattr(data[key][-1], local_name) picklist.append(pick) data['picklist_len'] = len(picklist) # Need to fix # pagestart can be greater than the length of data[key] if you # select a tag or search while on a page other than the first. # Should reset to the first page, this test won't do that every # time that it is needed. # Problem might go away if we don't hide non-matching files when # selecting for tags or searching. if pagestart > len(data[key]): pagestart = 0 pageend = pagestart + pagesize # Slice return data[key][pagestart:pageend] def paging_sws(data, key, pagestart, local_name, pagesize, extra_pages, offset): """Implement sliding window-style paging.""" # Create the picklist last_requested = pagestart + (extra_pages * pagesize) picklist = data['picklist'] = [] has_more = ezt.boolean(0) for i in range(0, len(data[key]), pagesize): pick = _item(start=None, end=None, count=None, more=ezt.boolean(0)) pick.start = getattr(data[key][i], local_name) pick.count = offset + i pick.page = (pick.count / pagesize) + 1 try: pick.end = getattr(data[key][i+pagesize-1], local_name) except IndexError: pick.end = getattr(data[key][-1], local_name) picklist.append(pick) if pick.count >= last_requested: pick.more = ezt.boolean(1) break data['picklist_len'] = len(picklist) first = pagestart - offset # FIXME: first can be greater than the length of data[key] if # you select a tag or search while on a page other than the first. # Should reset to the first page, but this test won't do that every # time that it is needed. Problem might go away if we don't hide # non-matching files when selecting for tags or searching. if first > len(data[key]): pagestart = 0 pageend = first + pagesize # Slice return data[key][first:pageend] def pathrev_form(request, data): lastrev = None if request.roottype == 'svn': data['pathrev_action'], data['pathrev_hidden_values'] = \ request.get_form(view_func=redirect_pathrev, params={'pathrev': None, 'orig_path': request.where, 'orig_pathtype': request.pathtype, 'orig_pathrev': request.pathrev, 'orig_view': _view_codes.get(request.view_func)}) if request.pathrev: youngest = request.repos.get_youngest_revision() lastrev = request.repos.last_rev(request.where, request.pathrev, youngest)[0] if lastrev == youngest: lastrev = None data['pathrev'] = request.pathrev data['lastrev'] = lastrev action, hidden_values = request.get_form(params={'pathrev': lastrev}) if request.roottype != 'svn': data['pathrev_action'] = action data['pathrev_hidden_values'] = hidden_values data['pathrev_clear_action'] = action data['pathrev_clear_hidden_values'] = hidden_values return lastrev def redirect_pathrev(request): assert request.roottype == 'svn' new_pathrev = request.query_dict.get('pathrev') or None path = request.query_dict.get('orig_path', '') pathtype = request.query_dict.get('orig_pathtype') pathrev = request.query_dict.get('orig_pathrev') view = _views.get(request.query_dict.get('orig_view')) youngest = request.repos.get_youngest_revision() # go out of the way to allow revision numbers higher than youngest try: new_pathrev = int(new_pathrev) except ValueError: new_pathrev = youngest except TypeError: pass else: if new_pathrev > youngest: new_pathrev = youngest if _repos_pathtype(request.repos, _path_parts(path), new_pathrev): pathrev = new_pathrev else: pathrev, path = request.repos.last_rev(path, pathrev, new_pathrev) # allow clearing sticky revision by submitting empty string if new_pathrev is None and pathrev == youngest: pathrev = None request.server.redirect(request.get_url(view_func=view, where=path, pathtype=pathtype, params={'pathrev': pathrev})) def view_log(request): cfg = request.cfg diff_format = request.query_dict.get('diff_format', cfg.options.diff_format) pathtype = request.pathtype if pathtype is vclib.DIR: if request.roottype == 'cvs': raise debug.ViewVCException('Unsupported feature: log view on CVS ' 'directory', '400 Bad Request') mime_type = encoding = None else: mime_type, encoding = calculate_mime_type(request, request.path_parts, request.pathrev) options = {} options['svn_show_all_dir_logs'] = 1 ### someday make this optional? options['svn_cross_copies'] = cfg.options.cross_copies logsort = request.query_dict.get('logsort', cfg.options.log_sort) if request.roottype == "svn": sortby = vclib.SORTBY_DEFAULT logsort = None else: if logsort == 'date': sortby = vclib.SORTBY_DATE elif logsort == 'rev': sortby = vclib.SORTBY_REV else: sortby = vclib.SORTBY_DEFAULT first = last = 0 log_pagestart = None if cfg.options.log_pagesize: log_pagestart = int(request.query_dict.get('log_pagestart', 0)) total = cfg.options.log_pagesextra * cfg.options.log_pagesize first = log_pagestart - min(log_pagestart, total) last = log_pagestart + (total + cfg.options.log_pagesize) + 1 show_revs = request.repos.itemlog(request.path_parts, request.pathrev, sortby, first, last - first, options) # selected revision selected_rev = request.query_dict.get('r1') entries = [ ] name_printed = { } cvs = request.roottype == 'cvs' for rev in show_revs: entry = _item() entry.rev = rev.string entry.state = (cvs and rev.dead and 'dead') entry.author = rev.author entry.changed = rev.changed entry.date = make_time_string(rev.date, cfg) entry.ago = None if rev.date is not None: entry.ago = html_time(request, rev.date, 1) entry.size = rev.size entry.lockinfo = rev.lockinfo entry.branch_point = None entry.next_main = None entry.orig_path = None entry.copy_path = None lf = LogFormatter(request, rev.log or '') entry.log = lf.get(maxlen=0, htmlize=1) entry.view_href = None entry.download_href = None entry.download_text_href = None entry.annotate_href = None entry.revision_href = None entry.sel_for_diff_href = None entry.diff_to_sel_href = None entry.diff_to_prev_href = None entry.diff_to_branch_href = None entry.diff_to_main_href = None if request.roottype == 'cvs': prev = rev.prev or rev.parent entry.prev = prev and prev.string branch = rev.branch_number entry.vendor_branch = ezt.boolean(branch and branch[2] % 2 == 1) entry.branches = prep_tags(request, rev.branches) entry.tags = prep_tags(request, rev.tags) entry.branch_points = prep_tags(request, rev.branch_points) entry.tag_names = map(lambda x: x.name, rev.tags) if branch and not name_printed.has_key(branch): entry.branch_names = map(lambda x: x.name, rev.branches) name_printed[branch] = 1 else: entry.branch_names = [ ] if rev.parent and rev.parent is not prev and not entry.vendor_branch: entry.branch_point = rev.parent.string # if it's the last revision on a branch then diff against the # last revision on the higher branch (e.g. change is committed and # brought over to -stable) if not rev.next and rev.parent and rev.parent.next: r = rev.parent.next while r.next: r = r.next entry.next_main = r.string elif request.roottype == 'svn': entry.prev = rev.prev and rev.prev.string entry.branches = entry.tags = entry.branch_points = [ ] entry.tag_names = entry.branch_names = [ ] entry.vendor_branch = None if rev.filename != request.where: entry.orig_path = rev.filename entry.copy_path = rev.copy_path entry.copy_rev = rev.copy_rev if entry.orig_path: entry.orig_href = request.get_url(view_func=view_log, where=entry.orig_path, pathtype=vclib.FILE, params={'pathrev': rev.string}, escape=1) if rev.copy_path: entry.copy_href = request.get_url(view_func=view_log, where=rev.copy_path, pathtype=vclib.FILE, params={'pathrev': rev.copy_rev}, escape=1) # view/download links if pathtype is vclib.FILE: fvi = get_file_view_info(request, request.where, rev.string, mime_type) entry.view_href = fvi.view_href entry.download_href = fvi.download_href entry.download_text_href = fvi.download_text_href entry.annotate_href = fvi.annotate_href entry.revision_href = fvi.revision_href entry.prefer_markup = fvi.prefer_markup else: entry.revision_href = request.get_url(view_func=view_revision, params={'revision': rev.string}, escape=1) entry.view_href = request.get_url(view_func=view_directory, where=rev.filename, pathtype=vclib.DIR, params={'pathrev': rev.string}, escape=1) # calculate diff links if selected_rev != entry.rev: entry.sel_for_diff_href = \ request.get_url(view_func=view_log, params={'r1': entry.rev, 'log_pagestart': log_pagestart}, escape=1) if entry.prev is not None: entry.diff_to_prev_href = \ request.get_url(view_func=view_diff, params={'r1': entry.prev, 'r2': entry.rev, 'diff_format': None}, escape=1) if selected_rev and \ selected_rev != str(entry.rev) and \ selected_rev != str(entry.prev) and \ selected_rev != str(entry.branch_point) and \ selected_rev != str(entry.next_main): entry.diff_to_sel_href = \ request.get_url(view_func=view_diff, params={'r1': selected_rev, 'r2': entry.rev, 'diff_format': None}, escape=1) if entry.next_main: entry.diff_to_main_href = \ request.get_url(view_func=view_diff, params={'r1': entry.next_main, 'r2': entry.rev, 'diff_format': None}, escape=1) if entry.branch_point: entry.diff_to_branch_href = \ request.get_url(view_func=view_diff, params={'r1': entry.branch_point, 'r2': entry.rev, 'diff_format': None}, escape=1) # Save our escaping until the end so stuff above works if entry.orig_path: entry.orig_path = request.server.escape(entry.orig_path) if entry.copy_path: entry.copy_path = request.server.escape(entry.copy_path) entries.append(entry) diff_select_action, diff_select_hidden_values = \ request.get_form(view_func=view_diff, params={'r1': None, 'r2': None, 'tr1': None, 'tr2': None, 'diff_format': None}) logsort_action, logsort_hidden_values = \ request.get_form(params={'logsort': None}) data = common_template_data(request) data.merge(ezt.TemplateData({ 'default_branch' : None, 'mime_type' : mime_type, 'rev_selected' : selected_rev, 'diff_format' : diff_format, 'logsort' : logsort, 'human_readable' : ezt.boolean(diff_format in ('h', 'l')), 'log_pagestart' : None, 'log_paging_action' : None, 'log_paging_hidden_values' : [], 'entries': entries, 'head_prefer_markup' : ezt.boolean(0), 'head_view_href' : None, 'head_download_href': None, 'head_download_text_href': None, 'head_annotate_href': None, 'tag_prefer_markup' : ezt.boolean(0), 'tag_view_href' : None, 'tag_download_href': None, 'tag_download_text_href': None, 'tag_annotate_href': None, 'diff_select_action' : diff_select_action, 'diff_select_hidden_values' : diff_select_hidden_values, 'logsort_action' : logsort_action, 'logsort_hidden_values' : logsort_hidden_values, 'tags' : [], 'branch_tags' : [], 'plain_tags' : [], # Populated by paging()/paging_sws() 'picklist' : [], 'picklist_len' : 0, # Populated by pathrev_form() 'pathrev_action' : None, 'pathrev_hidden_values' : [], 'pathrev_clear_action' : None, 'pathrev_clear_hidden_values' : [], 'pathrev' : None, 'lastrev' : None, })) lastrev = pathrev_form(request, data) if pathtype is vclib.FILE: if not request.pathrev or lastrev is None: fvi = get_file_view_info(request, request.where, None, mime_type, None) data['head_view_href']= fvi.view_href data['head_download_href']= fvi.download_href data['head_download_text_href']= fvi.download_text_href data['head_annotate_href']= fvi.annotate_href data['head_prefer_markup']= fvi.prefer_markup if request.pathrev and request.roottype == 'cvs': fvi = get_file_view_info(request, request.where, None, mime_type) data['tag_view_href']= fvi.view_href data['tag_download_href']= fvi.download_href data['tag_download_text_href']= fvi.download_text_href data['tag_annotate_href']= fvi.annotate_href data['tag_prefer_markup']= fvi.prefer_markup else: data['head_view_href'] = request.get_url(view_func=view_directory, params={}, escape=1) taginfo = options.get('cvs_tags', {}) tagitems = taginfo.items() tagitems.sort() tagitems.reverse() main = taginfo.get('MAIN') if main: # Default branch may have multiple names so we list them branches = [] for branch in main.aliases: # Don't list MAIN if branch is not main: branches.append(branch) data['default_branch'] = prep_tags(request, branches) for tag, rev in tagitems: if rev.co_rev: data['tags'].append(_item(rev=rev.co_rev.string, name=tag)) if rev.is_branch: data['branch_tags'].append(tag) else: data['plain_tags'].append(tag) if cfg.options.log_pagesize: data['log_paging_action'], data['log_paging_hidden_values'] = \ request.get_form(params={'log_pagestart': None, 'r1': selected_rev, }) data['log_pagestart'] = int(request.query_dict.get('log_pagestart',0)) data['entries'] = paging_sws(data, 'entries', data['log_pagestart'], 'rev', cfg.options.log_pagesize, cfg.options.log_pagesextra, first) generate_page(request, "log", data) def view_checkout(request): cfg = request.cfg if 'co' not in cfg.options.allowed_views: raise debug.ViewVCException('Checkout view is disabled', '403 Forbidden') if request.pathtype != vclib.FILE: raise debug.ViewVCException('Unsupported feature: checkout view on ' 'directory', '400 Bad Request') path, rev = _orig_path(request) fp, revision = request.repos.openfile(path, rev, {}) # The revision number acts as a strong validator. if not check_freshness(request, None, revision): mime_type, encoding = calculate_mime_type(request, path, rev) mime_type = request.query_dict.get('content-type') \ or mime_type \ or 'text/plain' server_fp = get_writeready_server_file(request, mime_type, encoding) copy_stream(fp, server_fp) fp.close() def view_cvsgraph_image(request): "output the image rendered by cvsgraph" # this function is derived from cgi/cvsgraphmkimg.cgi cfg = request.cfg if not cfg.options.use_cvsgraph: raise debug.ViewVCException('Graph view is disabled', '403 Forbidden') # If cvsgraph can't find its supporting libraries, uncomment and set # accordingly. Do the same in view_cvsgraph(). #os.environ['LD_LIBRARY_PATH'] = '/usr/lib:/usr/local/lib:/path/to/cvsgraph' rcsfile = request.repos.rcsfile(request.path_parts) fp = popen.popen(cfg.utilities.cvsgraph or 'cvsgraph', ("-c", cfg.path(cfg.options.cvsgraph_conf), "-r", request.repos.rootpath, rcsfile), 'rb', 0) copy_stream(fp, get_writeready_server_file(request, 'image/png')) fp.close() def view_cvsgraph(request): "output a page containing an image rendered by cvsgraph" cfg = request.cfg if not cfg.options.use_cvsgraph: raise debug.ViewVCException('Graph view is disabled', '403 Forbidden') # If cvsgraph can't find its supporting libraries, uncomment and set # accordingly. Do the same in view_cvsgraph_image(). #os.environ['LD_LIBRARY_PATH'] = '/usr/lib:/usr/local/lib:/path/to/cvsgraph' imagesrc = request.get_url(view_func=view_cvsgraph_image, escape=1) mime_type = guess_mime(request.where) view = default_view(mime_type, cfg) up_where = _path_join(request.path_parts[:-1]) # Create an image map rcsfile = request.repos.rcsfile(request.path_parts) fp = popen.popen(cfg.utilities.cvsgraph or 'cvsgraph', ("-i", "-c", cfg.path(cfg.options.cvsgraph_conf), "-r", request.repos.rootpath, "-x", "x", "-3", request.get_url(view_func=view_log, params={}, escape=1), "-4", request.get_url(view_func=view, params={'revision': None}, escape=1, partial=1), "-5", request.get_url(view_func=view_diff, params={'r1': None, 'r2': None}, escape=1, partial=1), "-6", request.get_url(view_func=view_directory, where=up_where, pathtype=vclib.DIR, params={'pathrev': None}, escape=1, partial=1), rcsfile), 'rb', 0) data = common_template_data(request) data.merge(ezt.TemplateData({ 'imagemap' : fp, 'imagesrc' : imagesrc, })) generate_page(request, "graph", data) def search_file(repos, path_parts, rev, search_re): """Return 1 iff the contents of the file at PATH_PARTS in REPOS as of revision REV matches regular expression SEARCH_RE.""" # Read in each line of a checked-out file, and then use re.search to # search line. fp = repos.openfile(path_parts, rev, {})[0] matches = 0 while 1: line = fp.readline() if not line: break if search_re.search(line): matches = 1 fp.close() break return matches def view_doc(request): """Serve ViewVC static content locally. Using this avoids the need for modifying the setup of the web server. """ cfg = request.cfg document = request.where filename = cfg.path(os.path.join(cfg.options.template_dir, "docroot", document)) # Stat the file to get content length and last-modified date. try: info = os.stat(filename) except OSError, v: raise debug.ViewVCException('Static file "%s" not available (%s)' % (document, str(v)), '404 Not Found') content_length = str(info[stat.ST_SIZE]) last_modified = info[stat.ST_MTIME] # content_length + mtime makes a pretty good etag. if check_freshness(request, last_modified, "%s-%s" % (content_length, last_modified)): return try: fp = open(filename, "rb") except IOError, v: raise debug.ViewVCException('Static file "%s" not available (%s)' % (document, str(v)), '404 Not Found') if document[-3:] == 'png': mime_type = 'image/png' elif document[-3:] == 'jpg': mime_type = 'image/jpeg' elif document[-3:] == 'gif': mime_type = 'image/gif' elif document[-3:] == 'css': mime_type = 'text/css' else: # assume HTML: mime_type = None copy_stream(fp, get_writeready_server_file(request, mime_type, content_length=content_length)) fp.close() def rcsdiff_date_reformat(date_str, cfg): if date_str is None: return None try: date = compat.cvs_strptime(date_str) except ValueError: return date_str return make_time_string(compat.timegm(date), cfg) _re_extract_rev = re.compile(r'^[-+*]{3} [^\t]+\t([^\t]+)\t((\d+\.)*\d+)$') _re_extract_info = re.compile(r'@@ \-([0-9]+).*\+([0-9]+).*@@(.*)') class DiffSource: def __init__(self, fp, cfg): self.fp = fp self.cfg = cfg self.save_line = None self.line_number = None self.prev_line_number = None # keep track of where we are during an iteration self.idx = -1 self.last = None # these will be set once we start reading self.state = 'no-changes' self.left_col = [ ] self.right_col = [ ] def __getitem__(self, idx): if idx == self.idx: return self.last if idx != self.idx + 1: raise DiffSequencingError() # keep calling _get_row until it gives us something. sometimes, it # doesn't return a row immediately because it is accumulating changes. # when it is out of data, _get_row will raise IndexError. while 1: item = self._get_row() if item: self.idx = idx self.last = item return item def _format_text(self, text): text = string.rstrip(text, '\r\n') if self.cfg.options.tabsize > 0: text = string.expandtabs(text, self.cfg.options.tabsize) hr_breakable = self.cfg.options.hr_breakable # in the code below, "\x01" will be our stand-in for "&". We don't want # to insert "&" because it would get escaped by sapi.escape(). Similarly, # we use "\x02" as a stand-in for "
" if hr_breakable > 1 and len(text) > hr_breakable: text = re.sub('(' + ('.' * hr_breakable) + ')', '\\1\x02', text) if hr_breakable: # make every other space "breakable" text = string.replace(text, ' ', ' \x01nbsp;') else: text = string.replace(text, ' ', '\x01nbsp;') text = sapi.escape(text) text = string.replace(text, '\x01', '&') text = string.replace(text, '\x02', '\
') return text def _get_row(self): if self.state[:5] == 'flush': item = self._flush_row() if item: return item self.state = 'dump' if self.save_line: line = self.save_line self.save_line = None else: line = self.fp.readline() if not line: if self.state == 'no-changes': self.state = 'done' return _item(type='no-changes') # see if there are lines to flush if self.left_col or self.right_col: # move into the flushing state self.state = 'flush-' + self.state return None # nothing more to return raise IndexError if line[:2] == '@@': self.state = 'dump' self.left_col = [ ] self.right_col = [ ] match = _re_extract_info.match(line) self.line_number = int(match.group(2)) - 1 self.prev_line_number = int(match.group(1)) - 1 return _item(type='header', line_info_left=match.group(1), line_info_right=match.group(2), line_info_extra=self._format_text(match.group(3))) if line[0] == '\\': # \ No newline at end of file # move into the flushing state. note: it doesn't matter if we really # have data to flush or not; that will be figured out later self.state = 'flush-' + self.state return None diff_code = line[0] output = self._format_text(line[1:]) if diff_code == '+': if self.state == 'dump': self.line_number = self.line_number + 1 return _item(type='add', right=output, line_number=self.line_number) self.state = 'pre-change-add' self.right_col.append(output) return None if diff_code == '-': self.state = 'pre-change-remove' self.left_col.append(output) return None # early exit to avoid line in if self.left_col or self.right_col: # save the line for processing again later, and move into the # flushing state self.save_line = line self.state = 'flush-' + self.state return None self.line_number = self.line_number + 1 self.prev_line_number = self.prev_line_number + 1 return _item(type='context', left=output, right=output, line_number=self.line_number) def _flush_row(self): if not self.left_col and not self.right_col: # nothing more to flush return None if self.state == 'flush-pre-change-remove': self.prev_line_number = self.prev_line_number + 1 return _item(type='remove', left=self.left_col.pop(0), line_number=self.prev_line_number) # state == flush-pre-change-add item = _item(type='change', have_left=ezt.boolean(0), have_right=ezt.boolean(0)) if self.left_col: self.prev_line_number = self.prev_line_number + 1 item.have_left = ezt.boolean(1) item.left = self.left_col.pop(0) item.line_number = self.prev_line_number if self.right_col: self.line_number = self.line_number + 1 item.have_right = ezt.boolean(1) item.right = self.right_col.pop(0) item.line_number = self.line_number return item class DiffSequencingError(Exception): pass def diff_parse_headers(fp, diff_type, path1, path2, rev1, rev2, sym1=None, sym2=None): date1 = date2 = log_rev1 = log_rev2 = flag = None header_lines = [] if diff_type == vclib.UNIFIED: f1 = '--- ' f2 = '+++ ' elif diff_type == vclib.CONTEXT: f1 = '*** ' f2 = '--- ' else: f1 = f2 = None # If we're parsing headers, then parse and tweak the diff headers, # collecting them in an array until we've read and handled them all. if f1 and f2: parsing = 1 len_f1 = len(f1) len_f2 = len(f2) while parsing: line = fp.readline() if not line: break if line[:len(f1)] == f1: match = _re_extract_rev.match(line) if match: date1 = match.group(1) log_rev1 = match.group(2) line = '%s%s\t%s\t%s%s\n' % (f1, path1, date1, log_rev1, sym1 and ' ' + sym1 or '') elif line[:len(f2)] == f2: match = _re_extract_rev.match(line) if match: date2 = match.group(1) log_rev2 = match.group(2) line = '%s%s\t%s\t%s%s\n' % (f2, path2, date2, log_rev2, sym2 and ' ' + sym2 or '') parsing = 0 elif line[:3] == 'Bin': flag = _RCSDIFF_IS_BINARY parsing = 0 elif (string.find(line, 'not found') != -1 or string.find(line, 'illegal option') != -1): flag = _RCSDIFF_ERROR parsing = 0 header_lines.append(line) if (log_rev1 and log_rev1 != rev1): raise debug.ViewVCException('rcsdiff found revision %s, but expected ' 'revision %s' % (log_rev1, rev1), '500 Internal Server Error') if (log_rev2 and log_rev2 != rev2): raise debug.ViewVCException('rcsdiff found revision %s, but expected ' 'revision %s' % (log_rev2, rev2), '500 Internal Server Error') return date1, date2, flag, string.join(header_lines, '') def _get_diff_path_parts(request, query_key, rev, base_rev): repos = request.repos if request.query_dict.has_key(query_key): parts = _path_parts(request.query_dict[query_key]) elif request.roottype == 'svn': try: parts = _path_parts(repos.get_location(request.where, repos._getrev(base_rev), repos._getrev(rev))) except vclib.InvalidRevision: raise debug.ViewVCException('Invalid path(s) or revision(s) passed ' 'to diff', '400 Bad Request') except vclib.ItemNotFound: raise debug.ViewVCException('Invalid path(s) or revision(s) passed ' 'to diff', '400 Bad Request') else: parts = request.path_parts return parts def setup_diff(request): query_dict = request.query_dict rev1 = r1 = query_dict['r1'] rev2 = r2 = query_dict['r2'] sym1 = sym2 = None # hack on the diff revisions if r1 == 'text': rev1 = query_dict.get('tr1', None) if not rev1: raise debug.ViewVCException('Missing revision from the diff ' 'form text field', '400 Bad Request') else: idx = string.find(r1, ':') if idx == -1: rev1 = r1 else: rev1 = r1[:idx] sym1 = r1[idx+1:] if r2 == 'text': rev2 = query_dict.get('tr2', None) if not rev2: raise debug.ViewVCException('Missing revision from the diff ' 'form text field', '400 Bad Request') sym2 = '' else: idx = string.find(r2, ':') if idx == -1: rev2 = r2 else: rev2 = r2[:idx] sym2 = r2[idx+1:] if request.roottype == 'svn': try: rev1 = str(request.repos._getrev(rev1)) rev2 = str(request.repos._getrev(rev2)) except vclib.InvalidRevision: raise debug.ViewVCException('Invalid revision(s) passed to diff', '400 Bad Request') p1 = _get_diff_path_parts(request, 'p1', rev1, request.pathrev) p2 = _get_diff_path_parts(request, 'p2', rev2, request.pathrev) try: if revcmp(rev1, rev2) > 0: rev1, rev2 = rev2, rev1 sym1, sym2 = sym2, sym1 p1, p2 = p2, p1 except ValueError: raise debug.ViewVCException('Invalid revision(s) passed to diff', '400 Bad Request') return p1, p2, rev1, rev2, sym1, sym2 def view_patch(request): if 'diff' not in request.cfg.options.allowed_views: raise debug.ViewVCException('Diff generation is disabled', '403 Forbidden') cfg = request.cfg query_dict = request.query_dict p1, p2, rev1, rev2, sym1, sym2 = setup_diff(request) mime_type1, encoding1 = calculate_mime_type(request, p1, rev1) mime_type2, encoding2 = calculate_mime_type(request, p2, rev2) if is_binary_file_mime_type(mime_type1, cfg) or \ is_binary_file_mime_type(mime_type2, cfg): raise debug.ViewVCException('Display of binary file content disabled ' 'by configuration', '403 Forbidden') # In the absence of a format dictation in the CGI params, we'll let # use the configured diff format, allowing 'c' to mean 'c' and # anything else to mean 'u'. format = query_dict.get('diff_format', cfg.options.diff_format == 'c' and 'c' or 'u') if format == 'c': diff_type = vclib.CONTEXT elif format == 'u': diff_type = vclib.UNIFIED else: raise debug.ViewVCException('Diff format %s not understood' % format, '400 Bad Request') try: fp = request.repos.rawdiff(p1, rev1, p2, rev2, diff_type) except vclib.InvalidRevision: raise debug.ViewVCException('Invalid path(s) or revision(s) passed ' 'to diff', '400 Bad Request') path_left = _path_join(p1) path_right = _path_join(p2) date1, date2, flag, headers = diff_parse_headers(fp, diff_type, path_left, path_right, rev1, rev2, sym1, sym2) server_fp = get_writeready_server_file(request, 'text/plain') server_fp.write(headers) copy_stream(fp, server_fp) fp.close() def view_diff(request): if 'diff' not in request.cfg.options.allowed_views: raise debug.ViewVCException('Diff generation is disabled', '403 Forbidden') cfg = request.cfg query_dict = request.query_dict p1, p2, rev1, rev2, sym1, sym2 = setup_diff(request) mime_type1, encoding1 = calculate_mime_type(request, p1, rev1) mime_type2, encoding2 = calculate_mime_type(request, p2, rev2) if is_binary_file_mime_type(mime_type1, cfg) or \ is_binary_file_mime_type(mime_type2, cfg): raise debug.ViewVCException('Display of binary file content disabled ' 'by configuration', '403 Forbidden') # since templates are in use and subversion allows changes to the dates, # we can't provide a strong etag if check_freshness(request, None, '%s-%s' % (rev1, rev2), weak=1): return # TODO: Is the slice necessary, or is limit enough? log_entry1 = request.repos.itemlog(p1, rev1, vclib.SORTBY_REV, 0, 1, {})[-1] log_entry2 = request.repos.itemlog(p2, rev2, vclib.SORTBY_REV, 0, 1, {})[-1] ago1 = log_entry1.date is not None \ and html_time(request, log_entry1.date, 1) or None ago2 = log_entry2.date is not None \ and html_time(request, log_entry2.date, 2) or None diff_type = None diff_options = {} human_readable = 0 format = query_dict.get('diff_format', cfg.options.diff_format) if format == 'c': diff_type = vclib.CONTEXT elif format == 's': diff_type = vclib.SIDE_BY_SIDE elif format == 'l': diff_type = vclib.UNIFIED diff_options['context'] = 15 human_readable = 1 elif format == 'f': diff_type = vclib.UNIFIED diff_options['context'] = None human_readable = 1 elif format == 'h': diff_type = vclib.UNIFIED human_readable = 1 elif format == 'u': diff_type = vclib.UNIFIED else: raise debug.ViewVCException('Diff format %s not understood' % format, '400 Bad Request') if human_readable: diff_options['funout'] = cfg.options.hr_funout diff_options['ignore_white'] = cfg.options.hr_ignore_white diff_options['ignore_keyword_subst'] = cfg.options.hr_ignore_keyword_subst try: fp = sidebyside = unified = None if (cfg.options.hr_intraline and idiff and ((human_readable and idiff.sidebyside) or (not human_readable and diff_type == vclib.UNIFIED))): f1 = request.repos.openfile(p1, rev1, {})[0] try: lines_left = f1.readlines() finally: f1.close() f2 = request.repos.openfile(p2, rev2, {})[0] try: lines_right = f2.readlines() finally: f2.close() if human_readable: sidebyside = idiff.sidebyside(lines_left, lines_right, diff_options.get("context", 5)) else: unified = idiff.unified(lines_left, lines_right, diff_options.get("context", 2)) else: fp = request.repos.rawdiff(p1, rev1, p2, rev2, diff_type, diff_options) except vclib.InvalidRevision: raise debug.ViewVCException('Invalid path(s) or revision(s) passed ' 'to diff', '400 Bad Request') path_left = _path_join(p1) path_right = _path_join(p2) date1 = date2 = raw_diff_fp = None changes = [] if fp: date1, date2, flag, headers = diff_parse_headers(fp, diff_type, path_left, path_right, rev1, rev2, sym1, sym2) if human_readable: if flag is not None: changes = [ _item(type=flag) ] else: changes = DiffSource(fp, cfg) else: raw_diff_fp = MarkupPipeWrapper(fp, request.server.escape(headers), None, 1) no_format_params = request.query_dict.copy() no_format_params['diff_format'] = None diff_format_action, diff_format_hidden_values = \ request.get_form(params=no_format_params) fvi = get_file_view_info(request, path_left, rev1) left = _item(date=make_time_string(log_entry1.date, cfg), author=log_entry1.author, log=LogFormatter(request, log_entry1.log).get(maxlen=0, htmlize=1), size=log_entry1.size, ago=ago1, path=path_left, rev=rev1, tag=sym1, view_href=fvi.view_href, download_href=fvi.download_href, download_text_href=fvi.download_text_href, annotate_href=fvi.annotate_href, revision_href=fvi.revision_href, prefer_markup=fvi.prefer_markup) fvi = get_file_view_info(request, path_right, rev2) right = _item(date=make_time_string(log_entry2.date, cfg), author=log_entry2.author, log=LogFormatter(request, log_entry2.log).get(maxlen=0, htmlize=1), size=log_entry2.size, ago=ago2, path=path_right, rev=rev2, tag=sym2, view_href=fvi.view_href, download_href=fvi.download_href, download_text_href=fvi.download_text_href, annotate_href=fvi.annotate_href, revision_href=fvi.revision_href, prefer_markup=fvi.prefer_markup) data = common_template_data(request) data.merge(ezt.TemplateData({ 'left' : left, 'right' : right, 'raw_diff' : raw_diff_fp, 'changes' : changes, 'sidebyside': sidebyside, 'unified': unified, 'diff_format' : request.query_dict.get('diff_format', cfg.options.diff_format), 'patch_href' : request.get_url(view_func=view_patch, params=no_format_params, escape=1), 'diff_format_action' : diff_format_action, 'diff_format_hidden_values' : diff_format_hidden_values, })) generate_page(request, "diff", data) def generate_tarball_header(out, name, size=0, mode=None, mtime=0, uid=0, gid=0, typeflag=None, linkname='', uname='viewvc', gname='viewvc', devmajor=1, devminor=0, prefix=None, magic='ustar', version='00', chksum=None): if not mode: if name[-1:] == '/': mode = 0755 else: mode = 0644 if not typeflag: if linkname: typeflag = '2' # symbolic link elif name[-1:] == '/': typeflag = '5' # directory else: typeflag = '0' # regular file if not prefix: prefix = '' # generate a GNU tar extension header for a long name. if len(name) >= 100: generate_tarball_header(out, '././@LongLink', len(name), 0, 0, 0, 0, 'L') out.write(name) out.write('\0' * (511 - ((len(name) + 511) % 512))) # generate a GNU tar extension header for a long symlink name. if len(linkname) >= 100: generate_tarball_header(out, '././@LongLink', len(linkname), 0, 0, 0, 0, 'K') out.write(linkname) out.write('\0' * (511 - ((len(linkname) + 511) % 512))) block1 = struct.pack('100s 8s 8s 8s 12s 12s', name, '%07o' % mode, '%07o' % uid, '%07o' % gid, '%011o' % size, '%011o' % mtime) block2 = struct.pack('c 100s 6s 2s 32s 32s 8s 8s 155s', typeflag, linkname, magic, version, uname, gname, '%07o' % devmajor, '%07o' % devminor, prefix) if not chksum: dummy_chksum = ' ' block = block1 + dummy_chksum + block2 chksum = 0 for i in range(len(block)): chksum = chksum + ord(block[i]) block = block1 + struct.pack('8s', '%07o' % chksum) + block2 block = block + '\0' * (512 - len(block)) out.write(block) def generate_tarball(out, request, reldir, stack, dir_mtime=None): # get directory info from repository rep_path = request.path_parts + reldir entries = request.repos.listdir(rep_path, request.pathrev, {}) request.repos.dirlogs(rep_path, request.pathrev, entries, {}) entries.sort(lambda a, b: cmp(a.name, b.name)) # figure out corresponding path in tar file. everything gets put underneath # a single top level directory named after the repository directory being # tarred if request.path_parts: tar_dir = request.path_parts[-1] + '/' else: tar_dir = request.rootname + '/' if reldir: tar_dir = tar_dir + _path_join(reldir) + '/' cvs = request.roottype == 'cvs' # If our caller doesn't dictate a datestamp to use for the current # directory, its datestamps will be the youngest of the datestamps # of versioned items in that subdirectory. We'll be ignoring dead # or busted items and, in CVS, subdirs. if dir_mtime is None: dir_mtime = 0 for file in entries: if cvs and (file.kind != vclib.FILE or file.rev is None or file.dead): continue if (file.date is not None) and (file.date > dir_mtime): dir_mtime = file.date # Push current directory onto the stack. stack.append(tar_dir) # If this is Subversion, we generate a header for this directory # regardless of its contents. For CVS it will only get into the # tarball if it has files underneath it, which we determine later. if not cvs: generate_tarball_header(out, tar_dir, mtime=dir_mtime) # Run through the files in this directory, skipping busted and # unauthorized ones. for file in entries: if file.kind != vclib.FILE: continue if cvs and (file.rev is None or file.dead): continue # If we get here, we've seen at least one valid file in the # current directory. For CVS, we need to make sure there are # directory parents to contain it, so we flush the stack. if cvs: for dir in stack: generate_tarball_header(out, dir, mtime=dir_mtime) del stack[:] # Calculate the mode for the file. Sure, we could look directly # at the ,v file in CVS, but that's a layering violation we'd like # to avoid as much as possible. if request.repos.isexecutable(rep_path + [file.name], request.pathrev): mode = 0755 else: mode = 0644 # Is this thing a symlink? # ### FIXME: A better solution would be to have vclib returning ### symlinks with a new vclib.SYMLINK path type. symlink_target = None if hasattr(request.repos, 'get_symlink_target'): symlink_target = request.repos.get_symlink_target(rep_path + [file.name], request.pathrev) # If the object is a symlink, generate the appropriate header. # Otherwise, we're dealing with a regular file. if symlink_target: generate_tarball_header(out, tar_dir + file.name, 0, mode, file.date is not None and file.date or 0, typeflag='2', linkname=symlink_target) else: filesize = request.repos.filesize(rep_path + [file.name], request.pathrev) if filesize == -1: # Bummer. We have to calculate the filesize manually. fp = request.repos.openfile(rep_path + [file.name], request.pathrev, {})[0] filesize = 0 while 1: chunk = retry_read(fp) if not chunk: break filesize = filesize + len(chunk) fp.close() # Write the tarball header... generate_tarball_header(out, tar_dir + file.name, filesize, mode, file.date is not None and file.date or 0) # ...the file's contents ... fp = request.repos.openfile(rep_path + [file.name], request.pathrev, {})[0] while 1: chunk = retry_read(fp) if not chunk: break out.write(chunk) fp.close() # ... and then add the block padding. out.write('\0' * (511 - (filesize + 511) % 512)) # Recurse into subdirectories, skipping busted and unauthorized (or # configured-to-be-hidden) ones. for file in entries: if file.errors or file.kind != vclib.DIR: continue if request.cfg.options.hide_cvsroot \ and is_cvsroot_path(request.roottype, rep_path + [file.name]): continue mtime = request.roottype == 'svn' and file.date or None generate_tarball(out, request, reldir + [file.name], stack, mtime) # Pop the current directory from the stack. del stack[-1:] def download_tarball(request): cfg = request.cfg if 'tar' not in request.cfg.options.allowed_views: raise debug.ViewVCException('Tarball generation is disabled', '403 Forbidden') # If debugging, we just need to open up the specified tar path for # writing. Otherwise, we get a writeable server output stream -- # disabling any default compression thereupon -- and wrap that in # our own gzip stream wrapper. if debug.TARFILE_PATH: fp = open(debug.TARFILE_PATH, 'w') else: tarfile = request.rootname if request.path_parts: tarfile = "%s-%s" % (tarfile, request.path_parts[-1]) request.server.addheader('Content-Disposition', 'attachment; filename="%s.tar.gz"' % (tarfile)) server_fp = get_writeready_server_file(request, 'application/x-gzip', allow_compress=False) request.server.flush() fp = gzip.GzipFile('', 'wb', 9, server_fp) ### FIXME: For Subversion repositories, we can get the real mtime of the ### top-level directory here. generate_tarball(fp, request, [], []) fp.write('\0' * 1024) fp.close() if debug.TARFILE_PATH: request.server.header('') print """

Tarball '%s' successfully generated!

""" % (debug.TARFILE_PATH) def view_revision(request): if request.roottype != "svn": raise debug.ViewVCException("Revision view not supported for CVS " "repositories at this time.", "400 Bad Request") cfg = request.cfg query_dict = request.query_dict try: rev = request.repos._getrev(query_dict.get('revision')) except vclib.InvalidRevision: raise debug.ViewVCException('Invalid revision', '404 Not Found') youngest_rev = request.repos.get_youngest_revision() # The revision number acts as a weak validator (but we tell browsers # not to cache the youngest revision). if rev != youngest_rev and check_freshness(request, None, str(rev), weak=1): return # Fetch the revision information. date, author, msg, revprops, changes = request.repos.revinfo(rev) date_str = make_time_string(date, cfg) # Fix up the revprops list (rather like get_itemprops()). propnames = revprops.keys() propnames.sort() props = [] for name in propnames: lf = LogFormatter(request, revprops[name]) value = lf.get(maxlen=0, htmlize=1) undisplayable = ezt.boolean(0) # skip non-utf8 property names try: unicode(name, 'utf8') except: continue # note non-utf8 property values try: unicode(value, 'utf8') except: value = None undisplayable = ezt.boolean(1) props.append(_item(name=name, value=value, undisplayable=undisplayable)) # Sort the changes list by path. def changes_sort_by_path(a, b): return cmp(a.path_parts, b.path_parts) changes.sort(changes_sort_by_path) # Handle limit_changes parameter cfg_limit_changes = cfg.options.limit_changes limit_changes = int(query_dict.get('limit_changes', cfg_limit_changes)) more_changes = None more_changes_href = None first_changes = None first_changes_href = None num_changes = len(changes) if limit_changes and len(changes) > limit_changes: more_changes = len(changes) - limit_changes params = query_dict.copy() params['limit_changes'] = 0 more_changes_href = request.get_url(params=params, escape=1) changes = changes[:limit_changes] elif cfg_limit_changes and len(changes) > cfg_limit_changes: first_changes = cfg_limit_changes params = query_dict.copy() params['limit_changes'] = None first_changes_href = request.get_url(params=params, escape=1) # Add the hrefs, types, and prev info for change in changes: change.view_href = change.diff_href = change.type = change.log_href = None # If the path is newly added, don't claim text or property # modifications. if (change.action == vclib.ADDED or change.action == vclib.REPLACED) \ and not change.copied: change.text_changed = 0 change.props_changed = 0 # Calculate the view link URLs (for which we must have a pathtype). if change.pathtype: view_func = None if change.pathtype is vclib.FILE \ and 'markup' in cfg.options.allowed_views: view_func = view_markup elif change.pathtype is vclib.DIR: view_func = view_directory path = _path_join(change.path_parts) base_path = _path_join(change.base_path_parts) if change.action == vclib.DELETED: link_rev = str(change.base_rev) link_where = base_path else: link_rev = str(rev) link_where = path change.view_href = request.get_url(view_func=view_func, where=link_where, pathtype=change.pathtype, params={'pathrev' : link_rev}, escape=1) change.log_href = request.get_url(view_func=view_log, where=link_where, pathtype=change.pathtype, params={'pathrev' : link_rev}, escape=1) if change.pathtype is vclib.FILE and change.text_changed: change.diff_href = request.get_url(view_func=view_diff, where=path, pathtype=change.pathtype, params={'pathrev' : str(rev), 'r1' : str(rev), 'r2' : str(change.base_rev), }, escape=1) # use same variable names as the log template change.path = _path_join(change.path_parts) change.copy_path = _path_join(change.base_path_parts) change.copy_rev = change.base_rev change.text_mods = ezt.boolean(change.text_changed) change.prop_mods = ezt.boolean(change.props_changed) change.is_copy = ezt.boolean(change.copied) change.pathtype = (change.pathtype == vclib.FILE and 'file') \ or (change.pathtype == vclib.DIR and 'dir') \ or None del change.path_parts del change.base_path_parts del change.base_rev del change.text_changed del change.props_changed del change.copied prev_rev_href = next_rev_href = None if rev > 0: prev_rev_href = request.get_url(view_func=view_revision, where=None, pathtype=None, params={'revision': str(rev - 1)}, escape=1) if rev < request.repos.get_youngest_revision(): next_rev_href = request.get_url(view_func=view_revision, where=None, pathtype=None, params={'revision': str(rev + 1)}, escape=1) jump_rev_action, jump_rev_hidden_values = \ request.get_form(params={'revision': None}) lf = LogFormatter(request, msg) data = common_template_data(request) data.merge(ezt.TemplateData({ 'rev' : str(rev), 'author' : author, 'date' : date_str, 'log' : lf.get(maxlen=0, htmlize=1), 'properties' : props, 'ago' : date is not None and html_time(request, date, 1) or None, 'changes' : changes, 'prev_href' : prev_rev_href, 'next_href' : next_rev_href, 'num_changes' : num_changes, 'limit_changes': limit_changes, 'more_changes': more_changes, 'more_changes_href': more_changes_href, 'first_changes': first_changes, 'first_changes_href': first_changes_href, 'jump_rev_action' : jump_rev_action, 'jump_rev_hidden_values' : jump_rev_hidden_values, 'revision_href' : request.get_url(view_func=view_revision, where=None, pathtype=None, params={'revision': str(rev)}, escape=1), })) if rev == youngest_rev: request.server.addheader("Cache-control", "no-store") generate_page(request, "revision", data) def is_query_supported(request): """Returns true if querying is supported for the given path.""" return request.cfg.cvsdb.enabled \ and request.pathtype == vclib.DIR \ and request.roottype in ['cvs', 'svn'] def is_querydb_nonempty_for_root(request): """Return 1 iff commits database integration is supported *and* the current root is found in that database. Only does this check if check_database is set to 1.""" if request.cfg.cvsdb.enabled and request.roottype in ['cvs', 'svn']: if request.cfg.cvsdb.check_database_for_root: global cvsdb import cvsdb db = cvsdb.ConnectDatabaseReadOnly(request.cfg) repos_root, repos_dir = cvsdb.FindRepository(db, request.rootpath) if repos_root: return 1 else: return 1 return 0 def validate_query_args(request): # Do some additional input validation of query form arguments beyond # what is offered by the CGI param validation loop in Request.run_viewvc(). for arg_base in ['branch', 'file', 'comment', 'who']: # First, make sure the the XXX_match args have valid values: arg_match = arg_base + '_match' arg_match_value = request.query_dict.get(arg_match, 'exact') if not arg_match_value in ('exact', 'like', 'glob', 'regex', 'notregex'): raise debug.ViewVCException( 'An illegal value was provided for the "%s" parameter.' % (arg_match), '400 Bad Request') # Now, for those args which are supposed to be regular expressions (per # their corresponding XXX_match values), make sure they are. if arg_match_value == 'regex' or arg_match_value == 'notregex': arg_base_value = request.query_dict.get(arg_base) if arg_base_value: try: re.compile(arg_base_value) except: raise debug.ViewVCException( 'An illegal value was provided for the "%s" parameter.' % (arg_base), '400 Bad Request') def view_queryform(request): if not is_query_supported(request): raise debug.ViewVCException('Can not query project root "%s" at "%s".' % (request.rootname, request.where), '403 Forbidden') # Do some more precise input validation. validate_query_args(request) query_action, query_hidden_values = \ request.get_form(view_func=view_query, params={'limit_changes': None}) limit_changes = \ int(request.query_dict.get('limit_changes', request.cfg.options.limit_changes)) def escaped_query_dict_get(itemname, itemdefault=''): return request.server.escape(request.query_dict.get(itemname, itemdefault)) data = common_template_data(request) data.merge(ezt.TemplateData({ 'branch' : escaped_query_dict_get('branch', ''), 'branch_match' : escaped_query_dict_get('branch_match', 'exact'), 'dir' : escaped_query_dict_get('dir', ''), 'file' : escaped_query_dict_get('file', ''), 'file_match' : escaped_query_dict_get('file_match', 'exact'), 'who' : escaped_query_dict_get('who', ''), 'who_match' : escaped_query_dict_get('who_match', 'exact'), 'comment' : escaped_query_dict_get('comment', ''), 'comment_match' : escaped_query_dict_get('comment_match', 'exact'), 'querysort' : escaped_query_dict_get('querysort', 'date'), 'date' : escaped_query_dict_get('date', 'hours'), 'hours' : escaped_query_dict_get('hours', '2'), 'mindate' : escaped_query_dict_get('mindate', ''), 'maxdate' : escaped_query_dict_get('maxdate', ''), 'query_action' : query_action, 'query_hidden_values' : query_hidden_values, 'limit_changes' : limit_changes, 'dir_href' : request.get_url(view_func=view_directory, params={}, escape=1), })) generate_page(request, "query_form", data) def parse_date(datestr): """Parse a date string from the query form.""" match = re.match(r'^(\d\d\d\d)-(\d\d)-(\d\d)(?:\ +' '(\d\d):(\d\d)(?::(\d\d))?)?$', datestr) if match: year = int(match.group(1)) month = int(match.group(2)) day = int(match.group(3)) hour = match.group(4) if hour is not None: hour = int(hour) else: hour = 0 minute = match.group(5) if minute is not None: minute = int(minute) else: minute = 0 second = match.group(6) if second is not None: second = int(second) else: second = 0 # return a "seconds since epoch" value assuming date given in UTC tm = (year, month, day, hour, minute, second, 0, 0, 0) return compat.timegm(tm) else: return None def english_query(request): """Generate a sentance describing the query.""" cfg = request.cfg ret = [ 'Checkins ' ] dir = request.query_dict.get('dir', '') if dir: ret.append('to ') if ',' in dir: ret.append('subdirectories') else: ret.append('subdirectory') ret.append(' %s ' % request.server.escape(dir)) file = request.query_dict.get('file', '') if file: if len(ret) != 1: ret.append('and ') ret.append('to file %s ' % request.server.escape(file)) who = request.query_dict.get('who', '') branch = request.query_dict.get('branch', '') if branch: ret.append('on branch %s ' % request.server.escape(branch)) else: ret.append('on all branches ') comment = request.query_dict.get('comment', '') if comment: ret.append('with comment %s ' % request.server.escape(comment)) if who: ret.append('by %s ' % request.server.escape(who)) date = request.query_dict.get('date', 'hours') if date == 'hours': ret.append('in the last %s hours' \ % request.server.escape(request.query_dict.get('hours', '2'))) elif date == 'day': ret.append('in the last day') elif date == 'week': ret.append('in the last week') elif date == 'month': ret.append('in the last month') elif date == 'all': ret.append('since the beginning of time') elif date == 'explicit': mindate = request.query_dict.get('mindate', '') maxdate = request.query_dict.get('maxdate', '') if mindate and maxdate: w1, w2 = 'between', 'and' else: w1, w2 = 'since', 'before' if mindate: mindate = make_time_string(parse_date(mindate), cfg) ret.append('%s %s ' % (w1, mindate)) if maxdate: maxdate = make_time_string(parse_date(maxdate), cfg) ret.append('%s %s ' % (w2, maxdate)) return string.join(ret, '') def prev_rev(rev): """Returns a string representing the previous revision of the argument.""" r = string.split(rev, '.') # decrement final revision component r[-1] = str(int(r[-1]) - 1) # prune if we pass the beginning of the branch if len(r) > 2 and r[-1] == '0': r = r[:-2] return string.join(r, '.') def build_commit(request, files, max_files, dir_strip, format): """Return a commit object build from the information in FILES, or None if no allowed files are present in the set. DIR_STRIP is the path prefix to remove from the commit object's set of files. If MAX_FILES is non-zero, it is used to limit the number of files returned in the commit object. FORMAT is the requested output format of the query request.""" cfg = request.cfg author = files[0].GetAuthor() date = files[0].GetTime() desc = files[0].GetDescription() commit_rev = files[0].GetRevision() len_strip = len(dir_strip) commit_files = [] num_allowed = 0 plus_count = 0 minus_count = 0 found_unreadable = 0 for f in files: dirname = f.GetDirectory() filename = f.GetFile() if dir_strip: assert dirname[:len_strip] == dir_strip assert len(dirname) == len_strip or dirname[len(dir_strip)] == '/' dirname = dirname[len_strip+1:] where = dirname and ("%s/%s" % (dirname, filename)) or filename rev = f.GetRevision() rev_prev = prev_rev(rev) commit_time = f.GetTime() if commit_time: commit_time = make_time_string(commit_time, cfg) change_type = f.GetTypeString() # In CVS, we can actually look at deleted revisions; in Subversion # we can't -- we'll look at the previous revision instead. exam_rev = rev if request.roottype == 'svn' and change_type == 'Remove': exam_rev = rev_prev # Check path access (since the commits database logic bypasses the # vclib layer and, thus, the vcauth stuff that layer uses). path_parts = _path_parts(where) if path_parts: # Skip files in CVSROOT if asked to hide such. if cfg.options.hide_cvsroot \ and is_cvsroot_path(request.roottype, path_parts): found_unreadable = 1 continue # We have to do a rare authz check here because this data comes # from the CVSdb, not from the vclib providers. # # WARNING: The Subversion CVSdb integration logic is weak, weak, # weak. It has no ability to track copies, so complex # situations like a copied directory with a deleted subfile (all # in the same revision) are very ... difficult. We've no choice # but to omit as unauthorized paths the authorization logic # can't find. try: readable = vclib.check_path_access(request.repos, path_parts, None, exam_rev) except vclib.ItemNotFound: readable = 0 if not readable: found_unreadable = 1 continue if request.roottype == 'svn': params = { 'pathrev': exam_rev } else: params = { 'revision': exam_rev, 'pathrev': f.GetBranch() or None } dir_href = request.get_url(view_func=view_directory, where=dirname, pathtype=vclib.DIR, params=params, escape=1) log_href = request.get_url(view_func=view_log, where=where, pathtype=vclib.FILE, params=params, escape=1) diff_href = view_href = download_href = None if 'markup' in cfg.options.allowed_views: view_href = request.get_url(view_func=view_markup, where=where, pathtype=vclib.FILE, params=params, escape=1) if 'co' in cfg.options.allowed_views: download_href = request.get_url(view_func=view_checkout, where=where, pathtype=vclib.FILE, params=params, escape=1) if change_type == 'Change': diff_href_params = params.copy() diff_href_params.update({ 'r1': rev_prev, 'r2': rev, 'diff_format': None }) diff_href = request.get_url(view_func=view_diff, where=where, pathtype=vclib.FILE, params=diff_href_params, escape=1) mime_type, encoding = calculate_mime_type(request, path_parts, exam_rev) prefer_markup = ezt.boolean(default_view(mime_type, cfg) == view_markup) # Update plus/minus line change count. plus = int(f.GetPlusCount()) minus = int(f.GetMinusCount()) plus_count = plus_count + plus minus_count = minus_count + minus num_allowed = num_allowed + 1 if max_files and num_allowed > max_files: continue commit_files.append(_item(date=commit_time, dir=request.server.escape(dirname), file=request.server.escape(filename), author=request.server.escape(f.GetAuthor()), rev=rev, branch=f.GetBranch(), plus=plus, minus=minus, type=change_type, dir_href=dir_href, log_href=log_href, view_href=view_href, download_href=download_href, prefer_markup=prefer_markup, diff_href=diff_href)) # No files survived authz checks? Let's just pretend this # little commit didn't happen, shall we? if not len(commit_files): return None commit = _item(num_files=len(commit_files), files=commit_files, plus=plus_count, minus=minus_count) commit.limited_files = ezt.boolean(num_allowed > len(commit_files)) # We'll mask log messages in commits which contain unreadable paths, # but even that is kinda iffy. If a person searches for # '/some/hidden/path' across log messages, then gets a response set # that shows commits lacking log message, said person can reasonably # assume that the log messages contained the hidden path, and that # this is likely because they are referencing a real path in the # repository -- a path the user isn't supposed to even know about. if found_unreadable: commit.log = None commit.short_log = None else: lf = LogFormatter(request, desc) htmlize = (format != 'rss') commit.log = lf.get(maxlen=0, htmlize=htmlize) commit.short_log = lf.get(maxlen=cfg.options.short_log_len, htmlize=htmlize) commit.author = request.server.escape(author) commit.rss_date = make_rss_time_string(date, request.cfg) if request.roottype == 'svn': commit.rev = commit_rev commit.rss_url = '%s://%s%s' % \ (request.server.getenv("HTTPS") == "on" and "https" or "http", request.server.getenv("HTTP_HOST"), request.get_url(view_func=view_revision, params={'revision': commit.rev}, escape=1)) else: commit.rev = None commit.rss_url = None return commit def query_backout(request, commits): server_fp = get_writeready_server_file(request, 'text/plain') if not commits: server_fp.write("""\ # No changes were selected by the query. # There is nothing to back out. """) return server_fp.write("""\ # This page can be saved as a shell script and executed. # It should be run at the top of your work area. It will update # your working copy to back out the changes selected by the # query. """) for commit in commits: for fileinfo in commit.files: if request.roottype == 'cvs': server_fp.write('cvs update -j %s -j %s %s/%s\n' % (fileinfo.rev, prev_rev(fileinfo.rev), fileinfo.dir, fileinfo.file)) elif request.roottype == 'svn': server_fp.write('svn merge -r %s:%s %s/%s\n' % (fileinfo.rev, prev_rev(fileinfo.rev), fileinfo.dir, fileinfo.file)) def view_query(request): if not is_query_supported(request): raise debug.ViewVCException('Can not query project root "%s" at "%s".' % (request.rootname, request.where), '403 Forbidden') cfg = request.cfg # Do some more precise input validation. validate_query_args(request) # get form data branch = request.query_dict.get('branch', '') branch_match = request.query_dict.get('branch_match', 'exact') dir = request.query_dict.get('dir', '') file = request.query_dict.get('file', '') file_match = request.query_dict.get('file_match', 'exact') who = request.query_dict.get('who', '') who_match = request.query_dict.get('who_match', 'exact') comment = request.query_dict.get('comment', '') comment_match = request.query_dict.get('comment_match', 'exact') querysort = request.query_dict.get('querysort', 'date') date = request.query_dict.get('date', 'hours') hours = request.query_dict.get('hours', '2') mindate = request.query_dict.get('mindate', '') maxdate = request.query_dict.get('maxdate', '') format = request.query_dict.get('format') limit_changes = int(request.query_dict.get('limit_changes', cfg.options.limit_changes)) match_types = { 'exact':1, 'like':1, 'glob':1, 'regex':1, 'notregex':1 } sort_types = { 'date':1, 'author':1, 'file':1 } date_types = { 'hours':1, 'day':1, 'week':1, 'month':1, 'all':1, 'explicit':1 } # parse various fields, validating or converting them if not match_types.has_key(branch_match): branch_match = 'exact' if not match_types.has_key(file_match): file_match = 'exact' if not match_types.has_key(who_match): who_match = 'exact' if not match_types.has_key(comment_match): comment_match = 'exact' if not sort_types.has_key(querysort): querysort = 'date' if not date_types.has_key(date): date = 'hours' mindate = parse_date(mindate) maxdate = parse_date(maxdate) global cvsdb import cvsdb db = cvsdb.ConnectDatabaseReadOnly(cfg) repos_root, repos_dir = cvsdb.FindRepository(db, request.rootpath) if not repos_root: raise debug.ViewVCException( "The root '%s' was not found in the commit database " % request.rootname) # create the database query from the form data query = cvsdb.CreateCheckinQuery() query.SetRepository(repos_root) # treat "HEAD" specially ... if branch_match == 'exact' and branch == 'HEAD': query.SetBranch('') elif branch: query.SetBranch(branch, branch_match) if dir: for subdir in string.split(dir, ','): path = (_path_join(repos_dir + request.path_parts + _path_parts(string.strip(subdir)))) query.SetDirectory(path, 'exact') query.SetDirectory('%s/%%' % cvsdb.EscapeLike(path), 'like') else: where = _path_join(repos_dir + request.path_parts) if where: # if we are in a subdirectory ... query.SetDirectory(where, 'exact') query.SetDirectory('%s/%%' % cvsdb.EscapeLike(where), 'like') if file: query.SetFile(file, file_match) if who: query.SetAuthor(who, who_match) if comment: query.SetComment(comment, comment_match) query.SetSortMethod(querysort) if date == 'hours': query.SetFromDateHoursAgo(int(hours)) elif date == 'day': query.SetFromDateDaysAgo(1) elif date == 'week': query.SetFromDateDaysAgo(7) elif date == 'month': query.SetFromDateDaysAgo(31) elif date == 'all': pass elif date == 'explicit': if mindate is not None: query.SetFromDateObject(mindate) if maxdate is not None: query.SetToDateObject(maxdate) # Set the admin-defined (via configuration) row limits. This is to avoid # slamming the database server with a monster query. if format == 'rss': query.SetLimit(cfg.cvsdb.rss_row_limit) else: query.SetLimit(cfg.cvsdb.row_limit) # run the query db.RunQuery(query) commit_list = query.GetCommitList() row_limit_reached = query.GetLimitReached() # gather commits commits = [] plus_count = 0 minus_count = 0 mod_time = -1 if commit_list: files = [] limited_files = 0 current_desc = commit_list[0].GetDescriptionID() current_rev = commit_list[0].GetRevision() dir_strip = _path_join(repos_dir) for commit in commit_list: commit_desc = commit.GetDescriptionID() commit_rev = commit.GetRevision() # base modification time on the newest commit if commit.GetTime() > mod_time: mod_time = commit.GetTime() # For CVS, group commits with the same commit message. # For Subversion, group them only if they have the same revision number if request.roottype == 'cvs': if current_desc == commit_desc: files.append(commit) continue else: if current_rev == commit_rev: files.append(commit) continue # append this grouping commit_item = build_commit(request, files, limit_changes, dir_strip, format) if commit_item: # update running plus/minus totals plus_count = plus_count + commit_item.plus minus_count = minus_count + commit_item.minus commits.append(commit_item) files = [ commit ] limited_files = 0 current_desc = commit_desc current_rev = commit_rev # we need to tack on our last commit grouping, if any commit_item = build_commit(request, files, limit_changes, dir_strip, format) if commit_item: # update running plus/minus totals plus_count = plus_count + commit_item.plus minus_count = minus_count + commit_item.minus commits.append(commit_item) # only show the branch column if we are querying all branches # or doing a non-exact branch match on a CVS repository. show_branch = ezt.boolean(request.roottype == 'cvs' and (branch == '' or branch_match != 'exact')) # backout link params = request.query_dict.copy() params['format'] = 'backout' backout_href = request.get_url(params=params, escape=1) # link to zero limit_changes value params = request.query_dict.copy() params['limit_changes'] = 0 limit_changes_href = request.get_url(params=params, escape=1) # if we got any results, use the newest commit as the modification time if mod_time >= 0: if check_freshness(request, mod_time): return if format == 'backout': query_backout(request, commits) return data = common_template_data(request) data.merge(ezt.TemplateData({ 'sql': request.server.escape(db.CreateSQLQueryString(query)), 'english_query': english_query(request), 'queryform_href': request.get_url(view_func=view_queryform, escape=1), 'backout_href': backout_href, 'plus_count': plus_count, 'minus_count': minus_count, 'show_branch': show_branch, 'querysort': querysort, 'commits': commits, 'row_limit_reached' : ezt.boolean(row_limit_reached), 'limit_changes': limit_changes, 'limit_changes_href': limit_changes_href, 'rss_link_href': request.get_url(view_func=view_query, params={'date': 'month'}, escape=1, prefix=1), })) if format == 'rss': generate_page(request, "rss", data, "application/rss+xml") else: generate_page(request, "query_results", data) _views = { 'annotate': view_annotate, 'co': view_checkout, 'diff': view_diff, 'dir': view_directory, 'graph': view_cvsgraph, 'graphimg': view_cvsgraph_image, 'log': view_log, 'markup': view_markup, 'patch': view_patch, 'query': view_query, 'queryform': view_queryform, 'revision': view_revision, 'roots': view_roots, 'tar': download_tarball, 'redirect_pathrev': redirect_pathrev, } _view_codes = {} for code, view in _views.items(): _view_codes[view] = code def list_roots(request): cfg = request.cfg allroots = { } # Add the viewable Subversion roots for root in cfg.general.svn_roots.keys(): auth = setup_authorizer(cfg, request.username, root) try: repos = vclib.svn.SubversionRepository(root, cfg.general.svn_roots[root], auth, cfg.utilities, cfg.options.svn_config_dir) lastmod = None if cfg.options.show_roots_lastmod: try: repos.open() youngest_rev = repos.youngest date, author, msg, revprops, changes = repos.revinfo(youngest_rev) date_str = make_time_string(date, cfg) ago = html_time(request, date) lf = LogFormatter(request, msg) log = lf.get(maxlen=0, htmlize=1) short_log = lf.get(maxlen=cfg.options.short_log_len, htmlize=1) lastmod = _item(ago=ago, author=author, date=date_str, log=log, short_log=short_log, rev=str(youngest_rev)) except: lastmod = None except vclib.ReposNotFound: continue allroots[root] = [cfg.general.svn_roots[root], 'svn', lastmod] # Add the viewable CVS roots for root in cfg.general.cvs_roots.keys(): auth = setup_authorizer(cfg, request.username, root) try: vclib.ccvs.CVSRepository(root, cfg.general.cvs_roots[root], auth, cfg.utilities, cfg.options.use_rcsparse) except vclib.ReposNotFound: continue allroots[root] = [cfg.general.cvs_roots[root], 'cvs', None] return allroots def expand_root_parents(cfg): """Expand the configured root parents into individual roots.""" # Each item in root_parents is a "directory : repo_type" string. for pp in cfg.general.root_parents: pos = string.rfind(pp, ':') if pos < 0: raise debug.ViewVCException( 'The path "%s" in "root_parents" does not include a ' 'repository type. Expected "cvs" or "svn".' % (pp)) repo_type = string.strip(pp[pos+1:]) pp = os.path.normpath(string.strip(pp[:pos])) if repo_type == 'cvs': roots = vclib.ccvs.expand_root_parent(pp) if cfg.options.hide_cvsroot and roots.has_key('CVSROOT'): del roots['CVSROOT'] cfg.general.cvs_roots.update(roots) elif repo_type == 'svn': roots = vclib.svn.expand_root_parent(pp) cfg.general.svn_roots.update(roots) else: raise debug.ViewVCException( 'The path "%s" in "root_parents" has an unrecognized ' 'repository type ("%s"). Expected "cvs" or "svn".' % (pp, repo_type)) def find_root_in_parents(cfg, rootname, roottype): """Return the rootpath for configured ROOTNAME of ROOTTYPE.""" # Easy out: caller wants rootname "CVSROOT", and we're hiding those. if rootname == 'CVSROOT' and cfg.options.hide_cvsroot: return None for pp in cfg.general.root_parents: pos = string.rfind(pp, ':') if pos < 0: continue repo_type = string.strip(pp[pos+1:]) if repo_type != roottype: continue pp = os.path.normpath(string.strip(pp[:pos])) rootpath = None if roottype == 'cvs': rootpath = vclib.ccvs.find_root_in_parent(pp, rootname) elif roottype == 'svn': rootpath = vclib.svn.find_root_in_parent(pp, rootname) if rootpath is not None: return rootpath return None def locate_root(cfg, rootname): """Return a 2-tuple ROOTTYPE, ROOTPATH for configured ROOTNAME.""" if cfg.general.cvs_roots.has_key(rootname): return 'cvs', cfg.general.cvs_roots[rootname] path_in_parent = find_root_in_parents(cfg, rootname, 'cvs') if path_in_parent: cfg.general.cvs_roots[rootname] = path_in_parent return 'cvs', path_in_parent if cfg.general.svn_roots.has_key(rootname): return 'svn', cfg.general.svn_roots[rootname] path_in_parent = find_root_in_parents(cfg, rootname, 'svn') if path_in_parent: cfg.general.svn_roots[rootname] = path_in_parent return 'svn', path_in_parent return None, None def load_config(pathname=None, server=None): """Load the ViewVC configuration file. SERVER is the server object that will be using this configuration. Consult the environment for the variable VIEWVC_CONF_PATHNAME and VIEWCVS_CONF_PATHNAME (its legacy name) and, if set, use its value as the path of the configuration file; otherwise, use PATHNAME (if provided). Failing all else, use a hardcoded default configuration path.""" debug.t_start('load-config') # See if the environment contains overrides to the configuration # path. If we have a SERVER object, consult its environment; use # the OS environment otherwise. env_pathname = None if server is not None: env_pathname = (server.getenv("VIEWVC_CONF_PATHNAME") or server.getenv("VIEWCVS_CONF_PATHNAME")) else: env_pathname = (os.environ.get("VIEWVC_CONF_PATHNAME") or os.environ.get("VIEWCVS_CONF_PATHNAME")) # Try to find the configuration pathname by searching these ordered # locations: the environment, the passed-in PATHNAME, the hard-coded # default. pathname = (env_pathname or pathname or os.path.join(os.path.dirname(os.path.dirname(__file__)), "viewvc.conf")) # Load the configuration! cfg = config.Config() cfg.set_defaults() cfg.load_config(pathname, server and server.getenv("HTTP_HOST")) # Load mime types file(s), but reverse the order -- our # configuration uses a most-to-least preferred approach, but the # 'mimetypes' package wants things the other way around. if cfg.general.mime_types_files: files = cfg.general.mime_types_files[:] files.reverse() files = map(lambda x, y=pathname: os.path.join(os.path.dirname(y), x), files) mimetypes.init(files) debug.t_end('load-config') return cfg def view_error(server, cfg): exc_dict = debug.GetExceptionData() status = exc_dict['status'] if exc_dict['msg']: exc_dict['msg'] = server.escape(exc_dict['msg']) if exc_dict['stacktrace']: exc_dict['stacktrace'] = server.escape(exc_dict['stacktrace']) handled = 0 # use the configured error template if possible try: if cfg and not server.headerSent: server.header(status=status) template = get_view_template(cfg, "error") template.generate(server.file(), exc_dict) handled = 1 except: pass # but fallback to the old exception printer if no configuration is # available, or if something went wrong if not handled: debug.PrintException(server, exc_dict) def main(server, cfg): try: debug.t_start('main') try: # build a Request object, which contains info about the HTTP request request = Request(server, cfg) request.run_viewvc() except SystemExit, e: return except: view_error(server, cfg) finally: debug.t_end('main') debug.t_dump(server.file()) debug.DumpChildren(server) class _item: def __init__(self, **kw): vars(self).update(kw) viewvc-1.1.22/lib/PyFontify.py0000644000175000017500000001211310542031164016343 0ustar cmpilatocmpilato"""Module to analyze Python source code; for syntax coloring tools. Interface: tags = fontify(pytext, searchfrom, searchto) The PYTEXT argument is a string containing Python source code. The (optional) arguments SEARCHFROM and SEARCHTO may contain a slice in PYTEXT. The returned value is a list of tuples, formatted like this: [('keyword', 0, 6, None), ('keyword', 11, 17, None), ('comment', 23, 53, None), ... ] The tuple contents are always like this: (tag, startindex, endindex, sublist) TAG is one of 'keyword', 'string', 'comment' or 'identifier' SUBLIST is not used, hence always None. """ # Based on FontText.py by Mitchell S. Chapman, # which was modified by Zachary Roadhouse, # then un-Tk'd by Just van Rossum. # Many thanks for regular expression debugging & authoring are due to: # Tim (the-incredib-ly y'rs) Peters and Cristian Tismer # So, who owns the copyright? ;-) How about this: # Copyright 1996-1997: # Mitchell S. Chapman, # Zachary Roadhouse, # Tim Peters, # Just van Rossum __version__ = "0.3.1" import string, re # This list of keywords is taken from ref/node13.html of the # Python 1.3 HTML documentation. ("access" is intentionally omitted.) keywordsList = ["and", "assert", "break", "class", "continue", "def", "del", "elif", "else", "except", "exec", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "not", "or", "pass", "print", "raise", "return", "try", "while", ] # A regexp for matching Python comments. commentPat = "#.*" # A regexp for matching simple quoted strings. pat = "q[^q\\n]*(\\[\000-\377][^q\\n]*)*q" quotePat = string.replace(pat, "q", "'") + "|" + string.replace(pat, 'q', '"') # A regexp for matching multi-line tripled-quoted strings. (Way to go, Tim!) pat = """ qqq [^q]* ( ( \\[\000-\377] | q ( \\[\000-\377] | [^q] | q ( \\[\000-\377] | [^q] ) ) ) [^q]* )* qqq """ pat = string.join(string.split(pat), '') # get rid of whitespace tripleQuotePat = string.replace(pat, "q", "'") + "|" \ + string.replace(pat, 'q', '"') # A regexp which matches all and only Python keywords. This will let # us skip the uninteresting identifier references. nonKeyPat = "(^|[^a-zA-Z0-9_.\"'])" # legal keyword-preceding characters keyPat = nonKeyPat + "(" + string.join(keywordsList, "|") + ")" + nonKeyPat # Our final syntax-matching regexp is the concatation of the regexp's we # constructed above. syntaxPat = keyPat + \ "|" + commentPat + \ "|" + tripleQuotePat + \ "|" + quotePat syntaxRE = re.compile(syntaxPat) # Finally, we construct a regexp for matching indentifiers (with # optional leading whitespace). idKeyPat = "[ \t]*[A-Za-z_][A-Za-z_0-9.]*" idRE = re.compile(idKeyPat) def fontify(pytext, searchfrom=0, searchto=None): if searchto is None: searchto = len(pytext) tags = [] commentTag = 'comment' stringTag = 'string' keywordTag = 'keyword' identifierTag = 'identifier' start = 0 end = searchfrom while 1: # Look for some syntax token we're interested in. If find # nothing, we're done. matchobj = syntaxRE.search(pytext, end) if not matchobj: break # If we found something outside our search area, it doesn't # count (and we're done). start = matchobj.start() if start >= searchto: break match = matchobj.group(0) end = start + len(match) c = match[0] if c == '#': # We matched a comment. tags.append((commentTag, start, end, None)) elif c == '"' or c == '\'': # We matched a string. tags.append((stringTag, start, end, None)) else: # We matched a keyword. if start != searchfrom: # there's still a redundant char before and after it, strip! match = match[1:-1] start = start + 1 else: # This is the first keyword in the text. # Only a space at the end. match = match[:-1] end = end - 1 tags.append((keywordTag, start, end, None)) # If this was a defining keyword, look ahead to the # following identifier. if match in ["def", "class"]: matchobj = idRE.search(pytext, end) if matchobj: start = matchobj.start() if start == end and start < searchto: end = start + len(matchobj.group(0)) tags.append((identifierTag, start, end, None)) return tags def test(path): f = open(path) text = f.read() f.close() tags = fontify(text) for tag, start, end, sublist in tags: print tag, `text[start:end]` if __name__ == "__main__": import sys test(sys.argv[0]) viewvc-1.1.22/lib/compat_ndiff.py0000644000175000017500000003012410370563455017063 0ustar cmpilatocmpilato#! /usr/bin/env python # Module ndiff version 1.6.0 # Released to the public domain 08-Dec-2000, # by Tim Peters (tim.one@home.com). # Backported to Python 1.5.2 for ViewCVS by pf@artcom-gmbh.de, 24-Dec-2001 # Provided as-is; use at your own risk; no warranty; no promises; enjoy! """ndiff [-q] file1 file2 or ndiff (-r1 | -r2) < ndiff_output > file1_or_file2 Print a human-friendly file difference report to stdout. Both inter- and intra-line differences are noted. In the second form, recreate file1 (-r1) or file2 (-r2) on stdout, from an ndiff report on stdin. In the first form, if -q ("quiet") is not specified, the first two lines of output are -: file1 +: file2 Each remaining line begins with a two-letter code: "- " line unique to file1 "+ " line unique to file2 " " line common to both files "? " line not present in either input file Lines beginning with "? " attempt to guide the eye to intraline differences, and were not present in either input file. These lines can be confusing if the source files contain tab characters. The first file can be recovered by retaining only lines that begin with " " or "- ", and deleting those 2-character prefixes; use ndiff with -r1. The second file can be recovered similarly, but by retaining only " " and "+ " lines; use ndiff with -r2; or, on Unix, the second file can be recovered by piping the output through sed -n '/^[+ ] /s/^..//p' See module comments for details and programmatic interface. """ __version__ = 1, 6, 1 # SequenceMatcher tries to compute a "human-friendly diff" between # two sequences (chiefly picturing a file as a sequence of lines, # and a line as a sequence of characters, here). Unlike e.g. UNIX(tm) # diff, the fundamental notion is the longest *contiguous* & junk-free # matching subsequence. That's what catches peoples' eyes. The # Windows(tm) windiff has another interesting notion, pairing up elements # that appear uniquely in each sequence. That, and the method here, # appear to yield more intuitive difference reports than does diff. This # method appears to be the least vulnerable to synching up on blocks # of "junk lines", though (like blank lines in ordinary text files, # or maybe "

" lines in HTML files). That may be because this is # the only method of the 3 that has a *concept* of "junk" . # # Note that ndiff makes no claim to produce a *minimal* diff. To the # contrary, minimal diffs are often counter-intuitive, because they # synch up anywhere possible, sometimes accidental matches 100 pages # apart. Restricting synch points to contiguous matches preserves some # notion of locality, at the occasional cost of producing a longer diff. # # With respect to junk, an earlier version of ndiff simply refused to # *start* a match with a junk element. The result was cases like this: # before: private Thread currentThread; # after: private volatile Thread currentThread; # If you consider whitespace to be junk, the longest contiguous match # not starting with junk is "e Thread currentThread". So ndiff reported # that "e volatil" was inserted between the 't' and the 'e' in "private". # While an accurate view, to people that's absurd. The current version # looks for matching blocks that are entirely junk-free, then extends the # longest one of those as far as possible but only with matching junk. # So now "currentThread" is matched, then extended to suck up the # preceding blank; then "private" is matched, and extended to suck up the # following blank; then "Thread" is matched; and finally ndiff reports # that "volatile " was inserted before "Thread". The only quibble # remaining is that perhaps it was really the case that " volatile" # was inserted after "private". I can live with that . # # NOTE on junk: the module-level names # IS_LINE_JUNK # IS_CHARACTER_JUNK # can be set to any functions you like. The first one should accept # a single string argument, and return true iff the string is junk. # The default is whether the regexp r"\s*#?\s*$" matches (i.e., a # line without visible characters, except for at most one splat). # The second should accept a string of length 1 etc. The default is # whether the character is a blank or tab (note: bad idea to include # newline in this!). # # After setting those, you can call fcompare(f1name, f2name) with the # names of the files you want to compare. The difference report # is sent to stdout. Or you can call main(args), passing what would # have been in sys.argv[1:] had the cmd-line form been used. from compat_difflib import SequenceMatcher TRACE = 0 # define what "junk" means import re def IS_LINE_JUNK(line, pat=re.compile(r"\s*#?\s*$").match): return pat(line) is not None def IS_CHARACTER_JUNK(ch, ws=" \t"): return ch in ws del re # meant for dumping lines def dump(tag, x, lo, hi): for i in xrange(lo, hi): print tag, x[i], def plain_replace(a, alo, ahi, b, blo, bhi): assert alo < ahi and blo < bhi # dump the shorter block first -- reduces the burden on short-term # memory if the blocks are of very different sizes if bhi - blo < ahi - alo: dump('+', b, blo, bhi) dump('-', a, alo, ahi) else: dump('-', a, alo, ahi) dump('+', b, blo, bhi) # When replacing one block of lines with another, this guy searches # the blocks for *similar* lines; the best-matching pair (if any) is # used as a synch point, and intraline difference marking is done on # the similar pair. Lots of work, but often worth it. def fancy_replace(a, alo, ahi, b, blo, bhi): if TRACE: print '*** fancy_replace', alo, ahi, blo, bhi dump('>', a, alo, ahi) dump('<', b, blo, bhi) # don't synch up unless the lines have a similarity score of at # least cutoff; best_ratio tracks the best score seen so far best_ratio, cutoff = 0.74, 0.75 cruncher = SequenceMatcher(IS_CHARACTER_JUNK) eqi, eqj = None, None # 1st indices of equal lines (if any) # search for the pair that matches best without being identical # (identical lines must be junk lines, & we don't want to synch up # on junk -- unless we have to) for j in xrange(blo, bhi): bj = b[j] cruncher.set_seq2(bj) for i in xrange(alo, ahi): ai = a[i] if ai == bj: if eqi is None: eqi, eqj = i, j continue cruncher.set_seq1(ai) # computing similarity is expensive, so use the quick # upper bounds first -- have seen this speed up messy # compares by a factor of 3. # note that ratio() is only expensive to compute the first # time it's called on a sequence pair; the expensive part # of the computation is cached by cruncher if cruncher.real_quick_ratio() > best_ratio and \ cruncher.quick_ratio() > best_ratio and \ cruncher.ratio() > best_ratio: best_ratio, best_i, best_j = cruncher.ratio(), i, j if best_ratio < cutoff: # no non-identical "pretty close" pair if eqi is None: # no identical pair either -- treat it as a straight replace plain_replace(a, alo, ahi, b, blo, bhi) return # no close pair, but an identical pair -- synch up on that best_i, best_j, best_ratio = eqi, eqj, 1.0 else: # there's a close pair, so forget the identical pair (if any) eqi = None # a[best_i] very similar to b[best_j]; eqi is None iff they're not # identical if TRACE: print '*** best_ratio', best_ratio, best_i, best_j dump('>', a, best_i, best_i+1) dump('<', b, best_j, best_j+1) # pump out diffs from before the synch point fancy_helper(a, alo, best_i, b, blo, best_j) # do intraline marking on the synch pair aelt, belt = a[best_i], b[best_j] if eqi is None: # pump out a '-', '?', '+', '?' quad for the synched lines atags = btags = "" cruncher.set_seqs(aelt, belt) for tag, ai1, ai2, bj1, bj2 in cruncher.get_opcodes(): la, lb = ai2 - ai1, bj2 - bj1 if tag == 'replace': atags = atags + '^' * la btags = btags + '^' * lb elif tag == 'delete': atags = atags + '-' * la elif tag == 'insert': btags = btags + '+' * lb elif tag == 'equal': atags = atags + ' ' * la btags = btags + ' ' * lb else: raise ValueError, 'unknown tag ' + `tag` printq(aelt, belt, atags, btags) else: # the synch pair is identical print ' ', aelt, # pump out diffs from after the synch point fancy_helper(a, best_i+1, ahi, b, best_j+1, bhi) def fancy_helper(a, alo, ahi, b, blo, bhi): if alo < ahi: if blo < bhi: fancy_replace(a, alo, ahi, b, blo, bhi) else: dump('-', a, alo, ahi) elif blo < bhi: dump('+', b, blo, bhi) # Crap to deal with leading tabs in "?" output. Can hurt, but will # probably help most of the time. def printq(aline, bline, atags, btags): common = min(count_leading(aline, "\t"), count_leading(bline, "\t")) common = min(common, count_leading(atags[:common], " ")) print "-", aline, if count_leading(atags, " ") < len(atags): print "?", "\t" * common + atags[common:] print "+", bline, if count_leading(btags, " ") < len(btags): print "?", "\t" * common + btags[common:] def count_leading(line, ch): i, n = 0, len(line) while i < n and line[i] == ch: i = i+1 return i def fail(msg): import sys out = sys.stderr.write out(msg + "\n\n") out(__doc__) return 0 # open a file & return the file object; gripe and return 0 if it # couldn't be opened def fopen(fname): try: return open(fname, 'r') except IOError, detail: return fail("couldn't open " + fname + ": " + str(detail)) # open two files & spray the diff to stdout; return false iff a problem def fcompare(f1name, f2name): f1 = fopen(f1name) f2 = fopen(f2name) if not f1 or not f2: return 0 a = f1.readlines(); f1.close() b = f2.readlines(); f2.close() cruncher = SequenceMatcher(IS_LINE_JUNK, a, b) for tag, alo, ahi, blo, bhi in cruncher.get_opcodes(): if tag == 'replace': fancy_replace(a, alo, ahi, b, blo, bhi) elif tag == 'delete': dump('-', a, alo, ahi) elif tag == 'insert': dump('+', b, blo, bhi) elif tag == 'equal': dump(' ', a, alo, ahi) else: raise ValueError, 'unknown tag ' + `tag` return 1 # crack args (sys.argv[1:] is normal) & compare; # return false iff a problem def main(args): import getopt try: opts, args = getopt.getopt(args, "qr:") except getopt.error, detail: return fail(str(detail)) noisy = 1 qseen = rseen = 0 for opt, val in opts: if opt == "-q": qseen = 1 noisy = 0 elif opt == "-r": rseen = 1 whichfile = val if qseen and rseen: return fail("can't specify both -q and -r") if rseen: if args: return fail("no args allowed with -r option") if whichfile in "12": restore(whichfile) return 1 return fail("-r value must be 1 or 2") if len(args) != 2: return fail("need 2 filename args") f1name, f2name = args if noisy: print '-:', f1name print '+:', f2name return fcompare(f1name, f2name) def restore(which): import sys tag = {"1": "- ", "2": "+ "}[which] prefixes = (" ", tag) for line in sys.stdin.readlines(): if line[:2] in prefixes: print line[2:], if __name__ == '__main__': import sys args = sys.argv[1:] if "-profile" in args: import profile, pstats args.remove("-profile") statf = "ndiff.pro" profile.run("main(args)", statf) stats = pstats.Stats(statf) stats.strip_dirs().sort_stats('time').print_stats() else: main(args) viewvc-1.1.22/lib/idiff.py0000644000175000017500000001255412071623704015514 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # idiff: display differences between files highlighting intraline changes # # ----------------------------------------------------------------------- from __future__ import generators import difflib import sys import re import ezt import sapi def sidebyside(fromlines, tolines, context): """Generate side by side diff""" ### for some reason mdiff chokes on \n's in input lines line_strip = lambda line: line.rstrip("\n") fromlines = map(line_strip, fromlines) tolines = map(line_strip, tolines) gap = False for fromdata, todata, flag in difflib._mdiff(fromlines, tolines, context): if fromdata is None and todata is None and flag is None: gap = True else: from_item = _mdiff_split(flag, fromdata) to_item = _mdiff_split(flag, todata) yield _item(gap=ezt.boolean(gap), columns=(from_item, to_item)) gap = False _re_mdiff = re.compile("\0([+-^])(.*?)\1") def _mdiff_split(flag, (line_number, text)): """Break up row from mdiff output into segments""" segments = [] pos = 0 while True: m = _re_mdiff.search(text, pos) if not m: segments.append(_item(text=sapi.escape(text[pos:]), type=None)) break if m.start() > pos: segments.append(_item(text=sapi.escape(text[pos:m.start()]), type=None)) if m.group(1) == "+": segments.append(_item(text=sapi.escape(m.group(2)), type="add")) elif m.group(1) == "-": segments.append(_item(text=sapi.escape(m.group(2)), type="remove")) elif m.group(1) == "^": segments.append(_item(text=sapi.escape(m.group(2)), type="change")) pos = m.end() return _item(segments=segments, line_number=line_number) def unified(fromlines, tolines, context): """Generate unified diff""" diff = difflib.Differ().compare(fromlines, tolines) lastrow = None for row in _trim_context(diff, context): if row[0].startswith("? "): yield _differ_split(lastrow, row[0]) lastrow = None else: if lastrow: yield _differ_split(lastrow, None) lastrow = row if lastrow: yield _differ_split(lastrow, None) def _trim_context(lines, context_size): """Trim context lines that don't surround changes from Differ results yields (line, leftnum, rightnum, gap) tuples""" # circular buffer to hold context lines context_buffer = [None] * (context_size or 0) context_start = context_len = 0 # number of context lines left to print after encountering a change context_owed = 0 # current line numbers leftnum = rightnum = 0 # whether context lines have been dropped gap = False for line in lines: row = save = None if line.startswith("- "): leftnum = leftnum + 1 row = line, leftnum, None context_owed = context_size elif line.startswith("+ "): rightnum = rightnum + 1 row = line, None, rightnum context_owed = context_size else: if line.startswith(" "): leftnum = leftnum = leftnum + 1 rightnum = rightnum = rightnum + 1 if context_owed > 0: context_owed = context_owed - 1 elif context_size is not None: save = True row = line, leftnum, rightnum if save: # don't yield row right away, store it in buffer context_buffer[(context_start + context_len) % context_size] = row if context_len == context_size: context_start = (context_start + 1) % context_size gap = True else: context_len = context_len + 1 else: # yield row, but first drain stuff in buffer context_len == context_size while context_len: yield context_buffer[context_start] + (gap,) gap = False context_start = (context_start + 1) % context_size context_len = context_len - 1 yield row + (gap,) gap = False _re_differ = re.compile(r"[+-^]+") def _differ_split(row, guide): """Break row into segments using guide line""" line, left_number, right_number, gap = row if left_number and right_number: type = "" elif left_number: type = "remove" elif right_number: type = "add" segments = [] pos = 2 if guide: assert guide.startswith("? ") for m in _re_differ.finditer(guide, pos): if m.start() > pos: segments.append(_item(text=sapi.escape(line[pos:m.start()]), type=None)) segments.append(_item(text=sapi.escape(line[m.start():m.end()]), type="change")) pos = m.end() segments.append(_item(text=sapi.escape(line[pos:]), type=None)) return _item(gap=ezt.boolean(gap), type=type, segments=segments, left_number=left_number, right_number=right_number) class _item: def __init__(self, **kw): vars(self).update(kw) try: ### Using difflib._mdiff function here was the easiest way of obtaining ### intraline diffs for use in ViewVC, but it doesn't exist prior to ### Python 2.4 and is not part of the public difflib API, so for now ### fall back if it doesn't exist. difflib._mdiff except AttributeError: sidebyside = None viewvc-1.1.22/lib/config.py0000644000175000017500000004441112134325756015703 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # config.py: configuration utilities # # ----------------------------------------------------------------------- import sys import os import string import ConfigParser import fnmatch ######################################################################### # # CONFIGURATION # ------------- # # There are three forms of configuration: # # 1. edit the viewvc.conf created by the viewvc-install(er) # 2. as (1), but delete all unchanged entries from viewvc.conf # 3. do not use viewvc.conf and just edit the defaults in this file # # Most users will want to use (1), but there are slight speed advantages # to the other two options. Note that viewvc.conf values are a bit easier # to work with since it is raw text, rather than python literal values. # # # A WORD ABOUT OPTION LAYERING/OVERRIDES # -------------------------------------- # # ViewVC has three "layers" of configuration options: # # 1. base configuration options - very basic configuration bits # found in sections like 'general', 'options', etc. # 2. vhost overrides - these options overlay/override the base # configuration on a per-vhost basis. # 3. root overrides - these options overlay/override the base # configuration and vhost overrides on a per-root basis. # # Here's a diagram of the valid overlays/overrides: # # PER-ROOT PER-VHOST BASE # # ,-----------. ,-----------. # | vhost-*/ | | | # | general | --> | general | # | | | | # `-----------' `-----------' # ,-----------. ,-----------. ,-----------. # | root-*/ | | vhost-*/ | | | # | options | --> | options | --> | options | # | | | | | | # `-----------' `-----------' `-----------' # ,-----------. ,-----------. ,-----------. # | root-*/ | | vhost-*/ | | | # | templates | --> | templates | --> | templates | # | | | | | | # `-----------' `-----------' `-----------' # ,-----------. ,-----------. ,-----------. # | root-*/ | | vhost-*/ | | | # | utilities | --> | utilities | --> | utilities | # | | | | | | # `-----------' `-----------' `-----------' # ,-----------. ,-----------. # | vhost-*/ | | | # | cvsdb | --> | cvsdb | # | | | | # `-----------' `-----------' # ,-----------. ,-----------. ,-----------. # | root-*/ | | vhost-*/ | | | # | authz-* | --> | authz-* | --> | authz-* | # | | | | | | # `-----------' `-----------' `-----------' # ,-----------. # | | # | vhosts | # | | # `-----------' # ,-----------. # | | # | query | # | | # `-----------' # # ### TODO: Figure out what this all means for the 'kv' stuff. # ######################################################################### class Config: _base_sections = ( # Base configuration sections. 'authz-*', 'cvsdb', 'general', 'options', 'query', 'templates', 'utilities', ) _force_multi_value = ( # Configuration values with multiple, comma-separated values. 'allowed_views', 'binary_mime_types', 'custom_log_formatting', 'cvs_roots', 'kv_files', 'languages', 'mime_types_files', 'root_parents', 'svn_roots', ) _allowed_overrides = { # Mapping of override types to allowed overridable sections. 'vhost' : ('authz-*', 'cvsdb', 'general', 'options', 'templates', 'utilities', ), 'root' : ('authz-*', 'options', 'templates', 'utilities', ) } def __init__(self): self.root_options_overlayed = 0 for section in self._base_sections: if section[-1] == '*': continue setattr(self, section, _sub_config()) def load_config(self, pathname, vhost=None): """Load the configuration file at PATHNAME, applying configuration settings there as overrides to the built-in default values. If VHOST is provided, also process the configuration overrides specific to that virtual host.""" self.conf_path = os.path.isfile(pathname) and pathname or None self.base = os.path.dirname(pathname) self.parser = ConfigParser.ConfigParser() self.parser.optionxform = lambda x: x # don't case-normalize option names. self.parser.read(self.conf_path or []) for section in self.parser.sections(): if self._is_allowed_section(section, self._base_sections): self._process_section(self.parser, section, section) if vhost and self.parser.has_section('vhosts'): self._process_vhost(self.parser, vhost) def load_kv_files(self, language): """Process the key/value (kv) files specified in the configuration, merging their values into the configuration as dotted heirarchical items.""" kv = _sub_config() for fname in self.general.kv_files: if fname[0] == '[': idx = string.index(fname, ']') parts = string.split(fname[1:idx], '.') fname = string.strip(fname[idx+1:]) else: parts = [ ] fname = string.replace(fname, '%lang%', language) parser = ConfigParser.ConfigParser() parser.optionxform = lambda x: x # don't case-normalize option names. parser.read(os.path.join(self.base, fname)) for section in parser.sections(): for option in parser.options(section): full_name = parts + [section] ob = kv for name in full_name: try: ob = getattr(ob, name) except AttributeError: c = _sub_config() setattr(ob, name, c) ob = c setattr(ob, option, parser.get(section, option)) return kv def path(self, path): """Return PATH relative to the config file directory.""" return os.path.join(self.base, path) def _process_section(self, parser, section, subcfg_name): if not hasattr(self, subcfg_name): setattr(self, subcfg_name, _sub_config()) sc = getattr(self, subcfg_name) for opt in parser.options(section): value = parser.get(section, opt) if opt in self._force_multi_value: value = map(string.strip, filter(None, string.split(value, ','))) else: try: value = int(value) except ValueError: pass ### FIXME: This feels like unnecessary depth of knowledge for a ### semi-generic configuration object. if opt == 'cvs_roots' or opt == 'svn_roots': value = _parse_roots(opt, value) setattr(sc, opt, value) def _is_allowed_section(self, section, allowed_sections): """Return 1 iff SECTION is an allowed section, defined as being explicitly present in the ALLOWED_SECTIONS list or present in the form 'someprefix-*' in that list.""" for allowed_section in allowed_sections: if allowed_section[-1] == '*': if _startswith(section, allowed_section[:-1]): return 1 elif allowed_section == section: return 1 return 0 def _is_allowed_override(self, sectype, secspec, section): """Test if SECTION is an allowed override section for sections of type SECTYPE ('vhosts' or 'root', currently) and type-specifier SECSPEC (a rootname or vhostname, currently). If it is, return the overridden base section name. If it's not an override section at all, return None. And if it's an override section but not an allowed one, raise IllegalOverrideSection.""" cv = '%s-%s/' % (sectype, secspec) lcv = len(cv) if section[:lcv] != cv: return None base_section = section[lcv:] if self._is_allowed_section(base_section, self._allowed_overrides[sectype]): return base_section raise IllegalOverrideSection(sectype, section) def _process_vhost(self, parser, vhost): # Find a vhost name for this VHOST, if any (else, we've nothing to do). canon_vhost = self._find_canon_vhost(parser, vhost) if not canon_vhost: return # Overlay any option sections associated with this vhost name. for section in parser.sections(): base_section = self._is_allowed_override('vhost', canon_vhost, section) if base_section: self._process_section(parser, section, base_section) def _find_canon_vhost(self, parser, vhost): vhost = string.split(string.lower(vhost), ':')[0] # lower-case, no port for canon_vhost in parser.options('vhosts'): value = parser.get('vhosts', canon_vhost) patterns = map(string.lower, map(string.strip, filter(None, string.split(value, ',')))) for pat in patterns: if fnmatch.fnmatchcase(vhost, pat): return canon_vhost return None def overlay_root_options(self, rootname): """Overlay per-root options for ROOTNAME atop the existing option set. This is a destructive change to the configuration.""" did_overlay = 0 if not self.conf_path: return for section in self.parser.sections(): base_section = self._is_allowed_override('root', rootname, section) if base_section: # We can currently only deal with root overlays happening # once, so check that we've not yet done any overlaying of # per-root options. assert(self.root_options_overlayed == 0) self._process_section(self.parser, section, base_section) did_overlay = 1 # If we actually did any overlaying, remember this fact so we # don't do it again later. if did_overlay: self.root_options_overlayed = 1 def _get_parser_items(self, parser, section): """Basically implement ConfigParser.items() for pre-Python-2.3 versions.""" try: return self.parser.items(section) except AttributeError: d = {} for option in parser.options(section): d[option] = parser.get(section, option) return d.items() def get_authorizer_and_params_hack(self, rootname): """Return a 2-tuple containing the name and parameters of the authorizer configured for use with ROOTNAME. ### FIXME: This whole thing is a hack caused by our not being able ### to non-destructively overlay root options when trying to do ### something like a root listing (which might need to get ### different authorizer bits for each and every root in the list). ### Until we have a good way to do that, we expose this function, ### which assumes that base and per-vhost configuration has been ### absorbed into this object and that per-root options have *not* ### been overlayed. See issue #371.""" # We assume that per-root options have *not* been overlayed. assert(self.root_options_overlayed == 0) if not self.conf_path: return None, {} # Figure out the authorizer by searching first for a per-root # override, then falling back to the base/vhost configuration. authorizer = None root_options_section = 'root-%s/options' % (rootname) if self.parser.has_section(root_options_section) \ and self.parser.has_option(root_options_section, 'authorizer'): authorizer = self.parser.get(root_options_section, 'authorizer') if not authorizer: authorizer = self.options.authorizer # No authorizer? Get outta here. if not authorizer: return None, {} # Dig up the parameters for the authorizer, starting with the # base/vhost items, then overlaying any root-specific ones we find. params = {} authz_section = 'authz-%s' % (authorizer) if hasattr(self, authz_section): sub_config = getattr(self, authz_section) for attr in dir(sub_config): params[attr] = getattr(sub_config, attr) root_authz_section = 'root-%s/authz-%s' % (rootname, authorizer) for section in self.parser.sections(): if section == root_authz_section: for key, value in self._get_parser_items(self.parser, section): params[key] = value return authorizer, params def get_authorizer_params(self, authorizer=None): """Return a dictionary of parameter names and values which belong to the configured authorizer (or AUTHORIZER, if provided).""" params = {} if authorizer is None: authorizer = self.options.authorizer if authorizer: authz_section = 'authz-%s' % (self.options.authorizer) if hasattr(self, authz_section): sub_config = getattr(self, authz_section) for attr in dir(sub_config): params[attr] = getattr(sub_config, attr) return params def set_defaults(self): "Set some default values in the configuration." self.general.cvs_roots = { } self.general.svn_roots = { } self.general.root_parents = [] self.general.default_root = '' self.general.mime_types_files = ["mimetypes.conf"] self.general.address = '' self.general.kv_files = [ ] self.general.languages = ['en-us'] self.utilities.rcs_dir = '' if sys.platform == "win32": self.utilities.cvsnt = 'cvs' else: self.utilities.cvsnt = None self.utilities.svn = '' self.utilities.diff = '' self.utilities.cvsgraph = '' self.options.root_as_url_component = 1 self.options.checkout_magic = 0 self.options.allowed_views = ['annotate', 'diff', 'markup', 'roots'] self.options.authorizer = None self.options.mangle_email_addresses = 0 self.options.custom_log_formatting = [] self.options.default_file_view = "log" self.options.binary_mime_types = [] self.options.http_expiration_time = 600 self.options.generate_etags = 1 self.options.svn_ignore_mimetype = 0 self.options.svn_config_dir = None self.options.max_filesize_kbytes = 512 self.options.use_rcsparse = 0 self.options.sort_by = 'file' self.options.sort_group_dirs = 1 self.options.hide_attic = 1 self.options.hide_errorful_entries = 0 self.options.log_sort = 'date' self.options.diff_format = 'h' self.options.hide_cvsroot = 1 self.options.hr_breakable = 1 self.options.hr_funout = 1 self.options.hr_ignore_white = 0 self.options.hr_ignore_keyword_subst = 1 self.options.hr_intraline = 0 self.options.allow_compress = 0 self.options.template_dir = "templates" self.options.docroot = None self.options.show_subdir_lastmod = 0 self.options.show_roots_lastmod = 0 self.options.show_logs = 1 self.options.show_log_in_markup = 1 self.options.cross_copies = 1 self.options.use_localtime = 0 self.options.iso8601_timestamps = 0 self.options.short_log_len = 80 self.options.enable_syntax_coloration = 1 self.options.tabsize = 8 self.options.detect_encoding = 0 self.options.use_cvsgraph = 0 self.options.cvsgraph_conf = "cvsgraph.conf" self.options.use_re_search = 0 self.options.dir_pagesize = 0 self.options.log_pagesize = 0 self.options.log_pagesextra = 3 self.options.limit_changes = 100 self.templates.diff = None self.templates.directory = None self.templates.error = None self.templates.file = None self.templates.graph = None self.templates.log = None self.templates.query = None self.templates.query_form = None self.templates.query_results = None self.templates.roots = None self.cvsdb.enabled = 0 self.cvsdb.host = '' self.cvsdb.port = 3306 self.cvsdb.database_name = '' self.cvsdb.user = '' self.cvsdb.passwd = '' self.cvsdb.readonly_user = '' self.cvsdb.readonly_passwd = '' self.cvsdb.row_limit = 1000 self.cvsdb.rss_row_limit = 100 self.cvsdb.check_database_for_root = 0 self.query.viewvc_base_url = None def _startswith(somestr, substr): return somestr[:len(substr)] == substr def _parse_roots(config_name, config_value): roots = { } for root in config_value: pos = string.find(root, ':') if pos < 0: raise MalformedRoot(config_name, root) name, path = map(string.strip, (root[:pos], root[pos+1:])) roots[name] = path return roots class ViewVCConfigurationError(Exception): pass class IllegalOverrideSection(ViewVCConfigurationError): def __init__(self, override_type, section_name): self.section_name = section_name self.override_type = override_type def __str__(self): return "malformed configuration: illegal %s override section: %s" \ % (self.override_type, self.section_name) class MalformedRoot(ViewVCConfigurationError): def __init__(self, config_name, value_given): Exception.__init__(self, config_name, value_given) self.config_name = config_name self.value_given = value_given def __str__(self): return "malformed configuration: '%s' uses invalid syntax: %s" \ % (self.config_name, self.value_given) class _sub_config: pass if not hasattr(sys, 'hexversion'): # Python 1.5 or 1.5.1. fix the syntax for ConfigParser options. import regex ConfigParser.option_cre = regex.compile('^\([-A-Za-z0-9._]+\)\(:\|[' + string.whitespace + ']*=\)\(.*\)$') viewvc-1.1.22/lib/vcauth/0000755000175000017500000000000012265242270015344 5ustar cmpilatocmpilatoviewvc-1.1.22/lib/vcauth/forbiddenre/0000755000175000017500000000000012265242270017627 5ustar cmpilatocmpilatoviewvc-1.1.22/lib/vcauth/forbiddenre/__init__.py0000644000175000017500000000403512071623704021742 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 2008-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- import vcauth import vclib import fnmatch import string import re def _split_regexp(restr): """Return a 2-tuple consisting of a compiled regular expression object and a boolean flag indicating if that object should be interpreted inversely.""" if restr[0] == '!': return re.compile(restr[1:]), 1 return re.compile(restr), 0 class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): """A simple regular-expression-based authorizer.""" def __init__(self, username, params={}): forbidden = params.get('forbiddenre', '') self.forbidden = map(lambda x: _split_regexp(string.strip(x)), filter(None, string.split(forbidden, ','))) def _check_root_path_access(self, root_path): default = 1 for forbidden, negated in self.forbidden: if negated: default = 0 if forbidden.search(root_path): return 1 elif forbidden.search(root_path): return 0 return default def check_root_access(self, rootname): return self._check_root_path_access(rootname) def check_universal_access(self, rootname): # If there aren't any forbidden regexps, we can grant universal # read access. Otherwise, we make no claim. if not self.forbidden: return 1 return None def check_path_access(self, rootname, path_parts, pathtype, rev=None): root_path = rootname if path_parts: root_path = root_path + '/' + string.join(path_parts, '/') if pathtype == vclib.DIR: root_path = root_path + '/' else: root_path = root_path + '/' return self._check_root_path_access(root_path) viewvc-1.1.22/lib/vcauth/svnauthz/0000755000175000017500000000000012265242270017226 5ustar cmpilatocmpilatoviewvc-1.1.22/lib/vcauth/svnauthz/__init__.py0000644000175000017500000002310612071623704021341 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 2006-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # (c) 2006 Sergey Lapin import vcauth import string import os.path import debug from ConfigParser import ConfigParser class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): """Subversion authz authorizer module""" def __init__(self, username, params={}): self.rootpaths = { } # {root -> { paths -> access boolean for USERNAME }} # Get the authz file location from a passed-in parameter. self.authz_file = params.get('authzfile') if not self.authz_file: raise debug.ViewVCException("No authzfile configured") if not os.path.exists(self.authz_file): raise debug.ViewVCException("Configured authzfile file not found") # See if the admin wants us to do case normalization of usernames. self.force_username_case = params.get('force_username_case') if self.force_username_case == "upper": self.username = username and string.upper(username) or username elif self.force_username_case == "lower": self.username = username and string.lower(username) or username elif not self.force_username_case: self.username = username else: raise debug.ViewVCException("Invalid value for force_username_case " "option") def _get_paths_for_root(self, rootname): if self.rootpaths.has_key(rootname): return self.rootpaths[rootname] paths_for_root = { } # Parse the authz file, replacing ConfigParser's optionxform() # method with something that won't futz with the case of the # option names. cp = ConfigParser() cp.optionxform = lambda x: x try: cp.read(self.authz_file) except: raise debug.ViewVCException("Unable to parse configured authzfile file") # Figure out if there are any aliases for the current username aliases = [] if cp.has_section('aliases'): for alias in cp.options('aliases'): entry = cp.get('aliases', alias) if entry == self.username: aliases.append(alias) # Figure out which groups USERNAME has a part of. groups = [] if cp.has_section('groups'): all_groups = [] def _process_group(groupname): """Inline function to handle groups within groups. For a group to be within another group in SVN, the group definitions must be in the correct order in the config file. ie. If group A is a member of group B then group A must be defined before group B in the [groups] section. Unfortunately, the ConfigParser class provides no way of finding the order in which groups were defined so, for reasons of practicality, this function lets you get away with them being defined in the wrong order. Recursion is guarded against though.""" # If we already know the user is part of this already- # processed group, return that fact. if groupname in groups: return 1 # Otherwise, ensure we don't process a group twice. if groupname in all_groups: return 0 # Store the group name in a global list so it won't be processed again all_groups.append(groupname) group_member = 0 groupname = groupname.strip() entries = string.split(cp.get('groups', groupname), ',') for entry in entries: entry = string.strip(entry) if entry == self.username: group_member = 1 break elif entry[0:1] == "@" and _process_group(entry[1:]): group_member = 1 break elif entry[0:1] == "&" and entry[1:] in aliases: group_member = 1 break if group_member: groups.append(groupname) return group_member # Process the groups for group in cp.options('groups'): _process_group(group) def _userspec_matches_user(userspec): # If there is an inversion character, recurse and return the # opposite result. if userspec[0:1] == '~': return not _userspec_matches_user(userspec[1:]) # See if the userspec applies to our current user. return userspec == '*' \ or userspec == self.username \ or (self.username is not None and userspec == "$authenticated") \ or (self.username is None and userspec == "$anonymous") \ or (userspec[0:1] == "@" and userspec[1:] in groups) \ or (userspec[0:1] == "&" and userspec[1:] in aliases) def _process_access_section(section): """Inline function for determining user access in a single config secction. Return a two-tuple (ALLOW, DENY) containing the access determination for USERNAME in a given authz file SECTION (if any).""" # Figure if this path is explicitly allowed or denied to USERNAME. allow = deny = 0 for user in cp.options(section): user = string.strip(user) if _userspec_matches_user(user): # See if the 'r' permission is among the ones granted to # USER. If so, we can stop looking. (Entry order is not # relevant -- we'll use the most permissive entry, meaning # one 'allow' is all we need.) allow = string.find(cp.get(section, user), 'r') != -1 deny = not allow if allow: break return allow, deny # Read the other (non-"groups") sections, and figure out in which # repositories USERNAME or his groups have read rights. We'll # first check groups that have no specific repository designation, # then superimpose those that have a repository designation which # matches the one we're asking about. root_sections = [] for section in cp.sections(): # Skip the "groups" section -- we handled that already. if section == 'groups': continue if section == 'aliases': continue # Process root-agnostic access sections; skip (but remember) # root-specific ones that match our root; ignore altogether # root-specific ones that don't match our root. While we're at # it, go ahead and figure out the repository path we're talking # about. if section.find(':') == -1: path = section else: name, path = string.split(section, ':', 1) if name == rootname: root_sections.append(section) continue # Check for a specific access determination. allow, deny = _process_access_section(section) # If we got an explicit access determination for this path and this # USERNAME, record it. if allow or deny: if path != '/': path = '/' + string.join(filter(None, string.split(path, '/')), '/') paths_for_root[path] = allow # Okay. Superimpose those root-specific values now. for section in root_sections: # Get the path again. name, path = string.split(section, ':', 1) # Check for a specific access determination. allow, deny = _process_access_section(section) # If we got an explicit access determination for this path and this # USERNAME, record it. if allow or deny: if path != '/': path = '/' + string.join(filter(None, string.split(path, '/')), '/') paths_for_root[path] = allow # If the root isn't readable, there's no point in caring about all # the specific paths the user can't see. Just point the rootname # to a None paths dictionary. root_is_readable = 0 for path in paths_for_root.keys(): if paths_for_root[path]: root_is_readable = 1 break if not root_is_readable: paths_for_root = None self.rootpaths[rootname] = paths_for_root return paths_for_root def check_root_access(self, rootname): paths = self._get_paths_for_root(rootname) return (paths is not None) and 1 or 0 def check_universal_access(self, rootname): paths = self._get_paths_for_root(rootname) if not paths: # None or empty. return 0 # Search the access determinations. If there's a mix, we can't # claim a universal access determination. found_allow = 0 found_deny = 0 for access in paths.values(): if access: found_allow = 1 else: found_deny = 1 if found_allow and found_deny: return None # We didn't find both allowances and denials, so we must have # found one or the other. Denials only is a universal denial. if found_deny: return 0 # ... but allowances only is only a universal allowance if read # access is granted to the root directory. if found_allow and paths.has_key('/'): return 1 # Anything else is indeterminable. return None def check_path_access(self, rootname, path_parts, pathtype, rev=None): # Crawl upward from the path represented by PATH_PARTS toward to # the root of the repository, looking for an explicitly grant or # denial of access. paths = self._get_paths_for_root(rootname) if paths is None: return 0 parts = path_parts[:] while parts: path = '/' + string.join(parts, '/') if paths.has_key(path): return paths[path] del parts[-1] return paths.get('/', 0) viewvc-1.1.22/lib/vcauth/__init__.py0000644000175000017500000000402312071623704017454 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 2006-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- """Generic API for implementing authorization checks employed by ViewVC.""" import string import vclib class GenericViewVCAuthorizer: """Abstract class encapsulating version control authorization routines.""" def __init__(self, username=None, params={}): """Create a GenericViewVCAuthorizer object which will be used to validate that USERNAME has the permissions needed to view version control repositories (in whole or in part). PARAMS is a dictionary of custom parameters for the authorizer.""" pass def check_root_access(self, rootname): """Return 1 iff the associated username is permitted to read ROOTNAME.""" pass def check_universal_access(self, rootname): """Return 1 if the associated username is permitted to read every path in the repository at every revision, 0 if the associated username is prohibited from reading any path in the repository, or None if no such determination can be made (perhaps because the cost of making it is too great).""" pass def check_path_access(self, rootname, path_parts, pathtype, rev=None): """Return 1 iff the associated username is permitted to read revision REV of the path PATH_PARTS (of type PATHTYPE) in repository ROOTNAME.""" pass ############################################################################## class ViewVCAuthorizer(GenericViewVCAuthorizer): """The uber-permissive authorizer.""" def check_root_access(self, rootname): return 1 def check_universal_access(self, rootname): return 1 def check_path_access(self, rootname, path_parts, pathtype, rev=None): return 1 viewvc-1.1.22/lib/vcauth/forbidden/0000755000175000017500000000000012265242270017300 5ustar cmpilatocmpilatoviewvc-1.1.22/lib/vcauth/forbidden/__init__.py0000644000175000017500000000310212071623704021405 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 2006-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- import vcauth import vclib import fnmatch import string class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): """A simple top-level module authorizer.""" def __init__(self, username, params={}): forbidden = params.get('forbidden', '') self.forbidden = map(string.strip, filter(None, string.split(forbidden, ','))) def check_root_access(self, rootname): return 1 def check_universal_access(self, rootname): # If there aren't any forbidden paths, we can grant universal read # access. Otherwise, we make no claim. if not self.forbidden: return 1 return None def check_path_access(self, rootname, path_parts, pathtype, rev=None): # No path? No problem. if not path_parts: return 1 # Not a directory? We aren't interested. if pathtype != vclib.DIR: return 1 # At this point we're looking at a directory path. module = path_parts[0] default = 1 for pat in self.forbidden: if pat[0] == '!': default = 0 if fnmatch.fnmatchcase(module, pat[1:]): return 1 elif fnmatch.fnmatchcase(module, pat): return 0 return default viewvc-1.1.22/lib/py2html.py0000755000175000017500000004104310405347157016034 0ustar cmpilatocmpilato#!/usr/bin/python -u """ Python Highlighter Version: 0.8 py2html.py [options] files... options: -h print help - read from stdin, write to stdout -stdout read from files, write to stdout -files read from files, write to filename+'.html' (default) -format: html output XHTML page (default) rawhtml output pure XHTML (without headers, titles, etc.) -mode: color output in color (default) mono output b/w (for printing) -title:Title use 'Title' as title of the generated page -bgcolor:color use color as background-color for page -header:file use contents of file as header -footer:file use contents of file as footer -URL replace all occurances of 'URL: link' with 'link'; this is always enabled in CGI mode -v verbose Takes the input, assuming it is Python code and formats it into colored XHTML. When called without parameters the script tries to work in CGI mode. It looks for a field 'script=URL' and tries to use that URL as input file. If it can't find this field, the path info (the part of the URL following the CGI script name) is tried. In case no host is given, the host where the CGI script lives and HTTP are used. * Uses Just van Rossum's PyFontify version 0.3 to tag Python scripts. You can get it via his homepage on starship: URL: http://starship.python.net/crew/just """ __comments__ = """ The following snippet is a small shell script I use for viewing Python scripts via less on Unix: pyless: #!/bin/sh # Browse pretty printed Python code using ANSI codes for highlighting py2html -stdout -format:ansi -mode:color $* | less -r History: 0.8: Added patch by Patrick Lynch to have py2html.py use style sheets for markup 0.7: Added patch by Ville Skyttä to make py2html.py output valid XHTML. 0.6: Fixed a bug in .escape_html(); thanks to Vespe Savikko for finding this one. 0.5: Added a few suggestions by Kevin Ng to make the CGI version a little more robust. """ __copyright__ = """\ Copyright (c) 1998-2000, Marc-Andre Lemburg; mailto:mal@lemburg.com Copyright (c) 2000-2002, eGenix.com Software GmbH; mailto:info@egenix.com Distributed under the terms and conditions of the eGenix.com Public License. See http://www.egenix.com/files/python/mxLicense.html for details, or contact the author. All Rights Reserved.\ """ __version__ = '0.8' __cgifooter__ = ('\n

# code highlighted using py2html.py '
                 'version %s
\n' % __version__) import sys,string,re # Adjust path so that PyFontify is found... sys.path.append('.') ### Constants # URL of the input form the user is redirected to in case no script=xxx # form field is given. The URL *must* be absolute. Leave blank to # have the script issue an error instead. INPUT_FORM = 'http://www.lemburg.com/files/python/SoftwareDescriptions.html#py2html.py' # HTML DOCTYPE and XML namespace HTML_DOCTYPE = '' HTML_XMLNS = ' xmlns="http://www.w3.org/1999/xhtml"' ### Helpers def fileio(file, mode='rb', data=None, close=0): if type(file) == type(''): f = open(file,mode) close = 1 else: f = file if data: f.write(data) else: data = f.read() if close: f.close() return data ### Converter class class PrettyPrint: """ generic Pretty Printer class * supports tagging Python scripts in the following ways: # format/mode | color mono # -------------------------- # rawhtml | x x (HTML without headers, etc.) # html | x x (a HTML page with HEAD&BODY:) # ansi | x x (with Ansi-escape sequences) * interfaces: file_filter -- takes two files: input & output (may be stdin/stdout) filter -- takes a string and returns the highlighted version * to create an instance use: c = PrettyPrint(tagfct,format,mode) where format and mode must be strings according to the above table if you plan to use PyFontify.fontify as tagfct * the tagfct has to take one argument, text, and return a taglist (format: [(id,left,right,sublist),...], where id is the "name" given to the slice left:right in text and sublist is a taglist for tags inside the slice or None) """ # misc settings title = '' bgcolor = '#FFFFFF' css = '' header = '' footer = '' replace_URLs = 0 # formats to be used formats = {} def __init__(self,tagfct=None,format='html',mode='color'): self.tag = tagfct self.set_mode = getattr(self,'set_mode_%s_%s' % (format, mode)) self.filter = getattr(self,'filter_%s' % format) def file_filter(self,infile,outfile): self.set_mode() text = fileio(infile,'r') if type(infile) == type('') and self.title == '': self.title = infile fileio(outfile,'w',self.filter(text)) ### Set pre- and postfixes for formats & modes # # These methods must set self.formats to a dictionary having # an entry for every tag returned by the tagging function. # # The format used is simple: # tag:(prefix,postfix) # where prefix and postfix are either strings or callable objects, # that return a string (they are called with the matching tag text # as only parameter). prefix is inserted in front of the tag, postfix # is inserted right after the tag. def set_mode_html_color(self): self.css = """ """ % self.bgcolor self.formats = { 'all':('
','
'), 'comment':('',''), 'keyword':('',''), 'parameter':('',''), 'identifier':( lambda x,strip=string.strip: '' % (strip(x)), ''), 'string':('','') } set_mode_rawhtml_color = set_mode_html_color def set_mode_html_mono(self): self.css = """ """ % self.bgcolor self.formats = { 'all':('
','
'), 'comment':('',''), 'keyword':( '',''), 'parameter':('',''), 'identifier':( lambda x,strip=string.strip: '' % (strip(x)), ''), 'string':('','') } set_mode_rawhtml_mono = set_mode_html_mono def set_mode_ansi_mono(self): self.formats = { 'all':('',''), 'comment':('\033[2m','\033[m'), 'keyword':('\033[4m','\033[m'), 'parameter':('',''), 'identifier':('\033[1m','\033[m'), 'string':('','') } def set_mode_ansi_color(self): self.formats = { 'all':('',''), 'comment':('\033[34;2m','\033[m'), 'keyword':('\033[1;34m','\033[m'), 'parameter':('',''), 'identifier':('\033[1;31m','\033[m'), 'string':('\033[32;2m','\033[m') } ### Filters for Python scripts given as string def escape_html(self,text): t = (('&','&'),('<','<'),('>','>')) for x,y in t: text = string.join(string.split(text,x),y) return text def filter_html(self,text): output = self.fontify(self.escape_html(text)) if self.replace_URLs: output = re.sub('URL:([ \t]+)([^ \n\r<]+)', 'URL:\\1\\2',output) html = """%s %s %s %s %s %s \n"""%(HTML_DOCTYPE, HTML_XMLNS, self.title, self.css, self.header, output, self.footer) return html def filter_rawhtml(self,text): output = self.fontify(self.escape_html(text)) if self.replace_URLs: output = re.sub('URL:([ \t]+)([^ \n\r<]+)', 'URL:\\1\\2',output) return self.header + output + self.footer def filter_ansi(self,text): output = self.fontify(text) return self.header + output + self.footer ### Fontify engine def fontify(self,pytext): # parse taglist = self.tag(pytext) # prepend special 'all' tag: taglist[:0] = [('all',0,len(pytext),None)] # prepare splitting splits = [] addsplits(splits,pytext,self.formats,taglist) # do splitting & inserting splits.sort() l = [] li = 0 for ri,dummy,insert in splits: if ri > li: l.append(pytext[li:ri]) l.append(insert) li = ri if li < len(pytext): l.append(pytext[li:]) return string.join(l,'') def addsplits(splits,text,formats,taglist): """ Helper for .fontify() """ for id,left,right,sublist in taglist: try: pre,post = formats[id] except KeyError: # sys.stderr.write('Warning: no format for %s specified\n'%repr(id)) pre,post = '','' if type(pre) != type(''): pre = pre(text[left:right]) if type(post) != type(''): post = post(text[left:right]) # len(splits) is a dummy used to make sorting stable splits.append((left,len(splits),pre)) if sublist: addsplits(splits,text,formats,sublist) splits.append((right,len(splits),post)) def write_html_error(titel,text): print """\ %s%s

%s

%s """ % (HTML_DOCTYPE,HTML_XMLNS,titel,titel,text) def redirect_to(url): sys.stdout.write('Content-Type: text/html\r\n') sys.stdout.write('Status: 302\r\n') sys.stdout.write('Location: %s\r\n\r\n' % url) print """ %s 302 Moved Temporarily

302 Moved Temporarily

The document has moved to %s.

""" % (HTML_DOCTYPE,HTML_XMLNS,url,url) def main(cmdline): """ main(cmdline) -- process cmdline as if it were sys.argv """ # parse options/files options = [] optvalues = {} for o in cmdline[1:]: if o[0] == '-': if ':' in o: k,v = tuple(string.split(o,':')) optvalues[k] = v options.append(k) else: options.append(o) else: break files = cmdline[len(options)+1:] ### create converting object # load fontifier if '-marcs' in options: # use mxTextTool's tagging engine as fontifier from mx.TextTools import tag from mx.TextTools.Examples.Python import python_script tagfct = lambda text,tag=tag,pytable=python_script: \ tag(text,pytable)[1] print "Py2HTML: using Marc's tagging engine" else: # load Just's fontifier try: import PyFontify if PyFontify.__version__ < '0.3': raise ValueError tagfct = PyFontify.fontify except: print """ Sorry, but this script needs the PyFontify.py module version 0.3; You can download it from Just's homepage at URL: http://starship.python.net/crew/just """ sys.exit() if '-format' in options: format = optvalues['-format'] else: # use default format = 'html' if '-mode' in options: mode = optvalues['-mode'] else: # use default mode = 'color' c = PrettyPrint(tagfct,format,mode) convert = c.file_filter ### start working if '-title' in options: c.title = optvalues['-title'] if '-bgcolor' in options: c.bgcolor = optvalues['-bgcolor'] if '-header' in options: try: f = open(optvalues['-header']) c.header = f.read() f.close() except IOError: if verbose: print 'IOError: header file not found' if '-footer' in options: try: f = open(optvalues['-footer']) c.footer = f.read() f.close() except IOError: if verbose: print 'IOError: footer file not found' if '-URL' in options: c.replace_URLs = 1 if '-' in options: convert(sys.stdin,sys.stdout) sys.exit() if '-h' in options: print __doc__ sys.exit() if len(files) == 0: # Turn URL processing on c.replace_URLs = 1 # Try CGI processing... import cgi,urllib,urlparse,os form = cgi.FieldStorage() if not form.has_key('script'): # Ok, then try pathinfo if not os.environ.has_key('PATH_INFO'): if INPUT_FORM: redirect_to(INPUT_FORM) else: sys.stdout.write('Content-Type: text/html\r\n\r\n') write_html_error('Missing Parameter', 'Missing script=URL field in request') sys.exit(1) url = os.environ['PATH_INFO'][1:] # skip the leading slash else: url = form['script'].value sys.stdout.write('Content-Type: text/html\r\n\r\n') scheme, host, path, params, query, frag = urlparse.urlparse(url) if not host: scheme = 'http' if os.environ.has_key('HTTP_HOST'): host = os.environ['HTTP_HOST'] else: host = 'localhost' url = urlparse.urlunparse((scheme, host, path, params, query, frag)) #print url; sys.exit() network = urllib.URLopener() try: tempfile,headers = network.retrieve(url) except IOError,reason: write_html_error('Error opening "%s"' % url, 'The given URL could not be opened. Reason: %s' %\ str(reason)) sys.exit(1) f = open(tempfile,'rb') c.title = url c.footer = __cgifooter__ convert(f,sys.stdout) f.close() network.close() sys.exit() if '-stdout' in options: filebreak = '-'*72 for f in files: try: if len(files) > 1: print filebreak print 'File:',f print filebreak convert(f,sys.stdout) except IOError: pass else: verbose = ('-v' in options) if verbose: print 'Py2HTML: working on', for f in files: try: if verbose: print f, convert(f,f+'.html') except IOError: if verbose: print '(IOError!)', if verbose: print print 'Done.' if __name__=='__main__': main(sys.argv) viewvc-1.1.22/lib/compat_difflib.py0000755000175000017500000007136510370563455017413 0ustar cmpilatocmpilato#! /usr/bin/env python # Backported to Python 1.5.2 for the ViewCVS project by pf@artcom-gmbh.de # 24-Dec-2001, original version "stolen" from Python-2.1.1 """ Module difflib -- helpers for computing deltas between objects. Function get_close_matches(word, possibilities, n=3, cutoff=0.6): Use SequenceMatcher to return list of the best "good enough" matches. word is a sequence for which close matches are desired (typically a string). possibilities is a list of sequences against which to match word (typically a list of strings). Optional arg n (default 3) is the maximum number of close matches to return. n must be > 0. Optional arg cutoff (default 0.6) is a float in [0, 1]. Possibilities that don't score at least that similar to word are ignored. The best (no more than n) matches among the possibilities are returned in a list, sorted by similarity score, most similar first. >>> get_close_matches("appel", ["ape", "apple", "peach", "puppy"]) ['apple', 'ape'] >>> import keyword >>> get_close_matches("wheel", keyword.kwlist) ['while'] >>> get_close_matches("apple", keyword.kwlist) [] >>> get_close_matches("accept", keyword.kwlist) ['except'] Class SequenceMatcher SequenceMatcher is a flexible class for comparing pairs of sequences of any type, so long as the sequence elements are hashable. The basic algorithm predates, and is a little fancier than, an algorithm published in the late 1980's by Ratcliff and Obershelp under the hyperbolic name "gestalt pattern matching". The basic idea is to find the longest contiguous matching subsequence that contains no "junk" elements (R-O doesn't address junk). The same idea is then applied recursively to the pieces of the sequences to the left and to the right of the matching subsequence. This does not yield minimal edit sequences, but does tend to yield matches that "look right" to people. Example, comparing two strings, and considering blanks to be "junk": >>> s = SequenceMatcher(lambda x: x == " ", ... "private Thread currentThread;", ... "private volatile Thread currentThread;") >>> .ratio() returns a float in [0, 1], measuring the "similarity" of the sequences. As a rule of thumb, a .ratio() value over 0.6 means the sequences are close matches: >>> print round(s.ratio(), 3) 0.866 >>> If you're only interested in where the sequences match, .get_matching_blocks() is handy: >>> for block in s.get_matching_blocks(): ... print "a[%d] and b[%d] match for %d elements" % block a[0] and b[0] match for 8 elements a[8] and b[17] match for 6 elements a[14] and b[23] match for 15 elements a[29] and b[38] match for 0 elements Note that the last tuple returned by .get_matching_blocks() is always a dummy, (len(a), len(b), 0), and this is the only case in which the last tuple element (number of elements matched) is 0. If you want to know how to change the first sequence into the second, use .get_opcodes(): >>> for opcode in s.get_opcodes(): ... print "%6s a[%d:%d] b[%d:%d]" % opcode equal a[0:8] b[0:8] insert a[8:8] b[8:17] equal a[8:14] b[17:23] equal a[14:29] b[23:38] See Tools/scripts/ndiff.py for a fancy human-friendly file differencer, which uses SequenceMatcher both to view files as sequences of lines, and lines as sequences of characters. See also function get_close_matches() in this module, which shows how simple code building on SequenceMatcher can be used to do useful work. Timing: Basic R-O is cubic time worst case and quadratic time expected case. SequenceMatcher is quadratic time for the worst case and has expected-case behavior dependent in a complicated way on how many elements the sequences have in common; best case time is linear. SequenceMatcher methods: __init__(isjunk=None, a='', b='') Construct a SequenceMatcher. Optional arg isjunk is None (the default), or a one-argument function that takes a sequence element and returns true iff the element is junk. None is equivalent to passing "lambda x: 0", i.e. no elements are considered to be junk. For example, pass lambda x: x in " \\t" if you're comparing lines as sequences of characters, and don't want to synch up on blanks or hard tabs. Optional arg a is the first of two sequences to be compared. By default, an empty string. The elements of a must be hashable. Optional arg b is the second of two sequences to be compared. By default, an empty string. The elements of b must be hashable. set_seqs(a, b) Set the two sequences to be compared. >>> s = SequenceMatcher() >>> s.set_seqs("abcd", "bcde") >>> s.ratio() 0.75 set_seq1(a) Set the first sequence to be compared. The second sequence to be compared is not changed. >>> s = SequenceMatcher(None, "abcd", "bcde") >>> s.ratio() 0.75 >>> s.set_seq1("bcde") >>> s.ratio() 1.0 >>> SequenceMatcher computes and caches detailed information about the second sequence, so if you want to compare one sequence S against many sequences, use .set_seq2(S) once and call .set_seq1(x) repeatedly for each of the other sequences. See also set_seqs() and set_seq2(). set_seq2(b) Set the second sequence to be compared. The first sequence to be compared is not changed. >>> s = SequenceMatcher(None, "abcd", "bcde") >>> s.ratio() 0.75 >>> s.set_seq2("abcd") >>> s.ratio() 1.0 >>> SequenceMatcher computes and caches detailed information about the second sequence, so if you want to compare one sequence S against many sequences, use .set_seq2(S) once and call .set_seq1(x) repeatedly for each of the other sequences. See also set_seqs() and set_seq1(). find_longest_match(alo, ahi, blo, bhi) Find longest matching block in a[alo:ahi] and b[blo:bhi]. If isjunk is not defined: Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where alo <= i <= i+k <= ahi blo <= j <= j+k <= bhi and for all (i',j',k') meeting those conditions, k >= k' i <= i' and if i == i', j <= j' In other words, of all maximal matching blocks, return one that starts earliest in a, and of all those maximal matching blocks that start earliest in a, return the one that starts earliest in b. >>> s = SequenceMatcher(None, " abcd", "abcd abcd") >>> s.find_longest_match(0, 5, 0, 9) (0, 4, 5) If isjunk is defined, first the longest matching block is determined as above, but with the additional restriction that no junk element appears in the block. Then that block is extended as far as possible by matching (only) junk elements on both sides. So the resulting block never matches on junk except as identical junk happens to be adjacent to an "interesting" match. Here's the same example as before, but considering blanks to be junk. That prevents " abcd" from matching the " abcd" at the tail end of the second sequence directly. Instead only the "abcd" can match, and matches the leftmost "abcd" in the second sequence: >>> s = SequenceMatcher(lambda x: x==" ", " abcd", "abcd abcd") >>> s.find_longest_match(0, 5, 0, 9) (1, 0, 4) If no blocks match, return (alo, blo, 0). >>> s = SequenceMatcher(None, "ab", "c") >>> s.find_longest_match(0, 2, 0, 1) (0, 0, 0) get_matching_blocks() Return list of triples describing matching subsequences. Each triple is of the form (i, j, n), and means that a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in i and in j. The last triple is a dummy, (len(a), len(b), 0), and is the only triple with n==0. >>> s = SequenceMatcher(None, "abxcd", "abcd") >>> s.get_matching_blocks() [(0, 0, 2), (3, 2, 2), (5, 4, 0)] get_opcodes() Return list of 5-tuples describing how to turn a into b. Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the tuple preceding it, and likewise for j1 == the previous j2. The tags are strings, with these meanings: 'replace': a[i1:i2] should be replaced by b[j1:j2] 'delete': a[i1:i2] should be deleted. Note that j1==j2 in this case. 'insert': b[j1:j2] should be inserted at a[i1:i1]. Note that i1==i2 in this case. 'equal': a[i1:i2] == b[j1:j2] >>> a = "qabxcd" >>> b = "abycdf" >>> s = SequenceMatcher(None, a, b) >>> for tag, i1, i2, j1, j2 in s.get_opcodes(): ... print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % ... (tag, i1, i2, a[i1:i2], j1, j2, b[j1:j2])) delete a[0:1] (q) b[0:0] () equal a[1:3] (ab) b[0:2] (ab) replace a[3:4] (x) b[2:3] (y) equal a[4:6] (cd) b[3:5] (cd) insert a[6:6] () b[5:6] (f) ratio() Return a measure of the sequences' similarity (float in [0,1]). Where T is the total number of elements in both sequences, and M is the number of matches, this is 2,0*M / T. Note that this is 1 if the sequences are identical, and 0 if they have nothing in common. .ratio() is expensive to compute if you haven't already computed .get_matching_blocks() or .get_opcodes(), in which case you may want to try .quick_ratio() or .real_quick_ratio() first to get an upper bound. >>> s = SequenceMatcher(None, "abcd", "bcde") >>> s.ratio() 0.75 >>> s.quick_ratio() 0.75 >>> s.real_quick_ratio() 1.0 quick_ratio() Return an upper bound on .ratio() relatively quickly. This isn't defined beyond that it is an upper bound on .ratio(), and is faster to compute. real_quick_ratio(): Return an upper bound on ratio() very quickly. This isn't defined beyond that it is an upper bound on .ratio(), and is faster to compute than either .ratio() or .quick_ratio(). """ TRACE = 0 class SequenceMatcher: def __init__(self, isjunk=None, a='', b=''): """Construct a SequenceMatcher. Optional arg isjunk is None (the default), or a one-argument function that takes a sequence element and returns true iff the element is junk. None is equivalent to passing "lambda x: 0", i.e. no elements are considered to be junk. For example, pass lambda x: x in " \\t" if you're comparing lines as sequences of characters, and don't want to synch up on blanks or hard tabs. Optional arg a is the first of two sequences to be compared. By default, an empty string. The elements of a must be hashable. See also .set_seqs() and .set_seq1(). Optional arg b is the second of two sequences to be compared. By default, an empty string. The elements of b must be hashable. See also .set_seqs() and .set_seq2(). """ # Members: # a # first sequence # b # second sequence; differences are computed as "what do # we need to do to 'a' to change it into 'b'?" # b2j # for x in b, b2j[x] is a list of the indices (into b) # at which x appears; junk elements do not appear # b2jhas # b2j.has_key # fullbcount # for x in b, fullbcount[x] == the number of times x # appears in b; only materialized if really needed (used # only for computing quick_ratio()) # matching_blocks # a list of (i, j, k) triples, where a[i:i+k] == b[j:j+k]; # ascending & non-overlapping in i and in j; terminated by # a dummy (len(a), len(b), 0) sentinel # opcodes # a list of (tag, i1, i2, j1, j2) tuples, where tag is # one of # 'replace' a[i1:i2] should be replaced by b[j1:j2] # 'delete' a[i1:i2] should be deleted # 'insert' b[j1:j2] should be inserted # 'equal' a[i1:i2] == b[j1:j2] # isjunk # a user-supplied function taking a sequence element and # returning true iff the element is "junk" -- this has # subtle but helpful effects on the algorithm, which I'll # get around to writing up someday <0.9 wink>. # DON'T USE! Only __chain_b uses this. Use isbjunk. # isbjunk # for x in b, isbjunk(x) == isjunk(x) but much faster; # it's really the has_key method of a hidden dict. # DOES NOT WORK for x in a! self.isjunk = isjunk self.a = self.b = None self.set_seqs(a, b) def set_seqs(self, a, b): """Set the two sequences to be compared. >>> s = SequenceMatcher() >>> s.set_seqs("abcd", "bcde") >>> s.ratio() 0.75 """ self.set_seq1(a) self.set_seq2(b) def set_seq1(self, a): """Set the first sequence to be compared. The second sequence to be compared is not changed. >>> s = SequenceMatcher(None, "abcd", "bcde") >>> s.ratio() 0.75 >>> s.set_seq1("bcde") >>> s.ratio() 1.0 >>> SequenceMatcher computes and caches detailed information about the second sequence, so if you want to compare one sequence S against many sequences, use .set_seq2(S) once and call .set_seq1(x) repeatedly for each of the other sequences. See also set_seqs() and set_seq2(). """ if a is self.a: return self.a = a self.matching_blocks = self.opcodes = None def set_seq2(self, b): """Set the second sequence to be compared. The first sequence to be compared is not changed. >>> s = SequenceMatcher(None, "abcd", "bcde") >>> s.ratio() 0.75 >>> s.set_seq2("abcd") >>> s.ratio() 1.0 >>> SequenceMatcher computes and caches detailed information about the second sequence, so if you want to compare one sequence S against many sequences, use .set_seq2(S) once and call .set_seq1(x) repeatedly for each of the other sequences. See also set_seqs() and set_seq1(). """ if b is self.b: return self.b = b self.matching_blocks = self.opcodes = None self.fullbcount = None self.__chain_b() # For each element x in b, set b2j[x] to a list of the indices in # b where x appears; the indices are in increasing order; note that # the number of times x appears in b is len(b2j[x]) ... # when self.isjunk is defined, junk elements don't show up in this # map at all, which stops the central find_longest_match method # from starting any matching block at a junk element ... # also creates the fast isbjunk function ... # note that this is only called when b changes; so for cross-product # kinds of matches, it's best to call set_seq2 once, then set_seq1 # repeatedly def __chain_b(self): # Because isjunk is a user-defined (not C) function, and we test # for junk a LOT, it's important to minimize the number of calls. # Before the tricks described here, __chain_b was by far the most # time-consuming routine in the whole module! If anyone sees # Jim Roskind, thank him again for profile.py -- I never would # have guessed that. # The first trick is to build b2j ignoring the possibility # of junk. I.e., we don't call isjunk at all yet. Throwing # out the junk later is much cheaper than building b2j "right" # from the start. b = self.b self.b2j = b2j = {} self.b2jhas = b2jhas = b2j.has_key for i in xrange(len(b)): elt = b[i] if b2jhas(elt): b2j[elt].append(i) else: b2j[elt] = [i] # Now b2j.keys() contains elements uniquely, and especially when # the sequence is a string, that's usually a good deal smaller # than len(string). The difference is the number of isjunk calls # saved. isjunk, junkdict = self.isjunk, {} if isjunk: for elt in b2j.keys(): if isjunk(elt): junkdict[elt] = 1 # value irrelevant; it's a set del b2j[elt] # Now for x in b, isjunk(x) == junkdict.has_key(x), but the # latter is much faster. Note too that while there may be a # lot of junk in the sequence, the number of *unique* junk # elements is probably small. So the memory burden of keeping # this dict alive is likely trivial compared to the size of b2j. self.isbjunk = junkdict.has_key def find_longest_match(self, alo, ahi, blo, bhi): """Find longest matching block in a[alo:ahi] and b[blo:bhi]. If isjunk is not defined: Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where alo <= i <= i+k <= ahi blo <= j <= j+k <= bhi and for all (i',j',k') meeting those conditions, k >= k' i <= i' and if i == i', j <= j' In other words, of all maximal matching blocks, return one that starts earliest in a, and of all those maximal matching blocks that start earliest in a, return the one that starts earliest in b. >>> s = SequenceMatcher(None, " abcd", "abcd abcd") >>> s.find_longest_match(0, 5, 0, 9) (0, 4, 5) If isjunk is defined, first the longest matching block is determined as above, but with the additional restriction that no junk element appears in the block. Then that block is extended as far as possible by matching (only) junk elements on both sides. So the resulting block never matches on junk except as identical junk happens to be adjacent to an "interesting" match. Here's the same example as before, but considering blanks to be junk. That prevents " abcd" from matching the " abcd" at the tail end of the second sequence directly. Instead only the "abcd" can match, and matches the leftmost "abcd" in the second sequence: >>> s = SequenceMatcher(lambda x: x==" ", " abcd", "abcd abcd") >>> s.find_longest_match(0, 5, 0, 9) (1, 0, 4) If no blocks match, return (alo, blo, 0). >>> s = SequenceMatcher(None, "ab", "c") >>> s.find_longest_match(0, 2, 0, 1) (0, 0, 0) """ # CAUTION: stripping common prefix or suffix would be incorrect. # E.g., # ab # acab # Longest matching block is "ab", but if common prefix is # stripped, it's "a" (tied with "b"). UNIX(tm) diff does so # strip, so ends up claiming that ab is changed to acab by # inserting "ca" in the middle. That's minimal but unintuitive: # "it's obvious" that someone inserted "ac" at the front. # Windiff ends up at the same place as diff, but by pairing up # the unique 'b's and then matching the first two 'a's. a, b, b2j, isbjunk = self.a, self.b, self.b2j, self.isbjunk besti, bestj, bestsize = alo, blo, 0 # find longest junk-free match # during an iteration of the loop, j2len[j] = length of longest # junk-free match ending with a[i-1] and b[j] j2len = {} nothing = [] for i in xrange(alo, ahi): # look at all instances of a[i] in b; note that because # b2j has no junk keys, the loop is skipped if a[i] is junk j2lenget = j2len.get newj2len = {} for j in b2j.get(a[i], nothing): # a[i] matches b[j] if j < blo: continue if j >= bhi: break k = newj2len[j] = j2lenget(j-1, 0) + 1 if k > bestsize: besti, bestj, bestsize = i-k+1, j-k+1, k j2len = newj2len # Now that we have a wholly interesting match (albeit possibly # empty!), we may as well suck up the matching junk on each # side of it too. Can't think of a good reason not to, and it # saves post-processing the (possibly considerable) expense of # figuring out what to do with it. In the case of an empty # interesting match, this is clearly the right thing to do, # because no other kind of match is possible in the regions. while besti > alo and bestj > blo and \ isbjunk(b[bestj-1]) and \ a[besti-1] == b[bestj-1]: besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 while besti+bestsize < ahi and bestj+bestsize < bhi and \ isbjunk(b[bestj+bestsize]) and \ a[besti+bestsize] == b[bestj+bestsize]: bestsize = bestsize + 1 if TRACE: print "get_matching_blocks", alo, ahi, blo, bhi print " returns", besti, bestj, bestsize return besti, bestj, bestsize def get_matching_blocks(self): """Return list of triples describing matching subsequences. Each triple is of the form (i, j, n), and means that a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in i and in j. The last triple is a dummy, (len(a), len(b), 0), and is the only triple with n==0. >>> s = SequenceMatcher(None, "abxcd", "abcd") >>> s.get_matching_blocks() [(0, 0, 2), (3, 2, 2), (5, 4, 0)] """ if self.matching_blocks is not None: return self.matching_blocks self.matching_blocks = [] la, lb = len(self.a), len(self.b) self.__helper(0, la, 0, lb, self.matching_blocks) self.matching_blocks.append( (la, lb, 0) ) if TRACE: print '*** matching blocks', self.matching_blocks return self.matching_blocks # builds list of matching blocks covering a[alo:ahi] and # b[blo:bhi], appending them in increasing order to answer def __helper(self, alo, ahi, blo, bhi, answer): i, j, k = x = self.find_longest_match(alo, ahi, blo, bhi) # a[alo:i] vs b[blo:j] unknown # a[i:i+k] same as b[j:j+k] # a[i+k:ahi] vs b[j+k:bhi] unknown if k: if alo < i and blo < j: self.__helper(alo, i, blo, j, answer) answer.append(x) if i+k < ahi and j+k < bhi: self.__helper(i+k, ahi, j+k, bhi, answer) def get_opcodes(self): """Return list of 5-tuples describing how to turn a into b. Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the tuple preceding it, and likewise for j1 == the previous j2. The tags are strings, with these meanings: 'replace': a[i1:i2] should be replaced by b[j1:j2] 'delete': a[i1:i2] should be deleted. Note that j1==j2 in this case. 'insert': b[j1:j2] should be inserted at a[i1:i1]. Note that i1==i2 in this case. 'equal': a[i1:i2] == b[j1:j2] >>> a = "qabxcd" >>> b = "abycdf" >>> s = SequenceMatcher(None, a, b) >>> for tag, i1, i2, j1, j2 in s.get_opcodes(): ... print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % ... (tag, i1, i2, a[i1:i2], j1, j2, b[j1:j2])) delete a[0:1] (q) b[0:0] () equal a[1:3] (ab) b[0:2] (ab) replace a[3:4] (x) b[2:3] (y) equal a[4:6] (cd) b[3:5] (cd) insert a[6:6] () b[5:6] (f) """ if self.opcodes is not None: return self.opcodes i = j = 0 self.opcodes = answer = [] for ai, bj, size in self.get_matching_blocks(): # invariant: we've pumped out correct diffs to change # a[:i] into b[:j], and the next matching block is # a[ai:ai+size] == b[bj:bj+size]. So we need to pump # out a diff to change a[i:ai] into b[j:bj], pump out # the matching block, and move (i,j) beyond the match tag = '' if i < ai and j < bj: tag = 'replace' elif i < ai: tag = 'delete' elif j < bj: tag = 'insert' if tag: answer.append( (tag, i, ai, j, bj) ) i, j = ai+size, bj+size # the list of matching blocks is terminated by a # sentinel with size 0 if size: answer.append( ('equal', ai, i, bj, j) ) return answer def ratio(self): """Return a measure of the sequences' similarity (float in [0,1]). Where T is the total number of elements in both sequences, and M is the number of matches, this is 2,0*M / T. Note that this is 1 if the sequences are identical, and 0 if they have nothing in common. .ratio() is expensive to compute if you haven't already computed .get_matching_blocks() or .get_opcodes(), in which case you may want to try .quick_ratio() or .real_quick_ratio() first to get an upper bound. >>> s = SequenceMatcher(None, "abcd", "bcde") >>> s.ratio() 0.75 >>> s.quick_ratio() 0.75 >>> s.real_quick_ratio() 1.0 """ matches = reduce(lambda sum, triple: sum + triple[-1], self.get_matching_blocks(), 0) return 2.0 * matches / (len(self.a) + len(self.b)) def quick_ratio(self): """Return an upper bound on ratio() relatively quickly. This isn't defined beyond that it is an upper bound on .ratio(), and is faster to compute. """ # viewing a and b as multisets, set matches to the cardinality # of their intersection; this counts the number of matches # without regard to order, so is clearly an upper bound if self.fullbcount is None: self.fullbcount = fullbcount = {} for elt in self.b: fullbcount[elt] = fullbcount.get(elt, 0) + 1 fullbcount = self.fullbcount # avail[x] is the number of times x appears in 'b' less the # number of times we've seen it in 'a' so far ... kinda avail = {} availhas, matches = avail.has_key, 0 for elt in self.a: if availhas(elt): numb = avail[elt] else: numb = fullbcount.get(elt, 0) avail[elt] = numb - 1 if numb > 0: matches = matches + 1 return 2.0 * matches / (len(self.a) + len(self.b)) def real_quick_ratio(self): """Return an upper bound on ratio() very quickly. This isn't defined beyond that it is an upper bound on .ratio(), and is faster to compute than either .ratio() or .quick_ratio(). """ la, lb = len(self.a), len(self.b) # can't have more matches than the number of elements in the # shorter sequence return 2.0 * min(la, lb) / (la + lb) def get_close_matches(word, possibilities, n=3, cutoff=0.6): """Use SequenceMatcher to return list of the best "good enough" matches. word is a sequence for which close matches are desired (typically a string). possibilities is a list of sequences against which to match word (typically a list of strings). Optional arg n (default 3) is the maximum number of close matches to return. n must be > 0. Optional arg cutoff (default 0.6) is a float in [0, 1]. Possibilities that don't score at least that similar to word are ignored. The best (no more than n) matches among the possibilities are returned in a list, sorted by similarity score, most similar first. >>> get_close_matches("appel", ["ape", "apple", "peach", "puppy"]) ['apple', 'ape'] >>> import keyword >>> get_close_matches("wheel", keyword.kwlist) ['while'] >>> get_close_matches("apple", keyword.kwlist) [] >>> get_close_matches("accept", keyword.kwlist) ['except'] """ if not n > 0: raise ValueError("n must be > 0: " + `n`) if not 0.0 <= cutoff <= 1.0: raise ValueError("cutoff must be in [0.0, 1.0]: " + `cutoff`) result = [] s = SequenceMatcher() s.set_seq2(word) for x in possibilities: s.set_seq1(x) if s.real_quick_ratio() >= cutoff and \ s.quick_ratio() >= cutoff and \ s.ratio() >= cutoff: result.append((s.ratio(), x)) # Sort by score. result.sort() # Retain only the best n. result = result[-n:] # Move best-scorer to head of list. result.reverse() # Strip scores. # Python 2.x list comprehensions: return [x for score, x in result] return_result = [] for score, x in result: return_result.append(x) return return_result def _test(): import doctest, difflib return doctest.testmod(difflib) if __name__ == "__main__": _test() viewvc-1.1.22/lib/vclib/0000755000175000017500000000000012265242270015151 5ustar cmpilatocmpilatoviewvc-1.1.22/lib/vclib/svn/0000755000175000017500000000000012265242270015757 5ustar cmpilatocmpilatoviewvc-1.1.22/lib/vclib/svn/__init__.py0000644000175000017500000000710012122334006020055 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- "Version Control lib driver for Subversion repositories" import os import os.path import re import urllib _re_url = re.compile('^(http|https|file|svn|svn\+[^:]+)://') def _canonicalize_path(path): import svn.core try: return svn.core.svn_path_canonicalize(path) except AttributeError: # svn_path_canonicalize() appeared in 1.4.0 bindings # There's so much more that we *could* do here, but if we're # here at all its because there's a really old Subversion in # place, and those older Subversion versions cared quite a bit # less about the specifics of path canonicalization. if re.search(_re_url, path): return path.rstrip('/') else: return os.path.normpath(path) def canonicalize_rootpath(rootpath): # Try to canonicalize the rootpath using Subversion semantics. rootpath = _canonicalize_path(rootpath) # ViewVC's support for local repositories is more complete and more # performant than its support for remote ones, so if we're on a # Unix-y system and we have a file:/// URL, convert it to a local # path instead. if os.name == 'posix': rootpath_lower = rootpath.lower() if rootpath_lower in ['file://localhost', 'file://localhost/', 'file://', 'file:///' ]: return '/' if rootpath_lower.startswith('file://localhost/'): rootpath = os.path.normpath(urllib.unquote(rootpath[16:])) elif rootpath_lower.startswith('file:///'): rootpath = os.path.normpath(urllib.unquote(rootpath[7:])) # Ensure that we have an absolute path (or URL), and return. if not re.search(_re_url, rootpath): assert os.path.isabs(rootpath) return rootpath def expand_root_parent(parent_path): roots = {} if re.search(_re_url, parent_path): pass else: # Any subdirectories of PARENT_PATH which themselves have a child # "format" are returned as roots. assert os.path.isabs(parent_path) subpaths = os.listdir(parent_path) for rootname in subpaths: rootpath = os.path.join(parent_path, rootname) if os.path.exists(os.path.join(rootpath, "format")): roots[rootname] = canonicalize_rootpath(rootpath) return roots def find_root_in_parent(parent_path, rootname): """Search PARENT_PATH for a root named ROOTNAME, returning the canonicalized ROOTPATH of the root if found; return None if no such root is found.""" if not re.search(_re_url, parent_path): assert os.path.isabs(parent_path) rootpath = os.path.join(parent_path, rootname) format_path = os.path.join(rootpath, "format") if os.path.exists(format_path): return canonicalize_rootpath(rootpath) return None def SubversionRepository(name, rootpath, authorizer, utilities, config_dir): rootpath = canonicalize_rootpath(rootpath) if re.search(_re_url, rootpath): import svn_ra return svn_ra.RemoteSubversionRepository(name, rootpath, authorizer, utilities, config_dir) else: import svn_repos return svn_repos.LocalSubversionRepository(name, rootpath, authorizer, utilities, config_dir) viewvc-1.1.22/lib/vclib/svn/svn_repos.py0000644000175000017500000010265712133055421020354 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- "Version Control lib driver for locally accessible Subversion repositories" import vclib import os import os.path import string import cStringIO import signal import time import tempfile import popen import re import urllib from svn import fs, repos, core, client, delta ### Require Subversion 1.3.1 or better. if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 3, 1): raise Exception, "Version requirement not met (needs 1.3.1 or better)" ### Pre-1.5 Subversion doesn't have SVN_ERR_CEASE_INVOCATION try: _SVN_ERR_CEASE_INVOCATION = core.SVN_ERR_CEASE_INVOCATION except: _SVN_ERR_CEASE_INVOCATION = core.SVN_ERR_CANCELLED ### Pre-1.5 SubversionException's might not have the .msg and .apr_err members def _fix_subversion_exception(e): if not hasattr(e, 'apr_err'): e.apr_err = e[1] if not hasattr(e, 'message'): e.message = e[0] ### Pre-1.4 Subversion doesn't have svn_path_canonicalize() def _canonicalize_path(path): try: return core.svn_path_canonicalize(path) except AttributeError: return path def _allow_all(root, path, pool): """Generic authz_read_func that permits access to all paths""" return 1 def _path_parts(path): return filter(None, string.split(path, '/')) def _cleanup_path(path): """Return a cleaned-up Subversion filesystem path""" return string.join(_path_parts(path), '/') def _fs_path_join(base, relative): return _cleanup_path(base + '/' + relative) def _compare_paths(path1, path2): path1_len = len (path1); path2_len = len (path2); min_len = min(path1_len, path2_len) i = 0 # Are the paths exactly the same? if path1 == path2: return 0 # Skip past common prefix while (i < min_len) and (path1[i] == path2[i]): i = i + 1 # Children of paths are greater than their parents, but less than # greater siblings of their parents char1 = '\0' char2 = '\0' if (i < path1_len): char1 = path1[i] if (i < path2_len): char2 = path2[i] if (char1 == '/') and (i == path2_len): return 1 if (char2 == '/') and (i == path1_len): return -1 if (i < path1_len) and (char1 == '/'): return -1 if (i < path2_len) and (char2 == '/'): return 1 # Common prefix was skipped above, next character is compared to # determine order return cmp(char1, char2) def _rev2optrev(rev): assert type(rev) is int rt = core.svn_opt_revision_t() rt.kind = core.svn_opt_revision_number rt.value.number = rev return rt def _rootpath2url(rootpath, path): rootpath = os.path.abspath(rootpath) drive, rootpath = os.path.splitdrive(rootpath) if os.sep != '/': rootpath = string.replace(rootpath, os.sep, '/') rootpath = urllib.quote(rootpath) path = urllib.quote(path) if drive: url = 'file:///' + drive + rootpath + '/' + path else: url = 'file://' + rootpath + '/' + path return _canonicalize_path(url) # Given a dictionary REVPROPS of revision properties, pull special # ones out of them and return a 4-tuple containing the log message, # the author, the date (converted from the date string property), and # a dictionary of any/all other revprops. def _split_revprops(revprops): if not revprops: return None, None, None, {} special_props = [] for prop in core.SVN_PROP_REVISION_LOG, \ core.SVN_PROP_REVISION_AUTHOR, \ core.SVN_PROP_REVISION_DATE: if revprops.has_key(prop): special_props.append(revprops[prop]) del(revprops[prop]) else: special_props.append(None) msg, author, datestr = tuple(special_props) date = _datestr_to_date(datestr) return msg, author, date, revprops def _datestr_to_date(datestr): try: return core.svn_time_from_cstring(datestr) / 1000000 except: return None class Revision(vclib.Revision): "Hold state for each revision's log entry." def __init__(self, rev, date, author, msg, size, lockinfo, filename, copy_path, copy_rev): vclib.Revision.__init__(self, rev, str(rev), date, author, None, msg, size, lockinfo) self.filename = filename self.copy_path = copy_path self.copy_rev = copy_rev class NodeHistory: """An iterable object that returns 2-tuples of (revision, path) locations along a node's change history, ordered from youngest to oldest.""" def __init__(self, fs_ptr, show_all_logs, limit=0): self.histories = [] self.fs_ptr = fs_ptr self.show_all_logs = show_all_logs self.oldest_rev = None self.limit = limit def add_history(self, path, revision, pool): # If filtering, only add the path and revision to the histories # list if they were actually changed in this revision (where # change means the path itself was changed, or one of its parents # was copied). This is useful for omitting bubble-up directory # changes. if not self.oldest_rev: self.oldest_rev = revision else: assert(revision < self.oldest_rev) if not self.show_all_logs: rev_root = fs.revision_root(self.fs_ptr, revision) changed_paths = fs.paths_changed(rev_root) paths = changed_paths.keys() if path not in paths: # Look for a copied parent test_path = path found = 0 while 1: off = string.rfind(test_path, '/') if off < 0: break test_path = test_path[0:off] if test_path in paths: copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, test_path) if copyfrom_rev >= 0 and copyfrom_path: found = 1 break if not found: return self.histories.append([revision, _cleanup_path(path)]) if self.limit and len(self.histories) == self.limit: raise core.SubversionException("", _SVN_ERR_CEASE_INVOCATION) def __getitem__(self, idx): return self.histories[idx] def _get_last_history_rev(fsroot, path): history = fs.node_history(fsroot, path) history = fs.history_prev(history, 0) history_path, history_rev = fs.history_location(history) return history_rev def temp_checkout(svnrepos, path, rev): """Check out file revision to temporary file""" temp = tempfile.mktemp() fp = open(temp, 'wb') try: root = svnrepos._getroot(rev) stream = fs.file_contents(root, path) try: while 1: chunk = core.svn_stream_read(stream, core.SVN_STREAM_CHUNK_SIZE) if not chunk: break fp.write(chunk) finally: core.svn_stream_close(stream) finally: fp.close() return temp class FileContentsPipe: def __init__(self, root, path): self._stream = fs.file_contents(root, path) self._eof = 0 def read(self, len=None): chunk = None if not self._eof: if len is None: buffer = cStringIO.StringIO() try: while 1: hunk = core.svn_stream_read(self._stream, 8192) if not hunk: break buffer.write(hunk) chunk = buffer.getvalue() finally: buffer.close() else: chunk = core.svn_stream_read(self._stream, len) if not chunk: self._eof = 1 return chunk def readline(self): chunk = None if not self._eof: chunk, self._eof = core.svn_stream_readline(self._stream, '\n') if not self._eof: chunk = chunk + '\n' if not chunk: self._eof = 1 return chunk def readlines(self): lines = [] while True: line = self.readline() if not line: break lines.append(line) return lines def close(self): return core.svn_stream_close(self._stream) def eof(self): return self._eof class BlameSource: def __init__(self, local_url, rev, first_rev, include_text, config_dir): self.idx = -1 self.first_rev = first_rev self.blame_data = [] self.include_text = include_text ctx = client.svn_client_create_context() core.svn_config_ensure(config_dir) ctx.config = core.svn_config_get_config(config_dir) ctx.auth_baton = core.svn_auth_open([]) try: ### TODO: Is this use of FIRST_REV always what we want? Should we ### pass 1 here instead and do filtering later? client.blame2(local_url, _rev2optrev(rev), _rev2optrev(first_rev), _rev2optrev(rev), self._blame_cb, ctx) except core.SubversionException, e: _fix_subversion_exception(e) if e.apr_err == core.SVN_ERR_CLIENT_IS_BINARY_FILE: raise vclib.NonTextualFileContents raise def _blame_cb(self, line_no, rev, author, date, text, pool): prev_rev = None if rev > self.first_rev: prev_rev = rev - 1 if not self.include_text: text = None self.blame_data.append(vclib.Annotation(text, line_no + 1, rev, prev_rev, author, None)) def __getitem__(self, idx): if idx != self.idx + 1: raise BlameSequencingError() self.idx = idx return self.blame_data[idx] class BlameSequencingError(Exception): pass class SVNChangedPath(vclib.ChangedPath): """Wrapper around vclib.ChangedPath which handles path splitting.""" def __init__(self, path, rev, pathtype, base_path, base_rev, action, copied, text_changed, props_changed): path_parts = _path_parts(path or '') base_path_parts = _path_parts(base_path or '') vclib.ChangedPath.__init__(self, path_parts, rev, pathtype, base_path_parts, base_rev, action, copied, text_changed, props_changed) class LocalSubversionRepository(vclib.Repository): def __init__(self, name, rootpath, authorizer, utilities, config_dir): if not (os.path.isdir(rootpath) \ and os.path.isfile(os.path.join(rootpath, 'format'))): raise vclib.ReposNotFound(name) # Initialize some stuff. self.rootpath = rootpath self.name = name self.auth = authorizer self.svn_client_path = utilities.svn or 'svn' self.diff_cmd = utilities.diff or 'diff' self.config_dir = config_dir or None # See if this repository is even viewable, authz-wise. if not vclib.check_root_access(self): raise vclib.ReposNotFound(name) def open(self): # Register a handler for SIGTERM so we can have a chance to # cleanup. If ViewVC takes too long to start generating CGI # output, Apache will grow impatient and SIGTERM it. While we # don't mind getting told to bail, we want to gracefully close the # repository before we bail. def _sigterm_handler(signum, frame, self=self): sys.exit(-1) try: signal.signal(signal.SIGTERM, _sigterm_handler) except ValueError: # This is probably "ValueError: signal only works in main # thread", which will get thrown by the likes of mod_python # when trying to install a signal handler from a thread that # isn't the main one. We'll just not care. pass # Open the repository and init some other variables. self.repos = repos.svn_repos_open(self.rootpath) self.fs_ptr = repos.svn_repos_fs(self.repos) self.youngest = fs.youngest_rev(self.fs_ptr) self._fsroots = {} self._revinfo_cache = {} # See if a universal read access determination can be made. if self.auth and self.auth.check_universal_access(self.name) == 1: self.auth = None def rootname(self): return self.name def rootpath(self): return self.rootpath def roottype(self): return vclib.SVN def authorizer(self): return self.auth def itemtype(self, path_parts, rev): rev = self._getrev(rev) basepath = self._getpath(path_parts) pathtype = self._gettype(basepath, rev) if pathtype is None: raise vclib.ItemNotFound(path_parts) if not vclib.check_path_access(self, path_parts, pathtype, rev): raise vclib.ItemNotFound(path_parts) return pathtype def openfile(self, path_parts, rev, options): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % path) rev = self._getrev(rev) fsroot = self._getroot(rev) revision = str(_get_last_history_rev(fsroot, path)) fp = FileContentsPipe(fsroot, path) return fp, revision def listdir(self, path_parts, rev, options): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check raise vclib.Error("Path '%s' is not a directory." % path) rev = self._getrev(rev) fsroot = self._getroot(rev) dirents = fs.dir_entries(fsroot, path) entries = [ ] for entry in dirents.values(): if entry.kind == core.svn_node_dir: kind = vclib.DIR elif entry.kind == core.svn_node_file: kind = vclib.FILE if vclib.check_path_access(self, path_parts + [entry.name], kind, rev): entries.append(vclib.DirEntry(entry.name, kind)) return entries def dirlogs(self, path_parts, rev, entries, options): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check raise vclib.Error("Path '%s' is not a directory." % path) fsroot = self._getroot(self._getrev(rev)) rev = self._getrev(rev) for entry in entries: entry_path_parts = path_parts + [entry.name] if not vclib.check_path_access(self, entry_path_parts, entry.kind, rev): continue path = self._getpath(entry_path_parts) entry_rev = _get_last_history_rev(fsroot, path) date, author, msg, revprops, changes = self._revinfo(entry_rev) entry.rev = str(entry_rev) entry.date = date entry.author = author entry.log = msg if entry.kind == vclib.FILE: entry.size = fs.file_length(fsroot, path) lock = fs.get_lock(self.fs_ptr, path) entry.lockinfo = lock and lock.owner or None def itemlog(self, path_parts, rev, sortby, first, limit, options): """see vclib.Repository.itemlog docstring Option values recognized by this implementation svn_show_all_dir_logs boolean, default false. if set for a directory path, will include revisions where files underneath the directory have changed svn_cross_copies boolean, default false. if set for a path created by a copy, will include revisions from before the copy svn_latest_log boolean, default false. if set will return only newest single log entry """ assert sortby == vclib.SORTBY_DEFAULT or sortby == vclib.SORTBY_REV path = self._getpath(path_parts) path_type = self.itemtype(path_parts, rev) # does auth-check rev = self._getrev(rev) revs = [] lockinfo = None # See if this path is locked. try: lock = fs.get_lock(self.fs_ptr, path) if lock: lockinfo = lock.owner except NameError: pass # If our caller only wants the latest log, we'll invoke # _log_helper for just the one revision. Otherwise, we go off # into history-fetching mode. ### TODO: we could stand to have a # 'limit' parameter here as numeric cut-off for the depth of our # history search. if options.get('svn_latest_log', 0): revision = self._log_helper(path, rev, lockinfo) if revision: revision.prev = None revs.append(revision) else: history = self._get_history(path, rev, path_type, first + limit, options) if len(history) < first: history = [] if limit: history = history[first:first+limit] for hist_rev, hist_path in history: revision = self._log_helper(hist_path, hist_rev, lockinfo) if revision: # If we have unreadable copyfrom data, obscure it. if revision.copy_path is not None: cp_parts = _path_parts(revision.copy_path) if not vclib.check_path_access(self, cp_parts, path_type, revision.copy_rev): revision.copy_path = revision.copy_rev = None revision.prev = None if len(revs): revs[-1].prev = revision revs.append(revision) return revs def itemprops(self, path_parts, rev): path = self._getpath(path_parts) path_type = self.itemtype(path_parts, rev) # does auth-check rev = self._getrev(rev) fsroot = self._getroot(rev) return fs.node_proplist(fsroot, path) def annotate(self, path_parts, rev, include_text=False): path = self._getpath(path_parts) path_type = self.itemtype(path_parts, rev) # does auth-check if path_type != vclib.FILE: raise vclib.Error("Path '%s' is not a file." % path) rev = self._getrev(rev) fsroot = self._getroot(rev) history = self._get_history(path, rev, path_type, 0, {'svn_cross_copies': 1}) youngest_rev, youngest_path = history[0] oldest_rev, oldest_path = history[-1] source = BlameSource(_rootpath2url(self.rootpath, path), youngest_rev, oldest_rev, include_text, self.config_dir) return source, youngest_rev def revinfo(self, rev): return self._revinfo(rev, 1) def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): p1 = self._getpath(path_parts1) p2 = self._getpath(path_parts2) r1 = self._getrev(rev1) r2 = self._getrev(rev2) if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1): raise vclib.ItemNotFound(path_parts1) if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2): raise vclib.ItemNotFound(path_parts2) args = vclib._diff_args(type, options) def _date_from_rev(rev): date, author, msg, revprops, changes = self._revinfo(rev) return date try: temp1 = temp_checkout(self, p1, r1) temp2 = temp_checkout(self, p2, r2) info1 = p1, _date_from_rev(r1), r1 info2 = p2, _date_from_rev(r2), r2 return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args) except core.SubversionException, e: _fix_subversion_exception(e) if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: raise vclib.InvalidRevision raise def isexecutable(self, path_parts, rev): props = self.itemprops(path_parts, rev) # does authz-check return props.has_key(core.SVN_PROP_EXECUTABLE) def filesize(self, path_parts, rev): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % path) fsroot = self._getroot(self._getrev(rev)) return fs.file_length(fsroot, path) ##--- helpers ---## def _revinfo(self, rev, include_changed_paths=0): """Internal-use, cache-friendly revision information harvester.""" def _get_changed_paths(fsroot): """Return a 3-tuple: found_readable, found_unreadable, changed_paths.""" editor = repos.ChangeCollector(self.fs_ptr, fsroot) e_ptr, e_baton = delta.make_editor(editor) repos.svn_repos_replay(fsroot, e_ptr, e_baton) changedpaths = {} changes = editor.get_changes() # Copy the Subversion changes into a new hash, checking # authorization and converting them into ChangedPath objects. found_readable = found_unreadable = 0 for path in changes.keys(): change = changes[path] if change.path: change.path = _cleanup_path(change.path) if change.base_path: change.base_path = _cleanup_path(change.base_path) is_copy = 0 if not hasattr(change, 'action'): # new to subversion 1.4.0 action = vclib.MODIFIED if not change.path: action = vclib.DELETED elif change.added: action = vclib.ADDED replace_check_path = path if change.base_path and change.base_rev: replace_check_path = change.base_path if changedpaths.has_key(replace_check_path) \ and changedpaths[replace_check_path].action == vclib.DELETED: action = vclib.REPLACED else: if change.action == repos.CHANGE_ACTION_ADD: action = vclib.ADDED elif change.action == repos.CHANGE_ACTION_DELETE: action = vclib.DELETED elif change.action == repos.CHANGE_ACTION_REPLACE: action = vclib.REPLACED else: action = vclib.MODIFIED if (action == vclib.ADDED or action == vclib.REPLACED) \ and change.base_path \ and change.base_rev: is_copy = 1 if change.item_kind == core.svn_node_dir: pathtype = vclib.DIR elif change.item_kind == core.svn_node_file: pathtype = vclib.FILE else: pathtype = None parts = _path_parts(path) if vclib.check_path_access(self, parts, pathtype, rev): if is_copy and change.base_path and (change.base_path != path): parts = _path_parts(change.base_path) if not vclib.check_path_access(self, parts, pathtype, change.base_rev): is_copy = 0 change.base_path = None change.base_rev = None found_unreadable = 1 changedpaths[path] = SVNChangedPath(path, rev, pathtype, change.base_path, change.base_rev, action, is_copy, change.text_changed, change.prop_changes) found_readable = 1 else: found_unreadable = 1 return found_readable, found_unreadable, changedpaths.values() def _get_change_copyinfo(fsroot, path, change): # If we know the copyfrom info, return it... if hasattr(change, 'copyfrom_known') and change.copyfrom_known: copyfrom_path = change.copyfrom_path copyfrom_rev = change.copyfrom_rev # ...otherwise, if this change could be a copy (that is, it # contains an add action), query the copyfrom info ... elif (change.change_kind == fs.path_change_add or change.change_kind == fs.path_change_replace): copyfrom_rev, copyfrom_path = fs.copied_from(fsroot, path) # ...else, there's no copyfrom info. else: copyfrom_rev = core.SVN_INVALID_REVNUM copyfrom_path = None return copyfrom_path, copyfrom_rev def _simple_auth_check(fsroot): """Return a 2-tuple: found_readable, found_unreadable.""" found_unreadable = found_readable = 0 if hasattr(fs, 'paths_changed2'): changes = fs.paths_changed2(fsroot) else: changes = fs.paths_changed(fsroot) paths = changes.keys() for path in paths: change = changes[path] pathtype = None if hasattr(change, 'node_kind'): if change.node_kind == core.svn_node_file: pathtype = vclib.FILE elif change.node_kind == core.svn_node_dir: pathtype = vclib.DIR parts = _path_parts(path) if pathtype is None: # Figure out the pathtype so we can query the authz subsystem. if change.change_kind == fs.path_change_delete: # Deletions are annoying, because they might be underneath # copies (make their previous location non-trivial). prev_parts = parts prev_rev = rev - 1 parent_parts = parts[:-1] while parent_parts: parent_path = '/' + self._getpath(parent_parts) parent_change = changes.get(parent_path) if not (parent_change and \ (parent_change.change_kind == fs.path_change_add or parent_change.change_kind == fs.path_change_replace)): del(parent_parts[-1]) continue copyfrom_path, copyfrom_rev = \ _get_change_copyinfo(fsroot, parent_path, parent_change) if copyfrom_path: prev_rev = copyfrom_rev prev_parts = _path_parts(copyfrom_path) + \ parts[len(parent_parts):] break del(parent_parts[-1]) pathtype = self._gettype(self._getpath(prev_parts), prev_rev) else: pathtype = self._gettype(self._getpath(parts), rev) if vclib.check_path_access(self, parts, pathtype, rev): found_readable = 1 copyfrom_path, copyfrom_rev = \ _get_change_copyinfo(fsroot, path, change) if copyfrom_path and copyfrom_path != path: parts = _path_parts(copyfrom_path) if not vclib.check_path_access(self, parts, pathtype, copyfrom_rev): found_unreadable = 1 else: found_unreadable = 1 if found_readable and found_unreadable: break return found_readable, found_unreadable def _revinfo_helper(rev, include_changed_paths): # Get the revision property info. (Would use # editor.get_root_props(), but something is broken there...) revprops = fs.revision_proplist(self.fs_ptr, rev) msg, author, date, revprops = _split_revprops(revprops) # Optimization: If our caller doesn't care about the changed # paths, and we don't need them to do authz determinations, let's # get outta here. if self.auth is None and not include_changed_paths: return date, author, msg, revprops, None # If we get here, then we either need the changed paths because we # were asked for them, or we need them to do authorization checks. # # If we only need them for authorization checks, though, we # won't bother generating fully populated ChangedPath items (the # cost is too great). fsroot = self._getroot(rev) if include_changed_paths: found_readable, found_unreadable, changedpaths = \ _get_changed_paths(fsroot) else: changedpaths = None found_readable, found_unreadable = _simple_auth_check(fsroot) # Filter our metadata where necessary, and return the requested data. if found_unreadable: msg = None if not found_readable: author = None date = None return date, author, msg, revprops, changedpaths # Consult the revinfo cache first. If we don't have cached info, # or our caller wants changed paths and we don't have those for # this revision, go do the real work. rev = self._getrev(rev) cached_info = self._revinfo_cache.get(rev) if not cached_info \ or (include_changed_paths and cached_info[4] is None): cached_info = _revinfo_helper(rev, include_changed_paths) self._revinfo_cache[rev] = cached_info return tuple(cached_info) def _log_helper(self, path, rev, lockinfo): rev_root = fs.revision_root(self.fs_ptr, rev) copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, path) date, author, msg, revprops, changes = self._revinfo(rev) if fs.is_file(rev_root, path): size = fs.file_length(rev_root, path) else: size = None return Revision(rev, date, author, msg, size, lockinfo, path, copyfrom_path and _cleanup_path(copyfrom_path), copyfrom_rev) def _get_history(self, path, rev, path_type, limit=0, options={}): if self.youngest == 0: return [] rev_paths = [] fsroot = self._getroot(rev) show_all_logs = options.get('svn_show_all_dir_logs', 0) if not show_all_logs: # See if the path is a file or directory. kind = fs.check_path(fsroot, path) if kind is core.svn_node_file: show_all_logs = 1 # Instantiate a NodeHistory collector object, and use it to collect # history items for PATH@REV. history = NodeHistory(self.fs_ptr, show_all_logs, limit) try: repos.svn_repos_history(self.fs_ptr, path, history.add_history, 1, rev, options.get('svn_cross_copies', 0)) except core.SubversionException, e: _fix_subversion_exception(e) if e.apr_err != _SVN_ERR_CEASE_INVOCATION: raise # Now, iterate over those history items, checking for changes of # location, pruning as necessitated by authz rules. for hist_rev, hist_path in history: path_parts = _path_parts(hist_path) if not vclib.check_path_access(self, path_parts, path_type, hist_rev): break rev_paths.append([hist_rev, hist_path]) return rev_paths def _getpath(self, path_parts): return string.join(path_parts, '/') def _getrev(self, rev): if rev is None or rev == 'HEAD': return self.youngest try: if type(rev) == type(''): while rev[0] == 'r': rev = rev[1:] rev = int(rev) except: raise vclib.InvalidRevision(rev) if (rev < 0) or (rev > self.youngest): raise vclib.InvalidRevision(rev) return rev def _getroot(self, rev): try: return self._fsroots[rev] except KeyError: r = self._fsroots[rev] = fs.revision_root(self.fs_ptr, rev) return r def _gettype(self, path, rev): # Similar to itemtype(), but without the authz check. Returns # None for missing paths. try: kind = fs.check_path(self._getroot(rev), path) except: return None if kind == core.svn_node_dir: return vclib.DIR if kind == core.svn_node_file: return vclib.FILE return None ##--- custom ---## def get_youngest_revision(self): return self.youngest def get_location(self, path, rev, old_rev): try: results = repos.svn_repos_trace_node_locations(self.fs_ptr, path, rev, [old_rev], _allow_all) except core.SubversionException, e: _fix_subversion_exception(e) if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: raise vclib.ItemNotFound(path) raise try: old_path = results[old_rev] except KeyError: raise vclib.ItemNotFound(path) return _cleanup_path(old_path) def created_rev(self, full_name, rev): return fs.node_created_rev(self._getroot(rev), full_name) def last_rev(self, path, peg_revision, limit_revision=None): """Given PATH, known to exist in PEG_REVISION, find the youngest revision older than, or equal to, LIMIT_REVISION in which path exists. Return that revision, and the path at which PATH exists in that revision.""" # Here's the plan, man. In the trivial case (where PEG_REVISION is # the same as LIMIT_REVISION), this is a no-brainer. If # LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's # history tracing code to find the right location. If, however, # LIMIT_REVISION is younger than PEG_REVISION, we suffer from # Subversion's lack of forward history searching. Our workaround, # ugly as it may be, involves a binary search through the revisions # between PEG_REVISION and LIMIT_REVISION to find our last live # revision. peg_revision = self._getrev(peg_revision) limit_revision = self._getrev(limit_revision) try: if peg_revision == limit_revision: return peg_revision, path elif peg_revision > limit_revision: fsroot = self._getroot(peg_revision) history = fs.node_history(fsroot, path) while history: path, peg_revision = fs.history_location(history) if peg_revision <= limit_revision: return max(peg_revision, limit_revision), _cleanup_path(path) history = fs.history_prev(history, 1) return peg_revision, _cleanup_path(path) else: orig_id = fs.node_id(self._getroot(peg_revision), path) while peg_revision != limit_revision: mid = (peg_revision + 1 + limit_revision) / 2 try: mid_id = fs.node_id(self._getroot(mid), path) except core.SubversionException, e: _fix_subversion_exception(e) if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: cmp = -1 else: raise else: ### Not quite right. Need a comparison function that only returns ### true when the two nodes are the same copy, not just related. cmp = fs.compare_ids(orig_id, mid_id) if cmp in (0, 1): peg_revision = mid else: limit_revision = mid - 1 return peg_revision, path finally: pass def get_symlink_target(self, path_parts, rev): """Return the target of the symbolic link versioned at PATH_PARTS in REV, or None if that object is not a symlink.""" path = self._getpath(path_parts) rev = self._getrev(rev) path_type = self.itemtype(path_parts, rev) # does auth-check fsroot = self._getroot(rev) # Symlinks must be files with the svn:special property set on them # and with file contents which read "link SOME_PATH". if path_type != vclib.FILE: return None props = fs.node_proplist(fsroot, path) if not props.has_key(core.SVN_PROP_SPECIAL): return None pathspec = '' ### FIXME: We're being a touch sloppy here, only checking the first line ### of the file. stream = fs.file_contents(fsroot, path) try: pathspec, eof = core.svn_stream_readline(stream, '\n') finally: core.svn_stream_close(stream) if pathspec[:5] != 'link ': return None return pathspec[5:] viewvc-1.1.22/lib/vclib/svn/svn_ra.py0000644000175000017500000007047212133563555017641 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- "Version Control lib driver for remotely accessible Subversion repositories." import vclib import sys import os import string import re import tempfile import time import urllib from svn_repos import Revision, SVNChangedPath, _datestr_to_date, \ _compare_paths, _path_parts, _cleanup_path, \ _rev2optrev, _fix_subversion_exception, \ _split_revprops, _canonicalize_path from svn import core, delta, client, wc, ra ### Require Subversion 1.3.1 or better. (for svn_ra_get_locations support) if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 3, 1): raise Exception, "Version requirement not met (needs 1.3.1 or better)" ### BEGIN COMPATABILITY CODE ### try: SVN_INVALID_REVNUM = core.SVN_INVALID_REVNUM except AttributeError: # The 1.4.x bindings are missing core.SVN_INVALID_REVNUM SVN_INVALID_REVNUM = -1 def list_directory(url, peg_rev, rev, flag, ctx): try: dirents, locks = client.svn_client_ls3(url, peg_rev, rev, flag, ctx) except TypeError: # 1.4.x bindings are goofed dirents = client.svn_client_ls3(None, url, peg_rev, rev, flag, ctx) locks = {} return dirents, locks def get_directory_props(ra_session, path, rev): try: dirents, fetched_rev, props = ra.svn_ra_get_dir(ra_session, path, rev) except ValueError: # older bindings are goofed props = ra.svn_ra_get_dir(ra_session, path, rev) return props def client_log(url, start_rev, end_rev, log_limit, include_changes, cross_copies, cb_func, ctx): include_changes = include_changes and 1 or 0 cross_copies = cross_copies and 1 or 0 try: client.svn_client_log4([url], start_rev, start_rev, end_rev, log_limit, include_changes, not cross_copies, 0, None, cb_func, ctx) except AttributeError: # Wrap old svn_log_message_receiver_t interface with a # svn_log_entry_t one. def cb_convert(paths, revision, author, date, message, pool): class svn_log_entry_t: pass log_entry = svn_log_entry_t() log_entry.changed_paths = paths log_entry.revision = revision log_entry.revprops = { core.SVN_PROP_REVISION_LOG : message, core.SVN_PROP_REVISION_AUTHOR : author, core.SVN_PROP_REVISION_DATE : date, } cb_func(log_entry, pool) client.svn_client_log2([url], start_rev, end_rev, log_limit, include_changes, not cross_copies, cb_convert, ctx) def setup_client_ctx(config_dir): # Ensure that the configuration directory exists. core.svn_config_ensure(config_dir) # Fetch the configuration (and 'config' bit thereof). cfg = core.svn_config_get_config(config_dir) config = cfg.get(core.SVN_CONFIG_CATEGORY_CONFIG) # Here's the compat-sensitive part: try to use # svn_cmdline_create_auth_baton(), and fall back to making our own # if that fails. try: auth_baton = core.svn_cmdline_create_auth_baton(1, None, None, config_dir, 1, 1, config, None) except AttributeError: auth_baton = core.svn_auth_open([ client.svn_client_get_simple_provider(), client.svn_client_get_username_provider(), client.svn_client_get_ssl_server_trust_file_provider(), client.svn_client_get_ssl_client_cert_file_provider(), client.svn_client_get_ssl_client_cert_pw_file_provider(), ]) if config_dir is not None: core.svn_auth_set_parameter(auth_baton, core.SVN_AUTH_PARAM_CONFIG_DIR, config_dir) # Create, setup, and return the client context baton. ctx = client.svn_client_create_context() ctx.config = cfg ctx.auth_baton = auth_baton return ctx ### END COMPATABILITY CODE ### class LogCollector: def __init__(self, path, show_all_logs, lockinfo, access_check_func): # This class uses leading slashes for paths internally if not path: self.path = '/' else: self.path = path[0] == '/' and path or '/' + path self.logs = [] self.show_all_logs = show_all_logs self.lockinfo = lockinfo self.access_check_func = access_check_func self.done = False def add_log(self, log_entry, pool): if self.done: return paths = log_entry.changed_paths revision = log_entry.revision msg, author, date, revprops = _split_revprops(log_entry.revprops) # Changed paths have leading slashes changed_paths = paths.keys() changed_paths.sort(lambda a, b: _compare_paths(a, b)) this_path = None if self.path in changed_paths: this_path = self.path change = paths[self.path] if change.copyfrom_path: this_path = change.copyfrom_path for changed_path in changed_paths: if changed_path != self.path: # If a parent of our path was copied, our "next previous" # (huh?) path will exist elsewhere (under the copy source). if (string.rfind(self.path, changed_path) == 0) and \ self.path[len(changed_path)] == '/': change = paths[changed_path] if change.copyfrom_path: this_path = change.copyfrom_path + self.path[len(changed_path):] if self.show_all_logs or this_path: if self.access_check_func is None \ or self.access_check_func(self.path[1:], revision): entry = Revision(revision, date, author, msg, None, self.lockinfo, self.path[1:], None, None) self.logs.append(entry) else: self.done = True if this_path: self.path = this_path def cat_to_tempfile(svnrepos, path, rev): """Check out file revision to temporary file""" temp = tempfile.mktemp() stream = core.svn_stream_from_aprfile(temp) url = svnrepos._geturl(path) client.svn_client_cat(core.Stream(stream), url, _rev2optrev(rev), svnrepos.ctx) core.svn_stream_close(stream) return temp class SelfCleanFP: def __init__(self, path): self._fp = open(path, 'r') self._path = path self._eof = 0 def read(self, len=None): if len: chunk = self._fp.read(len) else: chunk = self._fp.read() if chunk == '': self._eof = 1 return chunk def readline(self): chunk = self._fp.readline() if chunk == '': self._eof = 1 return chunk def readlines(self): lines = self._fp.readlines() self._eof = 1 return lines def close(self): self._fp.close() os.remove(self._path) def __del__(self): self.close() def eof(self): return self._eof class RemoteSubversionRepository(vclib.Repository): def __init__(self, name, rootpath, authorizer, utilities, config_dir): self.name = name self.rootpath = rootpath self.auth = authorizer self.diff_cmd = utilities.diff or 'diff' self.config_dir = config_dir or None # See if this repository is even viewable, authz-wise. if not vclib.check_root_access(self): raise vclib.ReposNotFound(name) def open(self): # Setup the client context baton, complete with non-prompting authstuffs. self.ctx = setup_client_ctx(self.config_dir) ra_callbacks = ra.svn_ra_callbacks_t() ra_callbacks.auth_baton = self.ctx.auth_baton self.ra_session = ra.svn_ra_open(self.rootpath, ra_callbacks, None, self.ctx.config) self.youngest = ra.svn_ra_get_latest_revnum(self.ra_session) self._dirent_cache = { } self._revinfo_cache = { } # See if a universal read access determination can be made. if self.auth and self.auth.check_universal_access(self.name) == 1: self.auth = None def rootname(self): return self.name def rootpath(self): return self.rootpath def roottype(self): return vclib.SVN def authorizer(self): return self.auth def itemtype(self, path_parts, rev): pathtype = None if not len(path_parts): pathtype = vclib.DIR else: path = self._getpath(path_parts) rev = self._getrev(rev) try: kind = ra.svn_ra_check_path(self.ra_session, path, rev) if kind == core.svn_node_file: pathtype = vclib.FILE elif kind == core.svn_node_dir: pathtype = vclib.DIR except: pass if pathtype is None: raise vclib.ItemNotFound(path_parts) if not vclib.check_path_access(self, path_parts, pathtype, rev): raise vclib.ItemNotFound(path_parts) return pathtype def openfile(self, path_parts, rev, options): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % path) rev = self._getrev(rev) url = self._geturl(path) ### rev here should be the last history revision of the URL fp = SelfCleanFP(cat_to_tempfile(self, path, rev)) lh_rev, c_rev = self._get_last_history_rev(path_parts, rev) return fp, lh_rev def listdir(self, path_parts, rev, options): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check raise vclib.Error("Path '%s' is not a directory." % path) rev = self._getrev(rev) entries = [] dirents, locks = self._get_dirents(path, rev) for name in dirents.keys(): entry = dirents[name] if entry.kind == core.svn_node_dir: kind = vclib.DIR elif entry.kind == core.svn_node_file: kind = vclib.FILE else: kind = None entries.append(vclib.DirEntry(name, kind)) return entries def dirlogs(self, path_parts, rev, entries, options): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check raise vclib.Error("Path '%s' is not a directory." % path) rev = self._getrev(rev) dirents, locks = self._get_dirents(path, rev) for entry in entries: entry_path_parts = path_parts + [entry.name] dirent = dirents.get(entry.name, None) # dirents is authz-sanitized, so ensure the entry is found therein. if dirent is None: continue # Get authz-sanitized revision metadata. entry.date, entry.author, entry.log, revprops, changes = \ self._revinfo(dirent.created_rev) entry.rev = str(dirent.created_rev) entry.size = dirent.size entry.lockinfo = None if locks.has_key(entry.name): entry.lockinfo = locks[entry.name].owner def itemlog(self, path_parts, rev, sortby, first, limit, options): assert sortby == vclib.SORTBY_DEFAULT or sortby == vclib.SORTBY_REV path_type = self.itemtype(path_parts, rev) # does auth-check path = self._getpath(path_parts) rev = self._getrev(rev) url = self._geturl(path) # If this is a file, fetch the lock status and size (as of REV) # for this item. lockinfo = size_in_rev = None if path_type == vclib.FILE: basename = path_parts[-1] list_url = self._geturl(self._getpath(path_parts[:-1])) dirents, locks = list_directory(list_url, _rev2optrev(rev), _rev2optrev(rev), 0, self.ctx) if locks.has_key(basename): lockinfo = locks[basename].owner if dirents.has_key(basename): size_in_rev = dirents[basename].size # Special handling for the 'svn_latest_log' scenario. ### FIXME: Don't like this hack. We should just introduce ### something more direct in the vclib API. if options.get('svn_latest_log', 0): dir_lh_rev, dir_c_rev = self._get_last_history_rev(path_parts, rev) date, author, log, revprops, changes = self._revinfo(dir_lh_rev) return [vclib.Revision(dir_lh_rev, str(dir_lh_rev), date, author, None, log, size_in_rev, lockinfo)] def _access_checker(check_path, check_rev): return vclib.check_path_access(self, _path_parts(check_path), path_type, check_rev) # It's okay if we're told to not show all logs on a file -- all # the revisions should match correctly anyway. lc = LogCollector(path, options.get('svn_show_all_dir_logs', 0), lockinfo, _access_checker) cross_copies = options.get('svn_cross_copies', 0) log_limit = 0 if limit: log_limit = first + limit client_log(url, _rev2optrev(rev), _rev2optrev(1), log_limit, 1, cross_copies, lc.add_log, self.ctx) revs = lc.logs revs.sort() prev = None for rev in revs: # Swap out revision info with stuff from the cache (which is # authz-sanitized). rev.date, rev.author, rev.log, revprops, changes \ = self._revinfo(rev.number) rev.prev = prev prev = rev revs.reverse() if len(revs) < first: return [] if limit: return revs[first:first+limit] return revs def itemprops(self, path_parts, rev): path = self._getpath(path_parts) path_type = self.itemtype(path_parts, rev) # does auth-check rev = self._getrev(rev) url = self._geturl(path) pairs = client.svn_client_proplist2(url, _rev2optrev(rev), _rev2optrev(rev), 0, self.ctx) return pairs and pairs[0][1] or {} def annotate(self, path_parts, rev, include_text=False): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % path) rev = self._getrev(rev) url = self._geturl(path) # Examine logs for the file to determine the oldest revision we are # permitted to see. log_options = { 'svn_cross_copies' : 1, 'svn_show_all_dir_logs' : 1, } revs = self.itemlog(path_parts, rev, vclib.SORTBY_REV, 0, 0, log_options) oldest_rev = revs[-1].number # Now calculate the annotation data. Note that we'll not # inherently trust the provided author and date, because authz # rules might necessitate that we strip that information out. blame_data = [] def _blame_cb(line_no, revision, author, date, line, pool, blame_data=blame_data): prev_rev = None if revision > 1: prev_rev = revision - 1 # If we have an invalid revision, clear the date and author # values. Otherwise, if we have authz filtering to do, use the # revinfo cache to do so. if revision < 0: date = author = None elif self.auth: date, author, msg, revprops, changes = self._revinfo(revision) # Strip text if the caller doesn't want it. if not include_text: line = None blame_data.append(vclib.Annotation(line, line_no + 1, revision, prev_rev, author, date)) client.blame2(url, _rev2optrev(rev), _rev2optrev(oldest_rev), _rev2optrev(rev), _blame_cb, self.ctx) return blame_data, rev def revinfo(self, rev): return self._revinfo(rev, 1) def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): p1 = self._getpath(path_parts1) p2 = self._getpath(path_parts2) r1 = self._getrev(rev1) r2 = self._getrev(rev2) if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1): raise vclib.ItemNotFound(path_parts1) if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2): raise vclib.ItemNotFound(path_parts2) args = vclib._diff_args(type, options) def _date_from_rev(rev): date, author, msg, revprops, changes = self._revinfo(rev) return date try: temp1 = cat_to_tempfile(self, p1, r1) temp2 = cat_to_tempfile(self, p2, r2) info1 = p1, _date_from_rev(r1), r1 info2 = p2, _date_from_rev(r2), r2 return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args) except core.SubversionException, e: _fix_subversion_exception(e) if e.apr_err == vclib.svn.core.SVN_ERR_FS_NOT_FOUND: raise vclib.InvalidRevision raise def isexecutable(self, path_parts, rev): props = self.itemprops(path_parts, rev) # does authz-check return props.has_key(core.SVN_PROP_EXECUTABLE) def filesize(self, path_parts, rev): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % path) rev = self._getrev(rev) dirents, locks = self._get_dirents(self._getpath(path_parts[:-1]), rev) dirent = dirents.get(path_parts[-1], None) return dirent.size def _getpath(self, path_parts): return string.join(path_parts, '/') def _getrev(self, rev): if rev is None or rev == 'HEAD': return self.youngest try: if type(rev) == type(''): while rev[0] == 'r': rev = rev[1:] rev = int(rev) except: raise vclib.InvalidRevision(rev) if (rev < 0) or (rev > self.youngest): raise vclib.InvalidRevision(rev) return rev def _geturl(self, path=None): if not path: return self.rootpath path = self.rootpath + '/' + urllib.quote(path) return _canonicalize_path(path) def _get_dirents(self, path, rev): """Return a 2-type of dirents and locks, possibly reading/writing from a local cache of that information. This functions performs authz checks, stripping out unreadable dirents.""" dir_url = self._geturl(path) path_parts = _path_parts(path) if path: key = str(rev) + '/' + path else: key = str(rev) # Ensure that the cache gets filled... dirents_locks = self._dirent_cache.get(key) if not dirents_locks: tmp_dirents, locks = list_directory(dir_url, _rev2optrev(rev), _rev2optrev(rev), 0, self.ctx) dirents = {} for name, dirent in tmp_dirents.items(): dirent_parts = path_parts + [name] kind = dirent.kind if (kind == core.svn_node_dir or kind == core.svn_node_file) \ and vclib.check_path_access(self, dirent_parts, kind == core.svn_node_dir \ and vclib.DIR or vclib.FILE, rev): lh_rev, c_rev = self._get_last_history_rev(dirent_parts, rev) dirent.created_rev = lh_rev dirents[name] = dirent dirents_locks = [dirents, locks] self._dirent_cache[key] = dirents_locks # ...then return the goodies from the cache. return dirents_locks[0], dirents_locks[1] def _get_last_history_rev(self, path_parts, rev): """Return the a 2-tuple which contains: - the last interesting revision equal to or older than REV in the history of PATH_PARTS. - the created_rev of of PATH_PARTS as of REV.""" path = self._getpath(path_parts) url = self._geturl(self._getpath(path_parts)) optrev = _rev2optrev(rev) # Get the last-changed-rev. revisions = [] def _info_cb(path, info, pool, retval=revisions): revisions.append(info.last_changed_rev) client.svn_client_info(url, optrev, optrev, _info_cb, 0, self.ctx) last_changed_rev = revisions[0] # Now, this object might not have been directly edited since the # last-changed-rev, but it might have been the child of a copy. # To determine this, we'll run a potentially no-op log between # LAST_CHANGED_REV and REV. lc = LogCollector(path, 1, None, None) client_log(url, optrev, _rev2optrev(last_changed_rev), 1, 1, 0, lc.add_log, self.ctx) revs = lc.logs if revs: revs.sort() return revs[0].number, last_changed_rev else: return last_changed_rev, last_changed_rev def _revinfo_fetch(self, rev, include_changed_paths=0): need_changes = include_changed_paths or self.auth revs = [] def _log_cb(log_entry, pool, retval=revs): # If Subversion happens to call us more than once, we choose not # to care. if retval: return revision = log_entry.revision msg, author, date, revprops = _split_revprops(log_entry.revprops) action_map = { 'D' : vclib.DELETED, 'A' : vclib.ADDED, 'R' : vclib.REPLACED, 'M' : vclib.MODIFIED, } # Easy out: if we won't use the changed-path info, just return a # changes-less tuple. if not need_changes: return revs.append([date, author, msg, revprops, None]) # Subversion 1.5 and earlier didn't offer the 'changed_paths2' # hash, and in Subversion 1.6, it's offered but broken. try: changed_paths = log_entry.changed_paths2 paths = (changed_paths or {}).keys() except: changed_paths = log_entry.changed_paths paths = (changed_paths or {}).keys() paths.sort(lambda a, b: _compare_paths(a, b)) # If we get this far, our caller needs changed-paths, or we need # them for authz-related sanitization. changes = [] found_readable = found_unreadable = 0 for path in paths: change = changed_paths[path] # svn_log_changed_path_t (which we might get instead of the # svn_log_changed_path2_t we'd prefer) doesn't have the # 'node_kind' member. pathtype = None if hasattr(change, 'node_kind'): if change.node_kind == core.svn_node_dir: pathtype = vclib.DIR elif change.node_kind == core.svn_node_file: pathtype = vclib.FILE # svn_log_changed_path2_t only has the 'text_modified' and # 'props_modified' bits in Subversion 1.7 and beyond. And # svn_log_changed_path_t is without. text_modified = props_modified = 0 if hasattr(change, 'text_modified'): if change.text_modified == core.svn_tristate_true: text_modified = 1 if hasattr(change, 'props_modified'): if change.props_modified == core.svn_tristate_true: props_modified = 1 # Wrong, diddily wrong wrong wrong. Can you say, # "Manufacturing data left and right because it hurts to # figure out the right stuff?" action = action_map.get(change.action, vclib.MODIFIED) if change.copyfrom_path and change.copyfrom_rev: is_copy = 1 base_path = change.copyfrom_path base_rev = change.copyfrom_rev elif action == vclib.ADDED or action == vclib.REPLACED: is_copy = 0 base_path = base_rev = None else: is_copy = 0 base_path = path base_rev = revision - 1 # Check authz rules (sadly, we have to lie about the path type) parts = _path_parts(path) if vclib.check_path_access(self, parts, vclib.FILE, revision): if is_copy and base_path and (base_path != path): parts = _path_parts(base_path) if not vclib.check_path_access(self, parts, vclib.FILE, base_rev): is_copy = 0 base_path = None base_rev = None found_unreadable = 1 changes.append(SVNChangedPath(path, revision, pathtype, base_path, base_rev, action, is_copy, text_modified, props_modified)) found_readable = 1 else: found_unreadable = 1 # If our caller doesn't want changed-path stuff, and we have # the info we need to make an authz determination already, # quit this loop and get on with it. if (not include_changed_paths) and found_unreadable and found_readable: break # Filter unreadable information. if found_unreadable: msg = None if not found_readable: author = None date = None # Drop unrequested changes. if not include_changed_paths: changes = None # Add this revision information to the "return" array. retval.append([date, author, msg, revprops, changes]) optrev = _rev2optrev(rev) client_log(self.rootpath, optrev, optrev, 1, need_changes, 0, _log_cb, self.ctx) return tuple(revs[0]) def _revinfo(self, rev, include_changed_paths=0): """Internal-use, cache-friendly revision information harvester.""" # Consult the revinfo cache first. If we don't have cached info, # or our caller wants changed paths and we don't have those for # this revision, go do the real work. rev = self._getrev(rev) cached_info = self._revinfo_cache.get(rev) if not cached_info \ or (include_changed_paths and cached_info[4] is None): cached_info = self._revinfo_fetch(rev, include_changed_paths) self._revinfo_cache[rev] = cached_info return cached_info ##--- custom --## def get_youngest_revision(self): return self.youngest def get_location(self, path, rev, old_rev): try: results = ra.get_locations(self.ra_session, path, rev, [old_rev]) except core.SubversionException, e: _fix_subversion_exception(e) if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: raise vclib.ItemNotFound(path) raise try: old_path = results[old_rev] except KeyError: raise vclib.ItemNotFound(path) old_path = _cleanup_path(old_path) old_path_parts = _path_parts(old_path) # Check access (lying about path types) if not vclib.check_path_access(self, old_path_parts, vclib.FILE, old_rev): raise vclib.ItemNotFound(path) return old_path def created_rev(self, path, rev): lh_rev, c_rev = self._get_last_history_rev(_path_parts(path), rev) return lh_rev def last_rev(self, path, peg_revision, limit_revision=None): """Given PATH, known to exist in PEG_REVISION, find the youngest revision older than, or equal to, LIMIT_REVISION in which path exists. Return that revision, and the path at which PATH exists in that revision.""" # Here's the plan, man. In the trivial case (where PEG_REVISION is # the same as LIMIT_REVISION), this is a no-brainer. If # LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's # history tracing code to find the right location. If, however, # LIMIT_REVISION is younger than PEG_REVISION, we suffer from # Subversion's lack of forward history searching. Our workaround, # ugly as it may be, involves a binary search through the revisions # between PEG_REVISION and LIMIT_REVISION to find our last live # revision. peg_revision = self._getrev(peg_revision) limit_revision = self._getrev(limit_revision) if peg_revision == limit_revision: return peg_revision, path elif peg_revision > limit_revision: path = self.get_location(path, peg_revision, limit_revision) return limit_revision, path else: direction = 1 while peg_revision != limit_revision: mid = (peg_revision + 1 + limit_revision) / 2 try: path = self.get_location(path, peg_revision, mid) except vclib.ItemNotFound: limit_revision = mid - 1 else: peg_revision = mid return peg_revision, path def get_symlink_target(self, path_parts, rev): """Return the target of the symbolic link versioned at PATH_PARTS in REV, or None if that object is not a symlink.""" path = self._getpath(path_parts) path_type = self.itemtype(path_parts, rev) # does auth-check rev = self._getrev(rev) url = self._geturl(path) # Symlinks must be files with the svn:special property set on them # and with file contents which read "link SOME_PATH". if path_type != vclib.FILE: return None pairs = client.svn_client_proplist2(url, _rev2optrev(rev), _rev2optrev(rev), 0, self.ctx) props = pairs and pairs[0][1] or {} if not props.has_key(core.SVN_PROP_SPECIAL): return None pathspec = '' ### FIXME: We're being a touch sloppy here, first by grabbing the ### whole file and then by checking only the first line ### of it. fp = SelfCleanFP(cat_to_tempfile(self, path, rev)) pathspec = fp.readline() fp.close() if pathspec[:5] != 'link ': return None return pathspec[5:] viewvc-1.1.22/lib/vclib/ccvs/0000755000175000017500000000000012265242270016107 5ustar cmpilatocmpilatoviewvc-1.1.22/lib/vclib/ccvs/bincvs.py0000644000175000017500000011762312133055421017751 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- "Version Control lib driver for locally accessible cvs-repositories." import vclib import vcauth import os import os.path import sys import stat import string import re import time # ViewVC libs import compat import popen class BaseCVSRepository(vclib.Repository): def __init__(self, name, rootpath, authorizer, utilities): if not os.path.isdir(rootpath): raise vclib.ReposNotFound(name) self.name = name self.rootpath = rootpath self.auth = authorizer self.utilities = utilities # See if this repository is even viewable, authz-wise. if not vclib.check_root_access(self): raise vclib.ReposNotFound(name) def open(self): # See if a universal read access determination can be made. if self.auth and self.auth.check_universal_access(self.name) == 1: self.auth = None def rootname(self): return self.name def rootpath(self): return self.rootpath def roottype(self): return vclib.CVS def authorizer(self): return self.auth def itemtype(self, path_parts, rev): basepath = self._getpath(path_parts) kind = None if os.path.isdir(basepath): kind = vclib.DIR elif os.path.isfile(basepath + ',v'): kind = vclib.FILE else: atticpath = self._getpath(self._atticpath(path_parts)) if os.path.isfile(atticpath + ',v'): kind = vclib.FILE if not kind: raise vclib.ItemNotFound(path_parts) if not vclib.check_path_access(self, path_parts, kind, rev): raise vclib.ItemNotFound(path_parts) return kind def itemprops(self, path_parts, rev): self.itemtype(path_parts, rev) # does auth-check return {} # CVS doesn't support properties def listdir(self, path_parts, rev, options): if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check raise vclib.Error("Path '%s' is not a directory." % (string.join(path_parts, "/"))) # Only RCS files (*,v) and subdirs are returned. data = [ ] full_name = self._getpath(path_parts) for file in os.listdir(full_name): name = None kind, errors = _check_path(os.path.join(full_name, file)) if kind == vclib.FILE: if file[-2:] == ',v': name = file[:-2] elif kind == vclib.DIR: if file != 'Attic' and file != 'CVS': # CVS directory is for fileattr name = file else: name = file if not name: continue if vclib.check_path_access(self, path_parts + [name], kind, rev): data.append(CVSDirEntry(name, kind, errors, 0)) full_name = os.path.join(full_name, 'Attic') if os.path.isdir(full_name): for file in os.listdir(full_name): name = None kind, errors = _check_path(os.path.join(full_name, file)) if kind == vclib.FILE: if file[-2:] == ',v': name = file[:-2] elif kind != vclib.DIR: name = file if not name: continue if vclib.check_path_access(self, path_parts + [name], kind, rev): data.append(CVSDirEntry(name, kind, errors, 1)) return data def _getpath(self, path_parts): return apply(os.path.join, (self.rootpath,) + tuple(path_parts)) def _atticpath(self, path_parts): return path_parts[:-1] + ['Attic'] + path_parts[-1:] def rcsfile(self, path_parts, root=0, v=1): "Return path to RCS file" ret_parts = path_parts ret_file = self._getpath(ret_parts) if not os.path.isfile(ret_file + ',v'): ret_parts = self._atticpath(path_parts) ret_file = self._getpath(ret_parts) if not os.path.isfile(ret_file + ',v'): raise vclib.ItemNotFound(path_parts) if root: ret = ret_file else: ret = string.join(ret_parts, "/") if v: ret = ret + ",v" return ret def isexecutable(self, path_parts, rev): if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (string.join(path_parts, "/"))) rcsfile = self.rcsfile(path_parts, 1) return os.access(rcsfile, os.X_OK) def filesize(self, path_parts, rev): if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) return -1 class BinCVSRepository(BaseCVSRepository): def _get_tip_revision(self, rcs_file, rev=None): """Get the (basically) youngest revision (filtered by REV).""" args = rcs_file, fp = self.rcs_popen('rlog', args, 'rt', 0) filename, default_branch, tags, lockinfo, msg, eof = _parse_log_header(fp) revs = [] while not eof: revision, eof = _parse_log_entry(fp) if revision: revs.append(revision) revs = _file_log(revs, tags, lockinfo, default_branch, rev) if revs: return revs[-1] return None def openfile(self, path_parts, rev, options): """see vclib.Repository.openfile docstring Option values recognized by this implementation: cvs_oldkeywords boolean. true to use the original keyword substitution values. """ if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (string.join(path_parts, "/"))) if not rev or rev == 'HEAD' or rev == 'MAIN': rev_flag = '-p' else: rev_flag = '-p' + rev if options.get('cvs_oldkeywords', 0): kv_flag = '-ko' else: kv_flag = '-kkv' full_name = self.rcsfile(path_parts, root=1, v=0) used_rlog = 0 tip_rev = None # used only if we have to fallback to using rlog fp = self.rcs_popen('co', (kv_flag, rev_flag, full_name), 'rb') try: filename, revision = _parse_co_header(fp) except COMissingRevision: # We got a "revision X.Y.Z absent" error from co. This could be # because we were asked to find a tip of a branch, which co # doesn't seem to handle. So we do rlog-gy stuff to figure out # which revision the tip of the branch currently maps to. ### TODO: Only do this when 'rev' is a branch symbol name? if not used_rlog: tip_rev = self._get_tip_revision(full_name + ',v', rev) used_rlog = 1 if not tip_rev: raise vclib.Error("Unable to find valid revision") fp = self.rcs_popen('co', ('-p' + tip_rev.string, full_name), 'rb') filename, revision = _parse_co_header(fp) if filename is None: # CVSNT's co exits without any output if a dead revision is requested. # Bug at http://www.cvsnt.org/cgi-bin/bugzilla/show_bug.cgi?id=190 # As a workaround, we invoke rlog to find the first non-dead revision # that precedes it and check out that revision instead. Of course, # if we've already invoked rlog above, we just reuse its output. if not used_rlog: tip_rev = self._get_tip_revision(full_name + ',v', rev) used_rlog = 1 if not (tip_rev and tip_rev.undead): raise vclib.Error( 'Could not find non-dead revision preceding "%s"' % rev) fp = self.rcs_popen('co', ('-p' + tip_rev.undead.string, full_name), 'rb') filename, revision = _parse_co_header(fp) if filename is None: raise vclib.Error('Missing output from co (filename = "%s")' % full_name) if not _paths_eq(filename, full_name): raise vclib.Error( 'The filename from co ("%s") did not match (expected "%s")' % (filename, full_name)) return fp, revision def dirlogs(self, path_parts, rev, entries, options): """see vclib.Repository.dirlogs docstring rev can be a tag name or None. if set only information from revisions matching the tag will be retrieved Option values recognized by this implementation: cvs_subdirs boolean. true to fetch logs of the most recently modified file in each subdirectory Option values returned by this implementation: cvs_tags, cvs_branches lists of tag and branch names encountered in the directory """ if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check raise vclib.Error("Path '%s' is not a directory." % (string.join(path_parts, "/"))) subdirs = options.get('cvs_subdirs', 0) entries_to_fetch = [] for entry in entries: if vclib.check_path_access(self, path_parts + [entry.name], None, rev): entries_to_fetch.append(entry) alltags = _get_logs(self, path_parts, entries_to_fetch, rev, subdirs) branches = options['cvs_branches'] = [] tags = options['cvs_tags'] = [] for name, rev in alltags.items(): if Tag(None, rev).is_branch: branches.append(name) else: tags.append(name) def itemlog(self, path_parts, rev, sortby, first, limit, options): """see vclib.Repository.itemlog docstring rev parameter can be a revision number, a branch number, a tag name, or None. If None, will return information about all revisions, otherwise, will only return information about the specified revision or branch. Option values recognized by this implementation: cvs_pass_rev boolean, default false. set to true to pass rev parameter as -r argument to rlog, this is more efficient but causes less information to be returned Option values returned by this implementation: cvs_tags dictionary of Tag objects for all tags encountered """ if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (string.join(path_parts, "/"))) # Invoke rlog rcsfile = self.rcsfile(path_parts, 1) if rev and options.get('cvs_pass_rev', 0): args = '-r' + rev, rcsfile else: args = rcsfile, fp = self.rcs_popen('rlog', args, 'rt', 0) filename, default_branch, tags, lockinfo, msg, eof = _parse_log_header(fp) # Retrieve revision objects revs = [] while not eof: revision, eof = _parse_log_entry(fp) if revision: revs.append(revision) filtered_revs = _file_log(revs, tags, lockinfo, default_branch, rev) options['cvs_tags'] = tags if sortby == vclib.SORTBY_DATE: filtered_revs.sort(_logsort_date_cmp) elif sortby == vclib.SORTBY_REV: filtered_revs.sort(_logsort_rev_cmp) if len(filtered_revs) < first: return [] if limit: return filtered_revs[first:first+limit] return filtered_revs def rcs_popen(self, rcs_cmd, rcs_args, mode, capture_err=1): if self.utilities.cvsnt: cmd = self.utilities.cvsnt args = ['rcsfile', rcs_cmd] args.extend(list(rcs_args)) else: cmd = os.path.join(self.utilities.rcs_dir, rcs_cmd) args = rcs_args return popen.popen(cmd, args, mode, capture_err) def annotate(self, path_parts, rev=None, include_text=False): if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (string.join(path_parts, "/"))) from vclib.ccvs import blame source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, include_text) return source, source.revision def revinfo(self, rev): raise vclib.UnsupportedFeature def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): """see vclib.Repository.rawdiff docstring Option values recognized by this implementation: ignore_keyword_subst - boolean, ignore keyword substitution """ if self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (string.join(path_parts1, "/"))) if self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (string.join(path_parts2, "/"))) args = vclib._diff_args(type, options) if options.get('ignore_keyword_subst', 0): args.append('-kk') rcsfile = self.rcsfile(path_parts1, 1) if path_parts1 != path_parts2: raise NotImplementedError, "cannot diff across paths in cvs" args.extend(['-r' + rev1, '-r' + rev2, rcsfile]) fp = self.rcs_popen('rcsdiff', args, 'rt') # Eat up the non-GNU-diff-y headers. while 1: line = fp.readline() if not line or line[0:5] == 'diff ': break return fp class CVSDirEntry(vclib.DirEntry): def __init__(self, name, kind, errors, in_attic, absent=0): vclib.DirEntry.__init__(self, name, kind, errors) self.in_attic = in_attic self.absent = absent # meaning, no revisions found on requested tag class Revision(vclib.Revision): def __init__(self, revstr, date=None, author=None, dead=None, changed=None, log=None): vclib.Revision.__init__(self, _revision_tuple(revstr), revstr, date, author, changed, log, None, None) self.dead = dead class Tag: def __init__(self, name, revstr): self.name = name self.number = _tag_tuple(revstr) self.is_branch = len(self.number) % 2 == 1 or not self.number # ====================================================================== # Functions for dealing with Revision and Tag objects def _logsort_date_cmp(rev1, rev2): # sort on date; secondary on revision number return -cmp(rev1.date, rev2.date) or -cmp(rev1.number, rev2.number) def _logsort_rev_cmp(rev1, rev2): # sort highest revision first return -cmp(rev1.number, rev2.number) def _match_revs_tags(revlist, taglist): """Match up a list of Revision objects with a list of Tag objects Sets the following properties on each Revision in revlist: "tags" list of non-branch tags which refer to this revision example: if revision is 1.2.3.4, tags is a list of all 1.2.3.4 tags "branches" list of branch tags which refer to this revision's branch example: if revision is 1.2.3.4, branches is a list of all 1.2.3 tags "branch_points" list of branch tags which branch off of this revision example: if revision is 1.2, it's a list of tags like 1.2.3 and 1.2.4 "prev" reference to the previous revision, possibly None example: if revision is 1.2.3.4, prev is 1.2.3.3 "next" reference to next revision, possibly None example: if revision is 1.2.3.4, next is 1.2.3.5 "parent" reference to revision this one branches off of, possibly None example: if revision is 1.2.3.4, parent is 1.2 "undead" If the revision is dead, then this is a reference to the first previous revision which isn't dead, otherwise it's a reference to itself. If all the previous revisions are dead it's None. "branch_number" tuple representing branch number or empty tuple if on trunk example: if revision is 1.2.3.4, branch_number is (1, 2, 3) Each tag in taglist gets these properties set: "co_rev" reference to revision that would be retrieved if tag were checked out "branch_rev" reference to revision branched off of, only set for branch tags example: if tag is 1.2.3, branch_rev points to 1.2 revision "aliases" list of tags that have the same number """ # map of branch numbers to lists of corresponding branch Tags branch_dict = {} # map of revision numbers to lists of non-branch Tags tag_dict = {} # map of revision numbers to lists of branch Tags branch_point_dict = {} # toss tags into "branch_dict", "tag_dict", and "branch_point_dict" # set "aliases" property and default "co_rev" and "branch_rev" values for tag in taglist: tag.co_rev = None if tag.is_branch: tag.branch_rev = None _dict_list_add(branch_point_dict, tag.number[:-1], tag) tag.aliases = _dict_list_add(branch_dict, tag.number, tag) else: tag.aliases = _dict_list_add(tag_dict, tag.number, tag) # sort the revisions so the loop below can work properly revlist.sort() # array of the most recently encountered revision objects indexed by depth history = [] # loop through revisions, setting properties and storing state in "history" for rev in revlist: depth = len(rev.number) / 2 - 1 # set "prev" and "next" properties rev.prev = rev.next = None if depth < len(history): prev = history[depth] if prev and (depth == 0 or rev.number[:-1] == prev.number[:-1]): rev.prev = prev prev.next = rev # set "parent" rev.parent = None if depth and depth <= len(history): parent = history[depth-1] if parent and parent.number == rev.number[:-2]: rev.parent = history[depth-1] # set "undead" if rev.dead: prev = rev.prev or rev.parent rev.undead = prev and prev.undead else: rev.undead = rev # set "tags" and "branch_points" rev.tags = tag_dict.get(rev.number, []) rev.branch_points = branch_point_dict.get(rev.number, []) # set "branches" and "branch_number" if rev.prev: rev.branches = rev.prev.branches rev.branch_number = rev.prev.branch_number else: rev.branch_number = depth and rev.number[:-1] or () try: rev.branches = branch_dict[rev.branch_number] except KeyError: rev.branches = [] # set "co_rev" and "branch_rev" for tag in rev.tags: tag.co_rev = rev for tag in rev.branch_points: tag.co_rev = rev tag.branch_rev = rev # This loop only needs to be run for revisions at the heads of branches, # but for the simplicity's sake, it actually runs for every revision on # a branch. The later revisions overwrite values set by the earlier ones. for branch in rev.branches: branch.co_rev = rev # end of outer loop, store most recent revision in "history" array while len(history) <= depth: history.append(None) history[depth] = rev def _add_tag(tag_name, revision): """Create a new tag object and associate it with a revision""" if revision: tag = Tag(tag_name, revision.string) tag.aliases = revision.tags revision.tags.append(tag) else: tag = Tag(tag_name, None) tag.aliases = [] tag.co_rev = revision tag.is_branch = 0 return tag def _remove_tag(tag): """Remove a tag's associations""" tag.aliases.remove(tag) if tag.is_branch and tag.branch_rev: tag.branch_rev.branch_points.remove(tag) def _revision_tuple(revision_string): """convert a revision number into a tuple of integers""" t = tuple(map(int, string.split(revision_string, '.'))) if len(t) % 2 == 0: return t raise ValueError def _tag_tuple(revision_string): """convert a revision number or branch number into a tuple of integers""" if revision_string: t = map(int, string.split(revision_string, '.')) l = len(t) if l == 1: return () if l > 2 and t[-2] == 0 and l % 2 == 0: del t[-2] return tuple(t) return () def _dict_list_add(dict, idx, elem): try: list = dict[idx] except KeyError: list = dict[idx] = [elem] else: list.append(elem) return list # ====================================================================== # Functions for parsing output from RCS utilities class COMalformedOutput(vclib.Error): pass class COMissingRevision(vclib.Error): pass ### suck up other warnings in _re_co_warning? _re_co_filename = re.compile(r'^(.*),v\s+-->\s+(?:(?:standard output)|(?:stdout))\s*\n?$') _re_co_warning = re.compile(r'^.*co: .*,v: warning: Unknown phrases like .*\n$') _re_co_missing_rev = re.compile(r'^.*co: .*,v: revision.*absent\n$') _re_co_side_branches = re.compile(r'^.*co: .*,v: no side branches present for [\d\.]+\n$') _re_co_revision = re.compile(r'^revision\s+([\d\.]+)\s*\n$') def _parse_co_header(fp): """Parse RCS co header. fp is a file (pipe) opened for reading the co standard error stream. Returns: (filename, revision) or (None, None) if output is empty """ # header from co: # #/home/cvsroot/mod_dav/dav_shared_stub.c,v --> standard output #revision 1.1 # # Sometimes, the following line might occur at line 2: #co: INSTALL,v: warning: Unknown phrases like `permissions ...;' are present. # parse the output header filename = None # look for a filename in the first line (if there is a first line). line = fp.readline() if not line: return None, None match = _re_co_filename.match(line) if not match: raise COMalformedOutput, "Unable to find filename in co output stream" filename = match.group(1) # look through subsequent lines for a revision. we might encounter # some ignorable or problematic lines along the way. while 1: line = fp.readline() if not line: break # look for a revision. match = _re_co_revision.match(line) if match: return filename, match.group(1) elif _re_co_missing_rev.match(line) or _re_co_side_branches.match(line): raise COMissingRevision, "Got missing revision error from co output stream" elif _re_co_warning.match(line): pass else: break raise COMalformedOutput, "Unable to find revision in co output stream" # if your rlog doesn't use 77 '=' characters, then this must change LOG_END_MARKER = '=' * 77 + '\n' ENTRY_END_MARKER = '-' * 28 + '\n' _EOF_FILE = 'end of file entries' # no more entries for this RCS file _EOF_LOG = 'end of log' # hit the true EOF on the pipe _EOF_ERROR = 'error message found' # rlog issued an error # rlog error messages look like # # rlog: filename/goes/here,v: error message # rlog: filename/goes/here,v:123: error message # # so we should be able to match them with a regex like # # ^rlog\: (.*)(?:\:\d+)?\: (.*)$ # # But for some reason the windows version of rlog omits the "rlog: " prefix # for the first error message when the standard error stream has been # redirected to a file or pipe. (the prefix is present in subsequent errors # and when rlog is run from the console). So the expression below is more # complicated _re_log_error = re.compile(r'^(?:rlog\: )*(.*,v)(?:\:\d+)?\: (.*)$') # CVSNT error messages look like: # cvs rcsfile: `C:/path/to/file,v' does not appear to be a valid rcs file # cvs [rcsfile aborted]: C:/path/to/file,v: No such file or directory # cvs [rcsfile aborted]: cannot open C:/path/to/file,v: Permission denied _re_cvsnt_error = re.compile(r'^(?:cvs rcsfile\: |cvs \[rcsfile aborted\]: )' r'(?:\`(.*,v)\' |cannot open (.*,v)\: |(.*,v)\: |)' r'(.*)$') def _parse_log_header(fp): """Parse and RCS/CVS log header. fp is a file (pipe) opened for reading the log information. On entry, fp should point to the start of a log entry. On exit, fp will have consumed the separator line between the header and the first revision log. If there is no revision information (e.g. the "-h" switch was passed to rlog), then fp will consumed the file separator line on exit. Returns: filename, default branch, tag dictionary, lock dictionary, rlog error message, and eof flag """ filename = head = branch = msg = "" taginfo = { } # tag name => number lockinfo = { } # revision => locker state = 0 # 0 = base, 1 = parsing symbols, 2 = parsing locks eof = None while 1: line = fp.readline() if not line: # the true end-of-file eof = _EOF_LOG break if state == 1: if line[0] == '\t': [ tag, rev ] = map(string.strip, string.split(line, ':')) taginfo[tag] = rev else: # oops. this line isn't tag info. stop parsing tags. state = 0 if state == 2: if line[0] == '\t': [ locker, rev ] = map(string.strip, string.split(line, ':')) lockinfo[rev] = locker else: # oops. this line isn't lock info. stop parsing tags. state = 0 if state == 0: if line[:9] == 'RCS file:': filename = line[10:-1] elif line[:5] == 'head:': head = line[6:-1] elif line[:7] == 'branch:': branch = line[8:-1] elif line[:6] == 'locks:': # start parsing the lock information state = 2 elif line[:14] == 'symbolic names': # start parsing the tag information state = 1 elif line == ENTRY_END_MARKER: # end of the headers break elif line == LOG_END_MARKER: # end of this file's log information eof = _EOF_FILE break else: error = _re_cvsnt_error.match(line) if error: p1, p2, p3, msg = error.groups() filename = p1 or p2 or p3 if not filename: raise vclib.Error("Could not get filename from CVSNT error:\n%s" % line) eof = _EOF_ERROR break error = _re_log_error.match(line) if error: filename, msg = error.groups() if msg[:30] == 'warning: Unknown phrases like ': # don't worry about this warning. it can happen with some RCS # files that have unknown fields in them (e.g. "permissions 644;" continue eof = _EOF_ERROR break return filename, branch, taginfo, lockinfo, msg, eof _re_log_info = re.compile(r'^date:\s+([^;]+);' r'\s+author:\s+([^;]+);' r'\s+state:\s+([^;]+);' r'(\s+lines:\s+([0-9\s+-]+);?)?' r'(\s+commitid:\s+([a-zA-Z0-9]+))?\n$') ### _re_rev should be updated to extract the "locked" flag _re_rev = re.compile(r'^revision\s+([0-9.]+).*') def _parse_log_entry(fp): """Parse a single log entry. On entry, fp should point to the first line of the entry (the "revision" line). On exit, fp will have consumed the log separator line (dashes) or the end-of-file marker (equals). Returns: Revision object and eof flag (see _EOF_*) """ rev = None line = fp.readline() if not line: return None, _EOF_LOG if line == LOG_END_MARKER: # Needed because some versions of RCS precede LOG_END_MARKER # with ENTRY_END_MARKER return None, _EOF_FILE if line[:8] == 'revision': match = _re_rev.match(line) if not match: return None, _EOF_LOG rev = match.group(1) line = fp.readline() if not line: return None, _EOF_LOG match = _re_log_info.match(line) eof = None log = '' while 1: line = fp.readline() if not line: # true end-of-file eof = _EOF_LOG break if line[:9] == 'branches:': continue if line == ENTRY_END_MARKER: break if line == LOG_END_MARKER: # end of this file's log information eof = _EOF_FILE break log = log + line if not rev or not match: # there was a parsing error return None, eof # parse out a time tuple for the local time tm = compat.cvs_strptime(match.group(1)) # rlog seems to assume that two-digit years are 1900-based (so, "04" # comes out as "1904", not "2004"). EPOCH = 1970 if tm[0] < EPOCH: tm = list(tm) if (tm[0] - 1900) < 70: tm[0] = tm[0] + 100 if tm[0] < EPOCH: raise ValueError, 'invalid year' date = compat.timegm(tm) return Revision(rev, date, # author, state, lines changed match.group(2), match.group(3) == "dead", match.group(5), log), eof def _skip_file(fp): "Skip the rest of a file's log information." while 1: line = fp.readline() if not line: break if line == LOG_END_MARKER: break def _paths_eq(path1, path2): "See if two path strings are the same" # This function is neccessary because CVSNT (since version 2.0.29) # converts paths passed as arguments to use upper case drive # letter and forward slashes return os.path.normcase(path1) == os.path.normcase(path2) # ====================================================================== # Functions for interpreting and manipulating log information def _file_log(revs, taginfo, lockinfo, cur_branch, filter): """Augment list of Revisions and a dictionary of Tags""" # Add artificial ViewVC tag MAIN. If the file has a default branch, then # MAIN acts like a branch tag pointing to that branch. Otherwise MAIN acts # like a branch tag that points to the trunk. (Note: A default branch is # just a branch number specified in an RCS file that tells CVS and RCS # what branch to use for checkout and update operations by default, when # there's no revision argument or sticky branch to override it. Default # branches get set by "cvs import" to point to newly created vendor # branches. Sometimes they are also set manually with "cvs admin -b") taginfo['MAIN'] = cur_branch # Create tag objects for name, num in taginfo.items(): taginfo[name] = Tag(name, num) tags = taginfo.values() # Set view_tag to a Tag object in order to filter results. We can filter by # revision number or branch number if filter: try: view_tag = Tag(None, filter) except ValueError: view_tag = None else: tags.append(view_tag) # Match up tags and revisions _match_revs_tags(revs, tags) # Match up lockinfo and revision for rev in revs: rev.lockinfo = lockinfo.get(rev.string) # Add artificial ViewVC tag HEAD, which acts like a non-branch tag pointing # at the latest revision on the MAIN branch. The HEAD revision doesn't have # anything to do with the "head" revision number specified in the RCS file # and in rlog output. HEAD refers to the revision that the CVS and RCS co # commands will check out by default, whereas the "head" field just refers # to the highest revision on the trunk. taginfo['HEAD'] = _add_tag('HEAD', taginfo['MAIN'].co_rev) # Determine what revisions to return if filter: # If view_tag isn't set, it means filter is not a valid revision or # branch number. Check taginfo to see if filter is set to a valid tag # name. If so, filter by that tag, otherwise raise an error. if not view_tag: try: view_tag = taginfo[filter] except KeyError: raise vclib.Error('Invalid tag or revision number "%s"' % filter) filtered_revs = [ ] # only include revisions on the tag branch or it's parent branches if view_tag.is_branch: branch = view_tag.number elif len(view_tag.number) > 2: branch = view_tag.number[:-1] else: branch = () # for a normal tag, include all tag revision and all preceding revisions. # for a branch tag, include revisions on branch, branch point revision, # and all preceding revisions for rev in revs: if (rev.number == view_tag.number or rev.branch_number == view_tag.number or (rev.number < view_tag.number and rev.branch_number == branch[:len(rev.branch_number)])): filtered_revs.append(rev) # get rid of the view_tag if it was only created for filtering if view_tag.name is None: _remove_tag(view_tag) else: filtered_revs = revs return filtered_revs def _get_logs(repos, dir_path_parts, entries, view_tag, get_dirs): alltags = { # all the tags seen in the files of this dir 'MAIN' : '', 'HEAD' : '1.1' } entries_idx = 0 entries_len = len(entries) max_args = 100 while 1: chunk = [] while len(chunk) < max_args and entries_idx < entries_len: entry = entries[entries_idx] path = _log_path(entry, repos._getpath(dir_path_parts), get_dirs) if path: entry.path = path entry.idx = entries_idx chunk.append(entry) # set properties even if we don't retrieve logs entry.rev = entry.date = entry.author = None entry.dead = entry.log = entry.lockinfo = None entries_idx = entries_idx + 1 if not chunk: return alltags args = [] if not view_tag: # NOTE: can't pass tag on command line since a tag may contain "-" # we'll search the output for the appropriate revision # fetch the latest revision on the default branch args.append('-r') args.extend(map(lambda x: x.path, chunk)) rlog = repos.rcs_popen('rlog', args, 'rt') # consume each file found in the resulting log chunk_idx = 0 while chunk_idx < len(chunk): file = chunk[chunk_idx] filename, default_branch, taginfo, lockinfo, msg, eof \ = _parse_log_header(rlog) if eof == _EOF_LOG: # the rlog output ended early. this can happen on errors that rlog # thinks are so serious that it stops parsing the current file and # refuses to parse any of the files that come after it. one of the # errors that triggers this obnoxious behavior looks like: # # rlog: c:\cvsroot\dir\file,v:8: unknown expand mode u # rlog aborted # if current file has errors, restart on the next one if file.errors: chunk_idx = chunk_idx + 1 if chunk_idx < len(chunk): entries_idx = chunk[chunk_idx].idx break # otherwise just error out raise vclib.Error('Rlog output ended early. Expected RCS file "%s"' % file.path) # if rlog filename doesn't match current file and we already have an # error message about this file, move on to the next file while not (file and _paths_eq(file.path, filename)): if file and file.errors: chunk_idx = chunk_idx + 1 file = chunk_idx < len(chunk) and chunk[chunk_idx] or None continue raise vclib.Error('Error parsing rlog output. Expected RCS file %s' ', found %s' % (file and file.path, filename)) # if we get an rlog error message, restart loop without advancing # chunk_idx cause there might be more output about the same file if eof == _EOF_ERROR: file.errors.append("rlog error: %s" % msg) continue tag = None if view_tag == 'MAIN' or view_tag == 'HEAD': tag = Tag(None, default_branch) elif taginfo.has_key(view_tag): tag = Tag(None, taginfo[view_tag]) elif view_tag and (eof != _EOF_FILE): # the tag wasn't found, so skip this file (unless we already # know there's nothing left of it to read) _skip_file(rlog) eof = _EOF_FILE # we don't care about the specific values -- just the keys and whether # the values point to branches or revisions. this the fastest way to # merge the set of keys and keep values that allow us to make the # distinction between branch tags and normal tags alltags.update(taginfo) # read all of the log entries until we find the revision we want wanted_entry = None while not eof: # fetch one of the log entries entry, eof = _parse_log_entry(rlog) if not entry: # parsing error break # A perfect match is a revision on the branch being viewed or # a revision having the tag being viewed or any revision # when nothing is being viewed. When there's a perfect match # we set the wanted_entry value and break out of the loop. # An imperfect match is a revision at the branch point of a # branch being viewed. When there's an imperfect match we # also set the wanted_entry value but keep looping in case # something better comes along. perfect = not tag or entry.number == tag.number or \ (len(entry.number) == 2 and not tag.number) or \ entry.number[:-1] == tag.number if perfect or entry.number == tag.number[:-1]: wanted_entry = entry if perfect: break if wanted_entry: file.rev = wanted_entry.string file.date = wanted_entry.date file.author = wanted_entry.author file.dead = file.kind == vclib.FILE and wanted_entry.dead file.absent = 0 file.log = wanted_entry.log file.lockinfo = lockinfo.get(file.rev) # suppress rlog errors if we find a usable revision in the end del file.errors[:] elif file.kind == vclib.FILE: file.dead = 0 #file.errors.append("No revisions exist on %s" % (view_tag or "MAIN")) file.absent = 1 # done with this file now, skip the rest of this file's revisions if not eof: _skip_file(rlog) # end of while loop, advance index chunk_idx = chunk_idx + 1 rlog.close() def _log_path(entry, dirpath, getdirs): path = name = None if not entry.errors: if entry.kind == vclib.FILE: path = entry.in_attic and 'Attic' or '' name = entry.name elif entry.kind == vclib.DIR and getdirs: entry.newest_file = _newest_file(os.path.join(dirpath, entry.name)) if entry.newest_file: path = entry.name name = entry.newest_file if name: return os.path.join(dirpath, path, name + ',v') return None # ====================================================================== # Functions for dealing with the filesystem if sys.platform == "win32": def _check_path(path): kind = None errors = [] if os.path.isfile(path): kind = vclib.FILE elif os.path.isdir(path): kind = vclib.DIR else: errors.append("error: path is not a file or directory") if not os.access(path, os.R_OK): errors.append("error: path is not accessible") return kind, errors else: _uid = os.getuid() _gid = os.getgid() def _check_path(pathname): try: info = os.stat(pathname) except os.error, e: return None, ["stat error: %s" % e] kind = None errors = [] mode = info[stat.ST_MODE] isdir = stat.S_ISDIR(mode) isreg = stat.S_ISREG(mode) if isreg or isdir: # # Quick version of access() where we use existing stat() data. # # This might not be perfect -- the OS may return slightly different # results for some bizarre reason. However, we make a good show of # "can I read this file/dir?" by checking the various perm bits. # # NOTE: if the UID matches, then we must match the user bits -- we # cannot defer to group or other bits. Similarly, if the GID matches, # then we must have read access in the group bits. # # If the UID or GID don't match, we need to check the # results of an os.access() call, in case the web server process # is in the group that owns the directory. # if isdir: mask = stat.S_IROTH | stat.S_IXOTH else: mask = stat.S_IROTH if info[stat.ST_UID] == _uid: if ((mode >> 6) & mask) != mask: errors.append("error: path is not accessible to user %i" % _uid) elif info[stat.ST_GID] == _gid: if ((mode >> 3) & mask) != mask: errors.append("error: path is not accessible to group %i" % _gid) # If the process running the web server is a member of # the group stat.ST_GID access may be granted. # so the fall back to os.access is needed to figure this out. elif (mode & mask) != mask: if not os.access(pathname, isdir and (os.R_OK | os.X_OK) or os.R_OK): errors.append("error: path is not accessible") if isdir: kind = vclib.DIR else: kind = vclib.FILE else: errors.append("error: path is not a file or directory") return kind, errors def _newest_file(dirpath): """Find the last modified RCS file in a directory""" newest_file = None newest_time = 0 ### FIXME: This sucker is leaking unauthorized paths! ### for subfile in os.listdir(dirpath): ### filter CVS locks? stale NFS handles? if subfile[-2:] != ',v': continue path = os.path.join(dirpath, subfile) info = os.stat(path) if not stat.S_ISREG(info[stat.ST_MODE]): continue if info[stat.ST_MTIME] > newest_time: kind, verboten = _check_path(path) if kind == vclib.FILE and not verboten: newest_file = subfile[:-2] newest_time = info[stat.ST_MTIME] return newest_file viewvc-1.1.22/lib/vclib/ccvs/__init__.py0000644000175000017500000000414512135554663020234 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- import os import os.path def canonicalize_rootpath(rootpath): assert os.path.isabs(rootpath) return os.path.normpath(rootpath) def _is_cvsroot(path): return os.path.exists(os.path.join(path, "CVSROOT", "config")) def expand_root_parent(parent_path): # Each subdirectory of PARENT_PATH that contains a child # "CVSROOT/config" is added the set of returned roots. Or, if the # PARENT_PATH itself contains a child "CVSROOT/config", then all its # subdirectories are returned as roots. assert os.path.isabs(parent_path) roots = {} subpaths = os.listdir(parent_path) for rootname in subpaths: rootpath = os.path.join(parent_path, rootname) if _is_cvsroot(parent_path) or _is_cvsroot(rootpath): roots[rootname] = canonicalize_rootpath(rootpath) return roots def find_root_in_parent(parent_path, rootname): """Search PARENT_PATH for a root named ROOTNAME, returning the canonicalized ROOTPATH of the root if found; return None if no such root is found.""" # Is PARENT_PATH itself a CVS repository? If so, we allow ROOTNAME # to be any subdir within it. Otherwise, we expect # PARENT_PATH/ROOTNAME to be a CVS repository. assert os.path.isabs(parent_path) rootpath = os.path.join(parent_path, rootname) if (_is_cvsroot(parent_path) and os.path.exists(rootpath)) \ or _is_cvsroot(rootpath): return canonicalize_rootpath(rootpath) return None def CVSRepository(name, rootpath, authorizer, utilities, use_rcsparse): rootpath = canonicalize_rootpath(rootpath) if use_rcsparse: import ccvs return ccvs.CCVSRepository(name, rootpath, authorizer, utilities) else: import bincvs return bincvs.BinCVSRepository(name, rootpath, authorizer, utilities) viewvc-1.1.22/lib/vclib/ccvs/ccvs.py0000644000175000017500000003260412112745661017427 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- import os import string import re import cStringIO import tempfile import vclib import rcsparse import blame ### The functionality shared with bincvs should probably be moved to a ### separate module from bincvs import BaseCVSRepository, Revision, Tag, _file_log, _log_path, _logsort_date_cmp, _logsort_rev_cmp class CCVSRepository(BaseCVSRepository): def dirlogs(self, path_parts, rev, entries, options): """see vclib.Repository.dirlogs docstring rev can be a tag name or None. if set only information from revisions matching the tag will be retrieved Option values recognized by this implementation: cvs_subdirs boolean. true to fetch logs of the most recently modified file in each subdirectory Option values returned by this implementation: cvs_tags, cvs_branches lists of tag and branch names encountered in the directory """ if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check raise vclib.Error("Path '%s' is not a directory." % (string.join(path_parts, "/"))) entries_to_fetch = [] for entry in entries: if vclib.check_path_access(self, path_parts + [entry.name], None, rev): entries_to_fetch.append(entry) subdirs = options.get('cvs_subdirs', 0) dirpath = self._getpath(path_parts) alltags = { # all the tags seen in the files of this dir 'MAIN' : '', 'HEAD' : '1.1' } for entry in entries_to_fetch: entry.rev = entry.date = entry.author = None entry.dead = entry.absent = entry.log = entry.lockinfo = None path = _log_path(entry, dirpath, subdirs) if path: entry.path = path try: rcsparse.parse(open(path, 'rb'), InfoSink(entry, rev, alltags)) except IOError, e: entry.errors.append("rcsparse error: %s" % e) except RuntimeError, e: entry.errors.append("rcsparse error: %s" % e) except rcsparse.RCSStopParser: pass branches = options['cvs_branches'] = [] tags = options['cvs_tags'] = [] for name, rev in alltags.items(): if Tag(None, rev).is_branch: branches.append(name) else: tags.append(name) def itemlog(self, path_parts, rev, sortby, first, limit, options): """see vclib.Repository.itemlog docstring rev parameter can be a revision number, a branch number, a tag name, or None. If None, will return information about all revisions, otherwise, will only return information about the specified revision or branch. Option values returned by this implementation: cvs_tags dictionary of Tag objects for all tags encountered """ if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (string.join(path_parts, "/"))) path = self.rcsfile(path_parts, 1) sink = TreeSink() rcsparse.parse(open(path, 'rb'), sink) filtered_revs = _file_log(sink.revs.values(), sink.tags, sink.lockinfo, sink.default_branch, rev) for rev in filtered_revs: if rev.prev and len(rev.number) == 2: rev.changed = rev.prev.next_changed options['cvs_tags'] = sink.tags if sortby == vclib.SORTBY_DATE: filtered_revs.sort(_logsort_date_cmp) elif sortby == vclib.SORTBY_REV: filtered_revs.sort(_logsort_rev_cmp) if len(filtered_revs) < first: return [] if limit: return filtered_revs[first:first+limit] return filtered_revs def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): if self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (string.join(path_parts1, "/"))) if self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (string.join(path_parts2, "/"))) temp1 = tempfile.mktemp() open(temp1, 'wb').write(self.openfile(path_parts1, rev1, {})[0].getvalue()) temp2 = tempfile.mktemp() open(temp2, 'wb').write(self.openfile(path_parts2, rev2, {})[0].getvalue()) r1 = self.itemlog(path_parts1, rev1, vclib.SORTBY_DEFAULT, 0, 0, {})[-1] r2 = self.itemlog(path_parts2, rev2, vclib.SORTBY_DEFAULT, 0, 0, {})[-1] info1 = (self.rcsfile(path_parts1, root=1, v=0), r1.date, r1.string) info2 = (self.rcsfile(path_parts2, root=1, v=0), r2.date, r2.string) diff_args = vclib._diff_args(type, options) return vclib._diff_fp(temp1, temp2, info1, info2, self.utilities.diff or 'diff', diff_args) def annotate(self, path_parts, rev=None, include_text=False): if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (string.join(path_parts, "/"))) source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, include_text) return source, source.revision def revinfo(self, rev): raise vclib.UnsupportedFeature def openfile(self, path_parts, rev, options): if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % (string.join(path_parts, "/"))) path = self.rcsfile(path_parts, 1) sink = COSink(rev) rcsparse.parse(open(path, 'rb'), sink) revision = sink.last and sink.last.string return cStringIO.StringIO(string.join(sink.sstext.text, "\n")), revision class MatchingSink(rcsparse.Sink): """Superclass for sinks that search for revisions based on tag or number""" def __init__(self, find): """Initialize with tag name or revision number string to match against""" if not find or find == 'MAIN' or find == 'HEAD': self.find = None else: self.find = find self.find_tag = None def set_principal_branch(self, branch_number): if self.find is None: self.find_tag = Tag(None, branch_number) def define_tag(self, name, revision): if name == self.find: self.find_tag = Tag(None, revision) def admin_completed(self): if self.find_tag is None: if self.find is None: self.find_tag = Tag(None, '') else: try: self.find_tag = Tag(None, self.find) except ValueError: pass class InfoSink(MatchingSink): def __init__(self, entry, tag, alltags): MatchingSink.__init__(self, tag) self.entry = entry self.alltags = alltags self.matching_rev = None self.perfect_match = 0 self.lockinfo = { } self.saw_revision = False def define_tag(self, name, revision): MatchingSink.define_tag(self, name, revision) self.alltags[name] = revision def admin_completed(self): MatchingSink.admin_completed(self) if self.find_tag is None: # tag we're looking for doesn't exist if self.entry.kind == vclib.FILE: self.entry.absent = 1 raise rcsparse.RCSStopParser def parse_completed(self): if not self.saw_revision: #self.entry.errors.append("No revisions exist on %s" % (view_tag or "MAIN")) self.entry.absent = 1 def set_locker(self, rev, locker): self.lockinfo[rev] = locker def define_revision(self, revision, date, author, state, branches, next): self.saw_revision = True if self.perfect_match: return tag = self.find_tag rev = Revision(revision, date, author, state == "dead") rev.lockinfo = self.lockinfo.get(revision) # perfect match if revision number matches tag number or if # revision is on trunk and tag points to trunk. imperfect match # if tag refers to a branch and either a) this revision is the # highest revision so far found on that branch, or b) this # revision is the branchpoint. perfect = ((rev.number == tag.number) or (not tag.number and len(rev.number) == 2)) if perfect or (tag.is_branch and \ ((tag.number == rev.number[:-1] and (not self.matching_rev or rev.number > self.matching_rev.number)) or (rev.number == tag.number[:-1]))): self.matching_rev = rev self.perfect_match = perfect def set_revision_info(self, revision, log, text): if self.matching_rev: if revision == self.matching_rev.string: self.entry.rev = self.matching_rev.string self.entry.date = self.matching_rev.date self.entry.author = self.matching_rev.author self.entry.dead = self.matching_rev.dead self.entry.lockinfo = self.matching_rev.lockinfo self.entry.absent = 0 self.entry.log = log raise rcsparse.RCSStopParser else: raise rcsparse.RCSStopParser class TreeSink(rcsparse.Sink): d_command = re.compile('^d(\d+)\\s(\\d+)') a_command = re.compile('^a(\d+)\\s(\\d+)') def __init__(self): self.revs = { } self.tags = { } self.head = None self.default_branch = None self.lockinfo = { } def set_head_revision(self, revision): self.head = revision def set_principal_branch(self, branch_number): self.default_branch = branch_number def set_locker(self, rev, locker): self.lockinfo[rev] = locker def define_tag(self, name, revision): # check !tags.has_key(tag_name) self.tags[name] = revision def define_revision(self, revision, date, author, state, branches, next): # check !revs.has_key(revision) self.revs[revision] = Revision(revision, date, author, state == "dead") def set_revision_info(self, revision, log, text): # check revs.has_key(revision) rev = self.revs[revision] rev.log = log changed = None added = 0 deled = 0 if self.head != revision: changed = 1 lines = string.split(text, '\n') idx = 0 while idx < len(lines): command = lines[idx] dmatch = self.d_command.match(command) idx = idx + 1 if dmatch: deled = deled + string.atoi(dmatch.group(2)) else: amatch = self.a_command.match(command) if amatch: count = string.atoi(amatch.group(2)) added = added + count idx = idx + count elif command: raise "error while parsing deltatext: %s" % command if len(rev.number) == 2: rev.next_changed = changed and "+%i -%i" % (deled, added) else: rev.changed = changed and "+%i -%i" % (added, deled) class StreamText: d_command = re.compile('^d(\d+)\\s(\\d+)') a_command = re.compile('^a(\d+)\\s(\\d+)') def __init__(self, text): self.text = string.split(text, "\n") def command(self, cmd): adjust = 0 add_lines_remaining = 0 diffs = string.split(cmd, "\n") if diffs[-1] == "": del diffs[-1] if len(diffs) == 0: return if diffs[0] == "": del diffs[0] for command in diffs: if add_lines_remaining > 0: # Insertion lines from a prior "a" command self.text.insert(start_line + adjust, command) add_lines_remaining = add_lines_remaining - 1 adjust = adjust + 1 continue dmatch = self.d_command.match(command) amatch = self.a_command.match(command) if dmatch: # "d" - Delete command start_line = string.atoi(dmatch.group(1)) count = string.atoi(dmatch.group(2)) begin = start_line + adjust - 1 del self.text[begin:begin + count] adjust = adjust - count elif amatch: # "a" - Add command start_line = string.atoi(amatch.group(1)) count = string.atoi(amatch.group(2)) add_lines_remaining = count else: raise RuntimeError, 'Error parsing diff commands' def secondnextdot(s, start): # find the position the second dot after the start index. return string.find(s, '.', string.find(s, '.', start) + 1) class COSink(MatchingSink): def __init__(self, rev): MatchingSink.__init__(self, rev) def set_head_revision(self, revision): self.head = Revision(revision) self.last = None self.sstext = None def admin_completed(self): MatchingSink.admin_completed(self) if self.find_tag is None: raise vclib.InvalidRevision(self.find) def set_revision_info(self, revision, log, text): tag = self.find_tag rev = Revision(revision) if rev.number == tag.number: self.log = log depth = len(rev.number) if rev.number == self.head.number: assert self.sstext is None self.sstext = StreamText(text) elif (depth == 2 and tag.number and rev.number >= tag.number[:depth]): assert len(self.last.number) == 2 assert rev.number < self.last.number self.sstext.command(text) elif (depth > 2 and rev.number[:depth-1] == tag.number[:depth-1] and (rev.number <= tag.number or len(tag.number) == depth-1)): assert len(rev.number) - len(self.last.number) in (0, 2) assert rev.number > self.last.number self.sstext.command(text) else: rev = None if rev: #print "tag =", tag.number, "rev =", rev.number, "
" self.last = rev viewvc-1.1.22/lib/vclib/ccvs/rcsparse/0000755000175000017500000000000012265242270017731 5ustar cmpilatocmpilatoviewvc-1.1.22/lib/vclib/ccvs/rcsparse/debug.py0000644000175000017500000000631112071623704021372 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- """debug.py: various debugging tools for the rcsparse package.""" import time from __init__ import parse import common class DebugSink(common.Sink): def set_head_revision(self, revision): print 'head:', revision def set_principal_branch(self, branch_name): print 'branch:', branch_name def define_tag(self, name, revision): print 'tag:', name, '=', revision def set_comment(self, comment): print 'comment:', comment def set_description(self, description): print 'description:', description def define_revision(self, revision, timestamp, author, state, branches, next): print 'revision:', revision print ' timestamp:', timestamp print ' author:', author print ' state:', state print ' branches:', branches print ' next:', next def set_revision_info(self, revision, log, text): print 'revision:', revision print ' log:', log print ' text:', text[:100], '...' class DumpSink(common.Sink): """Dump all the parse information directly to stdout. The output is relatively unformatted and untagged. It is intended as a raw dump of the data in the RCS file. A copy can be saved, then changes made to the parsing engine, then a comparison of the new output against the old output. """ def __init__(self): global sha import sha def set_head_revision(self, revision): print revision def set_principal_branch(self, branch_name): print branch_name def define_tag(self, name, revision): print name, revision def set_comment(self, comment): print comment def set_description(self, description): print description def define_revision(self, revision, timestamp, author, state, branches, next): print revision, timestamp, author, state, branches, next def set_revision_info(self, revision, log, text): print revision, sha.new(log).hexdigest(), sha.new(text).hexdigest() def tree_completed(self): print 'tree_completed' def parse_completed(self): print 'parse_completed' def dump_file(fname): parse(open(fname, 'rb'), DumpSink()) def time_file(fname): f = open(fname, 'rb') s = common.Sink() t = time.time() parse(f, s) t = time.time() - t print t def _usage(): print 'This is normally a module for importing, but it has a couple' print 'features for testing as an executable script.' print 'USAGE: %s COMMAND filename,v' % sys.argv[0] print ' where COMMAND is one of:' print ' dump: filename is "dumped" to stdout' print ' time: filename is parsed with the time written to stdout' sys.exit(1) if __name__ == '__main__': import sys if len(sys.argv) != 3: _usage() if sys.argv[1] == 'dump': dump_file(sys.argv[2]) elif sys.argv[1] == 'time': time_file(sys.argv[2]) else: _usage() viewvc-1.1.22/lib/vclib/ccvs/rcsparse/test-data/0000755000175000017500000000000012265242270021617 5ustar cmpilatocmpilatoviewvc-1.1.22/lib/vclib/ccvs/rcsparse/test-data/default.out0000644000175000017500000000316010720125421023764 0ustar cmpilatocmpilatoset_head_revision('1.2') define_tag('B_SPLIT', '1.2.0.4') define_tag('B_MIXED', '1.2.0.2') define_tag('T_MIXED', '1.2') define_tag('B_FROM_INITIALS_BUT_ONE', '1.1.1.1.0.4') define_tag('B_FROM_INITIALS', '1.1.1.1.0.2') define_tag('T_ALL_INITIAL_FILES_BUT_ONE', '1.1.1.1') define_tag('T_ALL_INITIAL_FILES', '1.1.1.1') define_tag('vendortag', '1.1.1.1') define_tag('vendorbranch', '1.1.1') set_locking('strict') set_comment('# ') admin_completed() define_revision('1.2', 1053649073, 'jrandom', 'Exp', ['1.2.2.1', '1.2.4.1'], '1.1') define_revision('1.1', 895879219, 'jrandom', 'Exp', ['1.1.1.1'], None) define_revision('1.1.1.1', 895879219, 'jrandom', 'Exp', [], None) define_revision('1.2.2.1', 1053649896, 'jrandom', 'Exp', [], None) define_revision('1.2.4.1', 1054610431, 'jrandom', 'Exp', [], None) tree_completed() set_description('') set_revision_info('1.2', 'Second commit to proj, affecting all 7 files.\n', "This is the file `default' in the top level of the project.\n\nEvery directory in the `proj' project has a file named `default'.\n\nThis line was added in the second commit (affecting all 7 files).\n") set_revision_info('1.2.4.1', 'First change on branch B_SPLIT.\n\nThis change excludes sub3/default, because it was not part of this\ncommit, and sub1/subsubB/default, which is not even on the branch yet.\n', 'a5 2\n\nFirst change on branch B_SPLIT.\n') set_revision_info('1.2.2.1', 'Modify three files, on branch B_MIXED.\n', 'a5 2\n\nThis line was added on branch B_MIXED only (affecting 3 files).\n') set_revision_info('1.1', 'Initial revision\n', 'd4 2\n') set_revision_info('1.1.1.1', 'Initial import.\n', '') parse_completed() viewvc-1.1.22/lib/vclib/ccvs/rcsparse/test-data/default,v0000644000175000017500000000260310720125421023421 0ustar cmpilatocmpilatohead 1.2; access; symbols B_SPLIT:1.2.0.4 B_MIXED:1.2.0.2 T_MIXED:1.2 B_FROM_INITIALS_BUT_ONE:1.1.1.1.0.4 B_FROM_INITIALS:1.1.1.1.0.2 T_ALL_INITIAL_FILES_BUT_ONE:1.1.1.1 T_ALL_INITIAL_FILES:1.1.1.1 vendortag:1.1.1.1 vendorbranch:1.1.1; locks; strict; comment @# @; 1.2 date 2003.05.23.00.17.53; author jrandom; state Exp; branches 1.2.2.1 1.2.4.1; next 1.1; 1.1 date 98.05.22.23.20.19; author jrandom; state Exp; branches 1.1.1.1; next ; 1.1.1.1 date 98.05.22.23.20.19; author jrandom; state Exp; branches; next ; 1.2.2.1 date 2003.05.23.00.31.36; author jrandom; state Exp; branches; next ; 1.2.4.1 date 2003.06.03.03.20.31; author jrandom; state Exp; branches; next ; desc @@ 1.2 log @Second commit to proj, affecting all 7 files. @ text @This is the file `default' in the top level of the project. Every directory in the `proj' project has a file named `default'. This line was added in the second commit (affecting all 7 files). @ 1.2.4.1 log @First change on branch B_SPLIT. This change excludes sub3/default, because it was not part of this commit, and sub1/subsubB/default, which is not even on the branch yet. @ text @a5 2 First change on branch B_SPLIT. @ 1.2.2.1 log @Modify three files, on branch B_MIXED. @ text @a5 2 This line was added on branch B_MIXED only (affecting 3 files). @ 1.1 log @Initial revision @ text @d4 2 @ 1.1.1.1 log @Initial import. @ text @@ viewvc-1.1.22/lib/vclib/ccvs/rcsparse/test-data/empty-file.out0000644000175000017500000000016110720122734024415 0ustar cmpilatocmpilatoset_locking('strict') set_comment('# ') admin_completed() tree_completed() set_description('') parse_completed() viewvc-1.1.22/lib/vclib/ccvs/rcsparse/test-data/empty-file,v0000644000175000017500000000010010720122734024042 0ustar cmpilatocmpilatohead ; access; symbols; locks; strict; comment @# @; desc @@ viewvc-1.1.22/lib/vclib/ccvs/rcsparse/common.py0000644000175000017500000002010412071623704021570 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- """common.py: common classes and functions for the RCS parsing tools.""" import calendar import string class Sink: def set_head_revision(self, revision): pass def set_principal_branch(self, branch_name): pass def set_access(self, accessors): pass def define_tag(self, name, revision): pass def set_locker(self, revision, locker): pass def set_locking(self, mode): """Used to signal locking mode. Called with mode argument 'strict' if strict locking Not called when no locking used.""" pass def set_comment(self, comment): pass def set_expansion(self, mode): pass def admin_completed(self): pass def define_revision(self, revision, timestamp, author, state, branches, next): pass def tree_completed(self): pass def set_description(self, description): pass def set_revision_info(self, revision, log, text): pass def parse_completed(self): pass # -------------------------------------------------------------------------- # # EXCEPTIONS USED BY RCSPARSE # class RCSParseError(Exception): pass class RCSIllegalCharacter(RCSParseError): pass class RCSExpected(RCSParseError): def __init__(self, got, wanted): RCSParseError.__init__( self, 'Unexpected parsing error in RCS file.\n' 'Expected token: %s, but saw: %s' % (wanted, got) ) class RCSStopParser(Exception): pass # -------------------------------------------------------------------------- # # STANDARD TOKEN STREAM-BASED PARSER # class _Parser: stream_class = None # subclasses need to define this def _read_until_semicolon(self): """Read all tokens up to and including the next semicolon token. Return the tokens (not including the semicolon) as a list.""" tokens = [] while 1: token = self.ts.get() if token == ';': break tokens.append(token) return tokens def _parse_admin_head(self, token): rev = self.ts.get() if rev == ';': # The head revision is not specified. Just drop the semicolon # on the floor. pass else: self.sink.set_head_revision(rev) self.ts.match(';') def _parse_admin_branch(self, token): branch = self.ts.get() if branch != ';': self.sink.set_principal_branch(branch) self.ts.match(';') def _parse_admin_access(self, token): accessors = self._read_until_semicolon() if accessors: self.sink.set_access(accessors) def _parse_admin_symbols(self, token): while 1: tag_name = self.ts.get() if tag_name == ';': break self.ts.match(':') tag_rev = self.ts.get() self.sink.define_tag(tag_name, tag_rev) def _parse_admin_locks(self, token): while 1: locker = self.ts.get() if locker == ';': break self.ts.match(':') rev = self.ts.get() self.sink.set_locker(rev, locker) def _parse_admin_strict(self, token): self.sink.set_locking("strict") self.ts.match(';') def _parse_admin_comment(self, token): self.sink.set_comment(self.ts.get()) self.ts.match(';') def _parse_admin_expand(self, token): expand_mode = self.ts.get() self.sink.set_expansion(expand_mode) self.ts.match(';') admin_token_map = { 'head' : _parse_admin_head, 'branch' : _parse_admin_branch, 'access' : _parse_admin_access, 'symbols' : _parse_admin_symbols, 'locks' : _parse_admin_locks, 'strict' : _parse_admin_strict, 'comment' : _parse_admin_comment, 'expand' : _parse_admin_expand, 'desc' : None, } def parse_rcs_admin(self): while 1: # Read initial token at beginning of line token = self.ts.get() try: f = self.admin_token_map[token] except KeyError: # We're done once we reach the description of the RCS tree if token[0] in string.digits: self.ts.unget(token) return else: # Chew up "newphrase" # warn("Unexpected RCS token: $token\n") while self.ts.get() != ';': pass else: if f is None: self.ts.unget(token) return else: f(self, token) def _parse_rcs_tree_entry(self, revision): # Parse date self.ts.match('date') date = self.ts.get() self.ts.match(';') # Convert date into timestamp date_fields = string.split(date, '.') # According to rcsfile(5): the year "contains just the last two # digits of the year for years from 1900 through 1999, and all the # digits of years thereafter". if len(date_fields[0]) == 2: date_fields[0] = '19' + date_fields[0] date_fields = map(string.atoi, date_fields) EPOCH = 1970 if date_fields[0] < EPOCH: raise ValueError, 'invalid year' timestamp = calendar.timegm(tuple(date_fields) + (0, 0, 0,)) # Parse author ### NOTE: authors containing whitespace are violations of the ### RCS specification. We are making an allowance here because ### CVSNT is known to produce these sorts of authors. self.ts.match('author') author = ' '.join(self._read_until_semicolon()) # Parse state self.ts.match('state') state = '' while 1: token = self.ts.get() if token == ';': break state = state + token + ' ' state = state[:-1] # toss the trailing space # Parse branches self.ts.match('branches') branches = self._read_until_semicolon() # Parse revision of next delta in chain self.ts.match('next') next = self.ts.get() if next == ';': next = None else: self.ts.match(';') # there are some files with extra tags in them. for example: # owner 640; # group 15; # permissions 644; # hardlinks @configure.in@; # this is "newphrase" in RCSFILE(5). we just want to skip over these. while 1: token = self.ts.get() if token == 'desc' or token[0] in string.digits: self.ts.unget(token) break # consume everything up to the semicolon self._read_until_semicolon() self.sink.define_revision(revision, timestamp, author, state, branches, next) def parse_rcs_tree(self): while 1: revision = self.ts.get() # End of RCS tree description ? if revision == 'desc': self.ts.unget(revision) return self._parse_rcs_tree_entry(revision) def parse_rcs_description(self): self.ts.match('desc') self.sink.set_description(self.ts.get()) def parse_rcs_deltatext(self): while 1: revision = self.ts.get() if revision is None: # EOF break text, sym2, log, sym1 = self.ts.mget(4) if sym1 != 'log': print `text[:100], sym2[:100], log[:100], sym1[:100]` raise RCSExpected(sym1, 'log') if sym2 != 'text': raise RCSExpected(sym2, 'text') ### need to add code to chew up "newphrase" self.sink.set_revision_info(revision, log, text) def parse(self, file, sink): self.ts = self.stream_class(file) self.sink = sink self.parse_rcs_admin() # let sink know when the admin section has been completed self.sink.admin_completed() self.parse_rcs_tree() # many sinks want to know when the tree has been completed so they can # do some work to prep for the arrival of the deltatext self.sink.tree_completed() self.parse_rcs_description() self.parse_rcs_deltatext() # easiest for us to tell the sink it is done, rather than worry about # higher level software doing it. self.sink.parse_completed() self.ts = self.sink = None # -------------------------------------------------------------------------- viewvc-1.1.22/lib/vclib/ccvs/rcsparse/run-tests.py0000755000175000017500000000425010720141675022254 0ustar cmpilatocmpilato#! /usr/bin/python # (Be in -*- python -*- mode.) # # ==================================================================== # Copyright (c) 2007 CollabNet. All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://subversion.tigris.org/license-1.html. # If newer versions of this license are posted there, you may use a # newer version instead, at your option. # # This software consists of voluntary contributions made by many # individuals. For exact contribution history, see the revision # history and logs, available at http://viewvc.tigris.org/. # ==================================================================== """Run tests of rcsparse code.""" import sys import os import glob from cStringIO import StringIO from difflib import Differ # Since there is nontrivial logic in __init__.py, we have to import # parse() via that file. First make sure that the directory # containing this script is in the path: script_dir = os.path.dirname(sys.argv[0]) sys.path.insert(0, script_dir) from __init__ import parse from parse_rcs_file import LoggingSink test_dir = os.path.join(script_dir, 'test-data') filelist = glob.glob(os.path.join(test_dir, '*,v')) filelist.sort() all_tests_ok = 1 for filename in filelist: sys.stderr.write('%s: ' % (filename,)) f = StringIO() try: parse(open(filename, 'rb'), LoggingSink(f)) except Exception, e: sys.stderr.write('Error parsing file: %s!\n' % (e,)) all_tests_ok = 0 else: output = f.getvalue() expected_output_filename = filename[:-2] + '.out' expected_output = open(expected_output_filename, 'rb').read() if output == expected_output: sys.stderr.write('OK\n') else: sys.stderr.write('Output does not match expected output!\n') differ = Differ() for diffline in differ.compare( expected_output.splitlines(1), output.splitlines(1) ): sys.stderr.write(diffline) all_tests_ok = 0 if all_tests_ok: sys.exit(0) else: sys.exit(1) viewvc-1.1.22/lib/vclib/ccvs/rcsparse/default.py0000644000175000017500000001122512071623704021730 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # This file was originally based on portions of the blame.py script by # Curt Hagenlocher. # # ----------------------------------------------------------------------- import string import common class _TokenStream: token_term = string.whitespace + ";:" try: token_term = frozenset(token_term) except NameError: pass # the algorithm is about the same speed for any CHUNK_SIZE chosen. # grab a good-sized chunk, but not too large to overwhelm memory. # note: we use a multiple of a standard block size CHUNK_SIZE = 192 * 512 # about 100k # CHUNK_SIZE = 5 # for debugging, make the function grind... def __init__(self, file): self.rcsfile = file self.idx = 0 self.buf = self.rcsfile.read(self.CHUNK_SIZE) if self.buf == '': raise RuntimeError, 'EOF' def get(self): "Get the next token from the RCS file." # Note: we can afford to loop within Python, examining individual # characters. For the whitespace and tokens, the number of iterations # is typically quite small. Thus, a simple iterative loop will beat # out more complex solutions. buf = self.buf lbuf = len(buf) idx = self.idx while 1: if idx == lbuf: buf = self.rcsfile.read(self.CHUNK_SIZE) if buf == '': # signal EOF by returning None as the token del self.buf # so we fail if get() is called again return None lbuf = len(buf) idx = 0 if buf[idx] not in string.whitespace: break idx = idx + 1 if buf[idx] in ';:': self.buf = buf self.idx = idx + 1 return buf[idx] if buf[idx] != '@': end = idx + 1 token = '' while 1: # find token characters in the current buffer while end < lbuf and buf[end] not in self.token_term: end = end + 1 token = token + buf[idx:end] if end < lbuf: # we stopped before the end, so we have a full token idx = end break # we stopped at the end of the buffer, so we may have a partial token buf = self.rcsfile.read(self.CHUNK_SIZE) lbuf = len(buf) idx = end = 0 self.buf = buf self.idx = idx return token # a "string" which starts with the "@" character. we'll skip it when we # search for content. idx = idx + 1 chunks = [ ] while 1: if idx == lbuf: idx = 0 buf = self.rcsfile.read(self.CHUNK_SIZE) if buf == '': raise RuntimeError, 'EOF' lbuf = len(buf) i = string.find(buf, '@', idx) if i == -1: chunks.append(buf[idx:]) idx = lbuf continue if i == lbuf - 1: chunks.append(buf[idx:i]) idx = 0 buf = '@' + self.rcsfile.read(self.CHUNK_SIZE) if buf == '@': raise RuntimeError, 'EOF' lbuf = len(buf) continue if buf[i + 1] == '@': chunks.append(buf[idx:i+1]) idx = i + 2 continue chunks.append(buf[idx:i]) self.buf = buf self.idx = i + 1 return string.join(chunks, '') # _get = get # def get(self): token = self._get() print 'T:', `token` return token def match(self, match): "Try to match the next token from the input buffer." token = self.get() if token != match: raise common.RCSExpected(token, match) def unget(self, token): "Put this token back, for the next get() to return." # Override the class' .get method with a function which clears the # overridden method then returns the pushed token. Since this function # will not be looked up via the class mechanism, it should be a "normal" # function, meaning it won't have "self" automatically inserted. # Therefore, we need to pass both self and the token thru via defaults. # note: we don't put this into the input buffer because it may have been # @-unescaped already. def give_it_back(self=self, token=token): del self.get return token self.get = give_it_back def mget(self, count): "Return multiple tokens. 'next' is at the end." result = [ ] for i in range(count): result.append(self.get()) result.reverse() return result class Parser(common._Parser): stream_class = _TokenStream viewvc-1.1.22/lib/vclib/ccvs/rcsparse/__init__.py0000644000175000017500000000130012071623704022034 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- """This package provides parsing tools for RCS files.""" from common import * try: from tparse import parse except ImportError: try: from texttools import Parser except ImportError: from default import Parser def parse(file, sink): return Parser().parse(file, sink) viewvc-1.1.22/lib/vclib/ccvs/rcsparse/texttools.py0000644000175000017500000002437012071623704022356 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- import string # note: this will raise an ImportError if it isn't available. the rcsparse # package will recognize this and switch over to the default parser. from mx import TextTools import common # for convenience _tt = TextTools _idchar_list = map(chr, range(33, 127)) + map(chr, range(160, 256)) _idchar_list.remove('$') _idchar_list.remove(',') #_idchar_list.remove('.') # leave as part of 'num' symbol _idchar_list.remove(':') _idchar_list.remove(';') _idchar_list.remove('@') _idchar = string.join(_idchar_list, '') _idchar_set = _tt.set(_idchar) _onechar_token_set = _tt.set(':;') _not_at_set = _tt.invset('@') _T_TOKEN = 30 _T_STRING_START = 40 _T_STRING_SPAN = 60 _T_STRING_END = 70 _E_COMPLETE = 100 # ended on a complete token _E_TOKEN = 110 # ended mid-token _E_STRING_SPAN = 130 # ended within a string _E_STRING_END = 140 # ended with string-end ('@') (could be mid-@@) _SUCCESS = +100 _EOF = 'EOF' _CONTINUE = 'CONTINUE' _UNUSED = 'UNUSED' # continuation of a token over a chunk boundary _c_token_table = ( (_T_TOKEN, _tt.AllInSet, _idchar_set), ) class _mxTokenStream: # the algorithm is about the same speed for any CHUNK_SIZE chosen. # grab a good-sized chunk, but not too large to overwhelm memory. # note: we use a multiple of a standard block size CHUNK_SIZE = 192 * 512 # about 100k # CHUNK_SIZE = 5 # for debugging, make the function grind... def __init__(self, file): self.rcsfile = file self.tokens = [ ] self.partial = None self.string_end = None def _parse_chunk(self, buf, start=0): "Get the next token from the RCS file." buflen = len(buf) assert start < buflen # construct a tag table which refers to the buffer we need to parse. table = ( #1: ignore whitespace. with or without whitespace, move to the next rule. (None, _tt.AllInSet, _tt.whitespace_set, +1), #2 (_E_COMPLETE, _tt.EOF + _tt.AppendTagobj, _tt.Here, +1, _SUCCESS), #3: accumulate token text and exit, or move to the next rule. (_UNUSED, _tt.AllInSet + _tt.AppendMatch, _idchar_set, +2), #4 (_E_TOKEN, _tt.EOF + _tt.AppendTagobj, _tt.Here, -3, _SUCCESS), #5: single character tokens exit immediately, or move to the next rule (_UNUSED, _tt.IsInSet + _tt.AppendMatch, _onechar_token_set, +2), #6 (_E_COMPLETE, _tt.EOF + _tt.AppendTagobj, _tt.Here, -5, _SUCCESS), #7: if this isn't an '@' symbol, then we have a syntax error (go to a # negative index to indicate that condition). otherwise, suck it up # and move to the next rule. (_T_STRING_START, _tt.Is + _tt.AppendTagobj, '@'), #8 (None, _tt.Is, '@', +4, +1), #9 (buf, _tt.Is, '@', +1, -1), #10 (_T_STRING_END, _tt.Skip + _tt.AppendTagobj, 0, 0, +1), #11 (_E_STRING_END, _tt.EOF + _tt.AppendTagobj, _tt.Here, -10, _SUCCESS), #12 (_E_STRING_SPAN, _tt.EOF + _tt.AppendTagobj, _tt.Here, +1, _SUCCESS), #13: suck up everything that isn't an AT. go to next rule to look for EOF (buf, _tt.AllInSet, _not_at_set, 0, +1), #14: go back to look for double AT if we aren't at the end of the string (_E_STRING_SPAN, _tt.EOF + _tt.AppendTagobj, _tt.Here, -6, _SUCCESS), ) # Fast, texttools may be, but it's somewhat lacking in clarity. # Here's an attempt to document the logic encoded in the table above: # # Flowchart: # _____ # / /\ # 1 -> 2 -> 3 -> 5 -> 7 -> 8 -> 9 -> 10 -> 11 # | \/ \/ \/ /\ \/ # \ 4 6 12 14 / # \_______/_____/ \ / / # \ 13 / # \__________________________________________/ # # #1: Skip over any whitespace. # #2: If now EOF, exit with code _E_COMPLETE. # #3: If we have a series of characters in _idchar_set, then: # #4: Output them as a token, and go back to #1. # #5: If we have a character in _onechar_token_set, then: # #6: Output it as a token, and go back to #1. # #7: If we do not have an '@', then error. # If we do, then log a _T_STRING_START and continue. # #8: If we have another '@', continue on to #9. Otherwise: # #12: If now EOF, exit with code _E_STRING_SPAN. # #13: Record the slice up to the next '@' (or EOF). # #14: If now EOF, exit with code _E_STRING_SPAN. # Otherwise, go back to #8. # #9: If we have another '@', then we've just seen an escaped # (by doubling) '@' within an @-string. Record a slice including # just one '@' character, and jump back to #8. # Otherwise, we've *either* seen the terminating '@' of an @-string, # *or* we've seen one half of an escaped @@ sequence that just # happened to be split over a chunk boundary - in either case, # we continue on to #10. # #10: Log a _T_STRING_END. # #11: If now EOF, exit with _E_STRING_END. Otherwise, go back to #1. success, taglist, idx = _tt.tag(buf, table, start) if not success: ### need a better way to report this error raise common.RCSIllegalCharacter() assert idx == buflen # pop off the last item last_which = taglist.pop() i = 0 tlen = len(taglist) while i < tlen: if taglist[i] == _T_STRING_START: j = i + 1 while j < tlen: if taglist[j] == _T_STRING_END: s = _tt.join(taglist, '', i+1, j) del taglist[i:j] tlen = len(taglist) taglist[i] = s break j = j + 1 else: assert last_which == _E_STRING_SPAN s = _tt.join(taglist, '', i+1) del taglist[i:] self.partial = (_T_STRING_SPAN, [ s ]) break i = i + 1 # figure out whether we have a partial last-token if last_which == _E_TOKEN: self.partial = (_T_TOKEN, [ taglist.pop() ]) elif last_which == _E_COMPLETE: pass elif last_which == _E_STRING_SPAN: assert self.partial else: assert last_which == _E_STRING_END self.partial = (_T_STRING_END, [ taglist.pop() ]) taglist.reverse() taglist.extend(self.tokens) self.tokens = taglist def _set_end(self, taglist, text, l, r, subtags): self.string_end = l def _handle_partial(self, buf): which, chunks = self.partial if which == _T_TOKEN: success, taglist, idx = _tt.tag(buf, _c_token_table) if not success: # The start of this buffer was not a token. So the end of the # prior buffer was a complete token. self.tokens.insert(0, string.join(chunks, '')) else: assert len(taglist) == 1 and taglist[0][0] == _T_TOKEN \ and taglist[0][1] == 0 and taglist[0][2] == idx if idx == len(buf): # # The whole buffer was one huge token, so we may have a # partial token again. # # Note: this modifies the list of chunks in self.partial # chunks.append(buf) # consumed the whole buffer return len(buf) # got the rest of the token. chunks.append(buf[:idx]) self.tokens.insert(0, string.join(chunks, '')) # no more partial token self.partial = None return idx if which == _T_STRING_END: if buf[0] != '@': self.tokens.insert(0, string.join(chunks, '')) return 0 chunks.append('@') start = 1 else: start = 0 self.string_end = None string_table = ( (None, _tt.Is, '@', +3, +1), (_UNUSED, _tt.Is + _tt.AppendMatch, '@', +1, -1), (self._set_end, _tt.Skip + _tt.CallTag, 0, 0, _SUCCESS), (None, _tt.EOF, _tt.Here, +1, _SUCCESS), # suck up everything that isn't an AT. move to next rule to look # for EOF (_UNUSED, _tt.AllInSet + _tt.AppendMatch, _not_at_set, 0, +1), # go back to look for double AT if we aren't at the end of the string (None, _tt.EOF, _tt.Here, -5, _SUCCESS), ) success, unused, idx = _tt.tag(buf, string_table, start, len(buf), chunks) # must have matched at least one item assert success if self.string_end is None: assert idx == len(buf) self.partial = (_T_STRING_SPAN, chunks) elif self.string_end < len(buf): self.partial = None self.tokens.insert(0, string.join(chunks, '')) else: self.partial = (_T_STRING_END, chunks) return idx def _parse_more(self): buf = self.rcsfile.read(self.CHUNK_SIZE) if not buf: return _EOF if self.partial: idx = self._handle_partial(buf) if idx is None: return _CONTINUE if idx < len(buf): self._parse_chunk(buf, idx) else: self._parse_chunk(buf) return _CONTINUE def get(self): try: return self.tokens.pop() except IndexError: pass while not self.tokens: action = self._parse_more() if action == _EOF: return None return self.tokens.pop() # _get = get # def get(self): token = self._get() print 'T:', `token` return token def match(self, match): if self.tokens: token = self.tokens.pop() else: token = self.get() if token != match: raise common.RCSExpected(token, match) def unget(self, token): self.tokens.append(token) def mget(self, count): "Return multiple tokens. 'next' is at the end." while len(self.tokens) < count: action = self._parse_more() if action == _EOF: ### fix this raise RuntimeError, 'EOF hit while expecting tokens' result = self.tokens[-count:] del self.tokens[-count:] return result class Parser(common._Parser): stream_class = _mxTokenStream viewvc-1.1.22/lib/vclib/ccvs/rcsparse/parse_rcs_file.py0000755000175000017500000000430610720141157023265 0ustar cmpilatocmpilato#! /usr/bin/python # (Be in -*- python -*- mode.) # # ==================================================================== # Copyright (c) 2006-2007 CollabNet. All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://subversion.tigris.org/license-1.html. # If newer versions of this license are posted there, you may use a # newer version instead, at your option. # # This software consists of voluntary contributions made by many # individuals. For exact contribution history, see the revision # history and logs, available at http://cvs2svn.tigris.org/. # ==================================================================== """Parse an RCS file, showing the rcsparse callbacks that are called. This program is useful to see whether an RCS file has a problem (in the sense of not being parseable by rcsparse) and also to illuminate the correspondence between RCS file contents and rcsparse callbacks. The output of this program can also be considered to be a kind of 'canonical' format for RCS files, at least in so far as rcsparse returns all relevant information in the file and provided that the order of callbacks is always the same.""" import sys import os class Logger: def __init__(self, f, name): self.f = f self.name = name def __call__(self, *args): self.f.write( '%s(%s)\n' % (self.name, ', '.join(['%r' % arg for arg in args]),) ) class LoggingSink: def __init__(self, f): self.f = f def __getattr__(self, name): return Logger(self.f, name) if __name__ == '__main__': # Since there is nontrivial logic in __init__.py, we have to import # parse() via that file. First make sure that the directory # containing this script is in the path: sys.path.insert(0, os.path.dirname(sys.argv[0])) from __init__ import parse if sys.argv[1:]: for path in sys.argv[1:]: if os.path.isfile(path) and path.endswith(',v'): parse( open(path, 'rb'), LoggingSink(sys.stdout) ) else: sys.stderr.write('%r is being ignored.\n' % path) else: parse(sys.stdin, LoggingSink(sys.stdout)) viewvc-1.1.22/lib/vclib/ccvs/blame.py0000644000175000017500000004066712071623704017556 0ustar cmpilatocmpilato#!/usr/bin/env python # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # Copyright (C) 2000 Curt Hagenlocher # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # blame.py: Annotate each line of a CVS file with its author, # revision #, date, etc. # # ----------------------------------------------------------------------- # # This file is based on the cvsblame.pl portion of the Bonsai CVS tool, # developed by Steve Lamm for Netscape Communications Corporation. More # information about Bonsai can be found at # http://www.mozilla.org/bonsai.html # # cvsblame.pl, in turn, was based on Scott Furman's cvsblame script # # ----------------------------------------------------------------------- import string import re import time import math import rcsparse import vclib class CVSParser(rcsparse.Sink): # Precompiled regular expressions trunk_rev = re.compile('^[0-9]+\\.[0-9]+$') last_branch = re.compile('(.*)\\.[0-9]+') is_branch = re.compile('^(.*)\\.0\\.([0-9]+)$') d_command = re.compile('^d(\d+)\\s(\\d+)') a_command = re.compile('^a(\d+)\\s(\\d+)') SECONDS_PER_DAY = 86400 def __init__(self): self.Reset() def Reset(self): self.last_revision = {} self.prev_revision = {} self.revision_date = {} self.revision_author = {} self.revision_branches = {} self.next_delta = {} self.prev_delta = {} self.tag_revision = {} self.timestamp = {} self.revision_ctime = {} self.revision_age = {} self.revision_log = {} self.revision_deltatext = {} self.revision_map = [] # map line numbers to revisions self.lines_added = {} self.lines_removed = {} # Map a tag to a numerical revision number. The tag can be a symbolic # branch tag, a symbolic revision tag, or an ordinary numerical # revision number. def map_tag_to_revision(self, tag_or_revision): try: revision = self.tag_revision[tag_or_revision] match = self.is_branch.match(revision) if match: branch = match.group(1) + '.' + match.group(2) if self.last_revision.get(branch): return self.last_revision[branch] else: return match.group(1) else: return revision except: return '' # Construct an ordered list of ancestor revisions to the given # revision, starting with the immediate ancestor and going back # to the primordial revision (1.1). # # Note: The generated path does not traverse the tree the same way # that the individual revision deltas do. In particular, # the path traverses the tree "backwards" on branches. def ancestor_revisions(self, revision): ancestors = [] revision = self.prev_revision.get(revision) while revision: ancestors.append(revision) revision = self.prev_revision.get(revision) return ancestors # Split deltatext specified by rev to each line. def deltatext_split(self, rev): lines = string.split(self.revision_deltatext[rev], '\n') if lines[-1] == '': del lines[-1] return lines # Extract the given revision from the digested RCS file. # (Essentially the equivalent of cvs up -rXXX) def extract_revision(self, revision): path = [] add_lines_remaining = 0 start_line = 0 count = 0 while revision: path.append(revision) revision = self.prev_delta.get(revision) path.reverse() path = path[1:] # Get rid of head revision text = self.deltatext_split(self.head_revision) # Iterate, applying deltas to previous revision for revision in path: adjust = 0 diffs = self.deltatext_split(revision) self.lines_added[revision] = 0 self.lines_removed[revision] = 0 lines_added_now = 0 lines_removed_now = 0 for command in diffs: dmatch = self.d_command.match(command) amatch = self.a_command.match(command) if add_lines_remaining > 0: # Insertion lines from a prior "a" command text.insert(start_line + adjust, command) add_lines_remaining = add_lines_remaining - 1 adjust = adjust + 1 elif dmatch: # "d" - Delete command start_line = string.atoi(dmatch.group(1)) count = string.atoi(dmatch.group(2)) begin = start_line + adjust - 1 del text[begin:begin + count] adjust = adjust - count lines_removed_now = lines_removed_now + count elif amatch: # "a" - Add command start_line = string.atoi(amatch.group(1)) count = string.atoi(amatch.group(2)) add_lines_remaining = count lines_added_now = lines_added_now + count else: raise RuntimeError, 'Error parsing diff commands' self.lines_added[revision] = self.lines_added[revision] + lines_added_now self.lines_removed[revision] = self.lines_removed[revision] + lines_removed_now return text def set_head_revision(self, revision): self.head_revision = revision def set_principal_branch(self, branch_name): self.principal_branch = branch_name def define_tag(self, name, revision): # Create an associate array that maps from tag name to # revision number and vice-versa. self.tag_revision[name] = revision def set_comment(self, comment): self.file_description = comment def set_description(self, description): self.rcs_file_description = description # Construct dicts that represent the topology of the RCS tree # and other arrays that contain info about individual revisions. # # The following dicts are created, keyed by revision number: # self.revision_date -- e.g. "96.02.23.00.21.52" # self.timestamp -- seconds since 12:00 AM, Jan 1, 1970 GMT # self.revision_author -- e.g. "tom" # self.revision_branches -- descendant branch revisions, separated by spaces, # e.g. "1.21.4.1 1.21.2.6.1" # self.prev_revision -- revision number of previous *ancestor* in RCS tree. # Traversal of this array occurs in the direction # of the primordial (1.1) revision. # self.prev_delta -- revision number of previous revision which forms # the basis for the edit commands in this revision. # This causes the tree to be traversed towards the # trunk when on a branch, and towards the latest trunk # revision when on the trunk. # self.next_delta -- revision number of next "delta". Inverts prev_delta. # # Also creates self.last_revision, keyed by a branch revision number, which # indicates the latest revision on a given branch, # e.g. self.last_revision{"1.2.8"} == 1.2.8.5 def define_revision(self, revision, timestamp, author, state, branches, next): self.tag_revision[revision] = revision branch = self.last_branch.match(revision).group(1) self.last_revision[branch] = revision #self.revision_date[revision] = date self.timestamp[revision] = timestamp # Pretty print the date string ltime = time.localtime(self.timestamp[revision]) formatted_date = time.strftime("%d %b %Y %H:%M", ltime) self.revision_ctime[revision] = formatted_date # Save age self.revision_age[revision] = ((time.time() - self.timestamp[revision]) / self.SECONDS_PER_DAY) # save author self.revision_author[revision] = author # ignore the state # process the branch information branch_text = '' for branch in branches: self.prev_revision[branch] = revision self.next_delta[revision] = branch self.prev_delta[branch] = revision branch_text = branch_text + branch + '' self.revision_branches[revision] = branch_text # process the "next revision" information if next: self.next_delta[revision] = next self.prev_delta[next] = revision is_trunk_revision = self.trunk_rev.match(revision) is not None if is_trunk_revision: self.prev_revision[revision] = next else: self.prev_revision[next] = revision # Construct associative arrays containing info about individual revisions. # # The following associative arrays are created, keyed by revision number: # revision_log -- log message # revision_deltatext -- Either the complete text of the revision, # in the case of the head revision, or the # encoded delta between this revision and another. # The delta is either with respect to the successor # revision if this revision is on the trunk or # relative to its immediate predecessor if this # revision is on a branch. def set_revision_info(self, revision, log, text): self.revision_log[revision] = log self.revision_deltatext[revision] = text def parse_cvs_file(self, rcs_pathname, opt_rev = None, opt_m_timestamp = None): # Args in: opt_rev - requested revision # opt_m - time since modified # Args out: revision_map # timestamp # revision_deltatext # CheckHidden(rcs_pathname) try: rcsfile = open(rcs_pathname, 'rb') except: raise RuntimeError, ('error: %s appeared to be under CVS control, ' + 'but the RCS file is inaccessible.') % rcs_pathname rcsparse.parse(rcsfile, self) rcsfile.close() if opt_rev in [None, '', 'HEAD']: # Explicitly specified topmost revision in tree revision = self.head_revision else: # Symbolic tag or specific revision number specified. revision = self.map_tag_to_revision(opt_rev) if revision == '': raise RuntimeError, 'error: -r: No such revision: ' + opt_rev # The primordial revision is not always 1.1! Go find it. primordial = revision while self.prev_revision.get(primordial): primordial = self.prev_revision[primordial] # Don't display file at all, if -m option is specified and no # changes have been made in the specified file. if opt_m_timestamp and self.timestamp[revision] < opt_m_timestamp: return '' # Figure out how many lines were in the primordial, i.e. version 1.1, # check-in by moving backward in time from the head revision to the # first revision. line_count = 0 if self.revision_deltatext.get(self.head_revision): tmp_array = self.deltatext_split(self.head_revision) line_count = len(tmp_array) skip = 0 rev = self.prev_revision.get(self.head_revision) while rev: diffs = self.deltatext_split(rev) for command in diffs: dmatch = self.d_command.match(command) amatch = self.a_command.match(command) if skip > 0: # Skip insertion lines from a prior "a" command skip = skip - 1 elif dmatch: # "d" - Delete command start_line = string.atoi(dmatch.group(1)) count = string.atoi(dmatch.group(2)) line_count = line_count - count elif amatch: # "a" - Add command start_line = string.atoi(amatch.group(1)) count = string.atoi(amatch.group(2)) skip = count line_count = line_count + count else: raise RuntimeError, 'error: illegal RCS file' rev = self.prev_revision.get(rev) # Now, play the delta edit commands *backwards* from the primordial # revision forward, but rather than applying the deltas to the text of # each revision, apply the changes to an array of revision numbers. # This creates a "revision map" -- an array where each element # represents a line of text in the given revision but contains only # the revision number in which the line was introduced rather than # the line text itself. # # Note: These are backward deltas for revisions on the trunk and # forward deltas for branch revisions. # Create initial revision map for primordial version. self.revision_map = [primordial] * line_count ancestors = [revision, ] + self.ancestor_revisions(revision) ancestors = ancestors[:-1] # Remove "1.1" last_revision = primordial ancestors.reverse() for revision in ancestors: is_trunk_revision = self.trunk_rev.match(revision) is not None if is_trunk_revision: diffs = self.deltatext_split(last_revision) # Revisions on the trunk specify deltas that transform a # revision into an earlier revision, so invert the translation # of the 'diff' commands. for command in diffs: if skip > 0: skip = skip - 1 else: dmatch = self.d_command.match(command) amatch = self.a_command.match(command) if dmatch: start_line = string.atoi(dmatch.group(1)) count = string.atoi(dmatch.group(2)) temp = [] while count > 0: temp.append(revision) count = count - 1 self.revision_map = (self.revision_map[:start_line - 1] + temp + self.revision_map[start_line - 1:]) elif amatch: start_line = string.atoi(amatch.group(1)) count = string.atoi(amatch.group(2)) del self.revision_map[start_line:start_line + count] skip = count else: raise RuntimeError, 'Error parsing diff commands' else: # Revisions on a branch are arranged backwards from those on # the trunk. They specify deltas that transform a revision # into a later revision. adjust = 0 diffs = self.deltatext_split(revision) for command in diffs: if skip > 0: skip = skip - 1 else: dmatch = self.d_command.match(command) amatch = self.a_command.match(command) if dmatch: start_line = string.atoi(dmatch.group(1)) count = string.atoi(dmatch.group(2)) adj_begin = start_line + adjust - 1 adj_end = start_line + adjust - 1 + count del self.revision_map[adj_begin:adj_end] adjust = adjust - count elif amatch: start_line = string.atoi(amatch.group(1)) count = string.atoi(amatch.group(2)) skip = count temp = [] while count > 0: temp.append(revision) count = count - 1 self.revision_map = (self.revision_map[:start_line + adjust] + temp + self.revision_map[start_line + adjust:]) adjust = adjust + skip else: raise RuntimeError, 'Error parsing diff commands' last_revision = revision return revision class BlameSource: def __init__(self, rcs_file, opt_rev=None, include_text=False): # Parse the CVS file parser = CVSParser() revision = parser.parse_cvs_file(rcs_file, opt_rev) count = len(parser.revision_map) lines = parser.extract_revision(revision) if len(lines) != count: raise RuntimeError, 'Internal consistency error' # set up some state variables self.revision = revision self.lines = lines self.num_lines = count self.parser = parser self.include_text = include_text # keep track of where we are during an iteration self.idx = -1 self.last = None def __getitem__(self, idx): if idx == self.idx: return self.last if idx >= self.num_lines: raise IndexError("No more annotations") if idx != self.idx + 1: raise BlameSequencingError() # Get the line and metadata for it. rev = self.parser.revision_map[idx] prev_rev = self.parser.prev_revision.get(rev) line_number = idx + 1 author = self.parser.revision_author[rev] thisline = self.lines[idx] if not self.include_text: thisline = None ### TODO: Put a real date in here. item = vclib.Annotation(thisline, line_number, rev, prev_rev, author, None) self.last = item self.idx = idx return item class BlameSequencingError(Exception): pass viewvc-1.1.22/lib/vclib/__init__.py0000644000175000017500000003403112133055421017255 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- """Version Control lib is an abstract API to access versioning systems such as CVS. """ import string import types # item types returned by Repository.itemtype(). FILE = 'FILE' DIR = 'DIR' # diff types recognized by Repository.rawdiff(). UNIFIED = 1 CONTEXT = 2 SIDE_BY_SIDE = 3 # root types returned by Repository.roottype(). CVS = 'cvs' SVN = 'svn' # action kinds found in ChangedPath.action ADDED = 'added' DELETED = 'deleted' REPLACED = 'replaced' MODIFIED = 'modified' # log sort keys SORTBY_DEFAULT = 0 # default/no sorting SORTBY_DATE = 1 # sorted by date, youngest first SORTBY_REV = 2 # sorted by revision, youngest first # ====================================================================== # class Repository: """Abstract class representing a repository.""" def rootname(self): """Return the name of this repository.""" def roottype(self): """Return the type of this repository (vclib.CVS, vclib.SVN, ...).""" def rootpath(self): """Return the location of this repository.""" def authorizer(self): """Return the vcauth.Authorizer object associated with this repository, or None if no such association has been made.""" def open(self): """Open a connection to the repository.""" def itemtype(self, path_parts, rev): """Return the type of the item (file or dir) at the given path and revision The result will be vclib.DIR or vclib.FILE The path is specified as a list of components, relative to the root of the repository. e.g. ["subdir1", "subdir2", "filename"] rev is the revision of the item to check """ pass def openfile(self, path_parts, rev, options): """Open a file object to read file contents at a given path and revision. The return value is a 2-tuple of containg the file object and revision number in canonical form. The path is specified as a list of components, relative to the root of the repository. e.g. ["subdir1", "subdir2", "filename"] rev is the revision of the file to check out options is a dictionary of implementation specific options """ def listdir(self, path_parts, rev, options): """Return list of files in a directory The result is a list of DirEntry objects The path is specified as a list of components, relative to the root of the repository. e.g. ["subdir1", "subdir2", "filename"] rev is the revision of the directory to list options is a dictionary of implementation specific options """ def dirlogs(self, path_parts, rev, entries, options): """Augment directory entries with log information New properties will be set on all of the DirEntry objects in the entries list. At the very least, a "rev" property will be set to a revision number or None if the entry doesn't have a number. Other properties that may be set include "date", "author", "log", "size", and "lockinfo". The path is specified as a list of components, relative to the root of the repository. e.g. ["subdir1", "subdir2", "filename"] rev is the revision of the directory listing and will effect which log messages are returned entries is a list of DirEntry objects returned from a previous call to the listdir() method options is a dictionary of implementation specific options """ def itemlog(self, path_parts, rev, sortby, first, limit, options): """Retrieve an item's log information The result is a list of Revision objects The path is specified as a list of components, relative to the root of the repository. e.g. ["subdir1", "subdir2", "filename"] rev is the revision of the item to return information about sortby indicates the way in which the returned list should be sorted (SORTBY_DEFAULT, SORTBY_DATE, SORTBY_REV) first is the 0-based index of the first Revision returned (after sorting, if any, has occured) limit is the maximum number of returned Revisions, or 0 to return all available data options is a dictionary of implementation specific options """ def itemprops(self, path_parts, rev): """Return a dictionary mapping property names to property values for properties stored on an item. The path is specified as a list of components, relative to the root of the repository. e.g. ["subdir1", "subdir2", "filename"] rev is the revision of the item to return information about. """ def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): """Return a diff (in GNU diff format) of two file revisions type is the requested diff type (UNIFIED, CONTEXT, etc) options is a dictionary that can contain the following options plus implementation-specific options context - integer, number of context lines to include funout - boolean, include C function names ignore_white - boolean, ignore whitespace Return value is a python file object """ def annotate(self, path_parts, rev, include_text=False): """Return a list of Annotation object, sorted by their "line_number" components, which describe the lines of given version of a file. The file path is specified as a list of components, relative to the root of the repository. e.g. ["subdir1", "subdir2", "filename"] rev is the revision of the item to return information about. If include_text is true, populate the Annotation objects' "text" members with the corresponding line of file content; otherwise, leave that member set to None.""" def revinfo(self, rev): """Return information about a global revision rev is the revision of the item to return information about Return value is a 5-tuple containing: the date, author, log message, a list of ChangedPath items representing paths changed, and a dictionary mapping property names to property values for properties stored on an item. Raise vclib.UnsupportedFeature if the version control system doesn't support a global revision concept. """ def isexecutable(self, path_parts, rev): """Return true iff a given revision of a versioned file is to be considered an executable program or script. The path is specified as a list of components, relative to the root of the repository. e.g. ["subdir1", "subdir2", "filename"] rev is the revision of the item to return information about """ def filesize(self, path_parts, rev): """Return the size of a versioned file's contents if it can be obtained without a brute force measurement; -1 otherwise. NOTE: Callers that require a filesize answer when this function returns -1 may obtain it by measuring the data returned via openfile(). The path is specified as a list of components, relative to the root of the repository. e.g. ["subdir1", "subdir2", "filename"] rev is the revision of the item to return information about """ # ====================================================================== class DirEntry: """Instances represent items in a directory listing""" def __init__(self, name, kind, errors=[]): """Create a new DirEntry() item: NAME: The name of the directory entry KIND: The path kind of the entry (vclib.DIR, vclib.FILE) ERRORS: A list of error strings representing problems encountered while determining the other info about this entry """ self.name = name self.kind = kind self.errors = errors class Revision: """Instances holds information about revisions of versioned resources""" def __init__(self, number, string, date, author, changed, log, size, lockinfo): """Create a new Revision() item: NUMBER: Revision in an integer-based, sortable format STRING: Revision as a string DATE: Seconds since Epoch (GMT) that this revision was created AUTHOR: Author of the revision CHANGED: Lines-changed (contextual diff) information LOG: Log message associated with the creation of this revision SIZE: Size (in bytes) of this revision's fulltext (files only) LOCKINFO: Information about locks held on this revision """ self.number = number self.string = string self.date = date self.author = author self.changed = changed self.log = log self.size = size self.lockinfo = lockinfo def __cmp__(self, other): return cmp(self.number, other.number) class Annotation: """Instances represent per-line file annotation information""" def __init__(self, text, line_number, rev, prev_rev, author, date): """Create a new Annotation() item: TEXT: Raw text of a line of file contents LINE_NUMBER: Line number on which the line is found REV: Revision in which the line was last modified PREV_REV: Revision prior to 'rev' AUTHOR: Author who last modified the line DATE: Date on which the line was last modified, in seconds since the epoch, GMT """ self.text = text self.line_number = line_number self.rev = rev self.prev_rev = prev_rev self.author = author self.date = date class ChangedPath: """Instances represent changes to paths""" def __init__(self, path_parts, rev, pathtype, base_path_parts, base_rev, action, copied, text_changed, props_changed): """Create a new ChangedPath() item: PATH_PARTS: Path that was changed REV: Revision represented by this change PATHTYPE: Type of this path (vclib.DIR, vclib.FILE, ...) BASE_PATH_PARTS: Previous path for this changed item BASE_REV: Previous revision for this changed item ACTION: Kind of change (vclib.ADDED, vclib.DELETED, ...) COPIED: Boolean -- was this path copied from elsewhere? TEXT_CHANGED: Boolean -- did the file's text change? PROPS_CHANGED: Boolean -- did the item's metadata change? """ self.path_parts = path_parts self.rev = rev self.pathtype = pathtype self.base_path_parts = base_path_parts self.base_rev = base_rev self.action = action self.copied = copied self.text_changed = text_changed self.props_changed = props_changed # ====================================================================== class Error(Exception): pass class ReposNotFound(Error): pass class UnsupportedFeature(Error): pass class ItemNotFound(Error): def __init__(self, path): # use '/' rather than os.sep because this is for user consumption, and # it was defined using URL separators if type(path) in (types.TupleType, types.ListType): path = string.join(path, '/') Error.__init__(self, path) class InvalidRevision(Error): def __init__(self, revision=None): if revision is None: Error.__init__(self, "Invalid revision") else: Error.__init__(self, "Invalid revision " + str(revision)) class NonTextualFileContents(Error): pass # ====================================================================== # Implementation code used by multiple vclib modules import popen import os import time def _diff_args(type, options): """generate argument list to pass to diff or rcsdiff""" args = [] if type == CONTEXT: if options.has_key('context'): if options['context'] is None: args.append('--context=-1') else: args.append('--context=%i' % options['context']) else: args.append('-c') elif type == UNIFIED: if options.has_key('context'): if options['context'] is None: args.append('--unified=-1') else: args.append('--unified=%i' % options['context']) else: args.append('-u') elif type == SIDE_BY_SIDE: args.append('--side-by-side') args.append('--width=164') else: raise NotImplementedError if options.get('funout', 0): args.append('-p') if options.get('ignore_white', 0): args.append('-w') return args class _diff_fp: """File object reading a diff between temporary files, cleaning up on close""" def __init__(self, temp1, temp2, info1=None, info2=None, diff_cmd='diff', diff_opts=[]): self.temp1 = temp1 self.temp2 = temp2 args = diff_opts[:] if info1 and info2: args.extend(["-L", self._label(info1), "-L", self._label(info2)]) args.extend([temp1, temp2]) self.fp = popen.popen(diff_cmd, args, "r") def read(self, bytes): return self.fp.read(bytes) def readline(self): return self.fp.readline() def close(self): try: if self.fp: self.fp.close() self.fp = None finally: try: if self.temp1: os.remove(self.temp1) self.temp1 = None finally: if self.temp2: os.remove(self.temp2) self.temp2 = None def __del__(self): self.close() def _label(self, (path, date, rev)): date = date and time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date)) return "%s\t%s\t%s" % (path, date, rev) def check_root_access(repos): """Return 1 iff the associated username is permitted to read REPOS, as determined by consulting REPOS's Authorizer object (if any).""" auth = repos.authorizer() if not auth: return 1 return auth.check_root_access(repos.rootname()) def check_path_access(repos, path_parts, pathtype=None, rev=None): """Return 1 iff the associated username is permitted to read revision REV of the path PATH_PARTS (of type PATHTYPE) in repository REPOS, as determined by consulting REPOS's Authorizer object (if any).""" auth = repos.authorizer() if not auth: return 1 if not pathtype: pathtype = repos.itemtype(path_parts, rev) return auth.check_path_access(repos.rootname(), path_parts, pathtype, rev) viewvc-1.1.22/lib/dbi.py0000644000175000017500000000421012071623704015157 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- import sys import time import types import re import compat import MySQLdb # set to 1 to store commit times in UTC, or 0 to use the ViewVC machine's # local timezone. Using UTC is recommended because it ensures that the # database will remain valid even if it is moved to another machine or the host # computer's time zone is changed. UTC also avoids the ambiguity associated # with daylight saving time (for example if a computer in New York recorded the # local time 2002/10/27 1:30 am, there would be no way to tell whether the # actual time was recorded before or after clocks were rolled back). Use local # times for compatibility with databases used by ViewCVS 0.92 and earlier # versions. utc_time = 1 def DateTimeFromTicks(ticks): """Return a MySQL DATETIME value from a unix timestamp""" if utc_time: t = time.gmtime(ticks) else: t = time.localtime(ticks) return "%04d-%02d-%02d %02d:%02d:%02d" % t[:6] _re_datetime = re.compile('([0-9]{4})-([0-9][0-9])-([0-9][0-9]) ' '([0-9][0-9]):([0-9][0-9]):([0-9][0-9])') def TicksFromDateTime(datetime): """Return a unix timestamp from a MySQL DATETIME value""" if type(datetime) == types.StringType: # datetime is a MySQL DATETIME string matches = _re_datetime.match(datetime).groups() t = tuple(map(int, matches)) + (0, 0, 0) elif hasattr(datetime, "timetuple"): # datetime is a Python >=2.3 datetime.DateTime object t = datetime.timetuple() else: # datetime is an eGenix mx.DateTime object t = datetime.tuple() if utc_time: return compat.timegm(t) else: return time.mktime(t[:8] + (-1,)) def connect(host, port, user, passwd, db): return MySQLdb.connect(host=host, port=port, user=user, passwd=passwd, db=db) viewvc-1.1.22/lib/blame.py0000644000175000017500000001144512071623704015511 0ustar cmpilatocmpilato#!/usr/bin/env python # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # Copyright (C) 2000 Curt Hagenlocher # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # blame.py: Annotate each line of a CVS file with its author, # revision #, date, etc. # # ----------------------------------------------------------------------- # # This file is based on the cvsblame.pl portion of the Bonsai CVS tool, # developed by Steve Lamm for Netscape Communications Corporation. More # information about Bonsai can be found at # http://www.mozilla.org/bonsai.html # # cvsblame.pl, in turn, was based on Scott Furman's cvsblame script # # ----------------------------------------------------------------------- import sys import string import os import re import time import math import vclib import sapi re_includes = re.compile('\\#(\\s*)include(\\s*)"(.*?)"') def link_includes(text, repos, path_parts, include_url): match = re_includes.match(text) if match: incfile = match.group(3) include_path_parts = path_parts[:-1] for part in filter(None, string.split(incfile, '/')): if part == "..": if not include_path_parts: # nothing left to pop; don't bother marking up this include. return text include_path_parts.pop() elif part and part != ".": include_path_parts.append(part) include_path = None try: if repos.itemtype(include_path_parts, None) == vclib.FILE: include_path = string.join(include_path_parts, '/') except vclib.ItemNotFound: pass if include_path: return '#%sinclude%s"%s"' % \ (match.group(1), match.group(2), string.replace(include_url, '/WHERE/', include_path), incfile) return text class HTMLBlameSource: """Wrapper around a the object by the vclib.annotate() which does HTML escaping, diff URL generation, and #include linking.""" def __init__(self, repos, path_parts, diff_url, include_url, opt_rev=None): self.repos = repos self.path_parts = path_parts self.diff_url = diff_url self.include_url = include_url self.annotation, self.revision = self.repos.annotate(path_parts, opt_rev, True) def __getitem__(self, idx): item = self.annotation.__getitem__(idx) diff_url = None if item.prev_rev: diff_url = '%sr1=%s&r2=%s' % (self.diff_url, item.prev_rev, item.rev) thisline = link_includes(sapi.escape(item.text), self.repos, self.path_parts, self.include_url) return _item(text=thisline, line_number=item.line_number, rev=item.rev, prev_rev=item.prev_rev, diff_url=diff_url, date=item.date, author=item.author) def blame(repos, path_parts, diff_url, include_url, opt_rev=None): source = HTMLBlameSource(repos, path_parts, diff_url, include_url, opt_rev) return source, source.revision class _item: def __init__(self, **kw): vars(self).update(kw) def make_html(root, rcs_path): import vclib.ccvs.blame bs = vclib.ccvs.blame.BlameSource(os.path.join(root, rcs_path)) line = 0 old_revision = 0 row_color = 'ffffff' rev_count = 0 align = ' style="text-align: %s;"' sys.stdout.write('\n') for line_data in bs: revision = line_data.rev thisline = line_data.text line = line_data.line_number author = line_data.author prev_rev = line_data.prev_rev if old_revision != revision and line != 1: if row_color == 'ffffff': row_color = 'e7e7e7' else: row_color = 'ffffff' sys.stdout.write('' % (line, row_color)) sys.stdout.write('%d' % (align % 'right', line)) if old_revision != revision or rev_count > 20: sys.stdout.write('%s' % (align % 'right', author or ' ')) sys.stdout.write('%s' % (align % 'left', revision)) old_revision = revision rev_count = 0 else: sys.stdout.write('') rev_count = rev_count + 1 sys.stdout.write('%s\n' % (align % 'left', string.rstrip(thisline) or ' ')) sys.stdout.write('
  
\n') def main(): import sys if len(sys.argv) != 3: print 'USAGE: %s cvsroot rcs-file' % sys.argv[0] sys.exit(1) make_html(sys.argv[1], sys.argv[2]) if __name__ == '__main__': main() viewvc-1.1.22/lib/ezt.py0000644000175000017500000007251511375304632015242 0ustar cmpilatocmpilato#!/usr/bin/env python """ezt.py -- easy templating ezt templates are simply text files in whatever format you so desire (such as XML, HTML, etc.) which contain directives sprinkled throughout. With these directives it is possible to generate the dynamic content from the ezt templates. These directives are enclosed in square brackets. If you are a C-programmer, you might be familar with the #ifdef directives of the C preprocessor 'cpp'. ezt provides a similar concept. Additionally EZT has a 'for' directive, which allows it to iterate (repeat) certain subsections of the template according to sequence of data items provided by the application. The final rendering is performed by the method generate() of the Template class. Building template instances can either be done using external EZT files (convention: use the suffix .ezt for such files): >>> template = Template("../templates/log.ezt") or by calling the parse() method of a template instance directly with a EZT template string: >>> template = Template() >>> template.parse(''' ... [title_string] ...

[title_string]

... [for a_sequence]

[a_sequence]

... [end]
... The [person] is [if-any state]in[else]out[end]. ... ... ... ''') The application should build a dictionary 'data' and pass it together with the output fileobject to the templates generate method: >>> data = {'title_string' : "A Dummy Page", ... 'a_sequence' : ['list item 1', 'list item 2', 'another element'], ... 'person': "doctor", ... 'state' : None } >>> import sys >>> template.generate(sys.stdout, data) A Dummy Page

A Dummy Page

list item 1

list item 2

another element


The doctor is out. Template syntax error reporting should be improved. Currently it is very sparse (template line numbers would be nice): >>> Template().parse("[if-any where] foo [else] bar [end unexpected args]") Traceback (innermost last): File "", line 1, in ? File "ezt.py", line 220, in parse self.program = self._parse(text) File "ezt.py", line 275, in _parse raise ArgCountSyntaxError(str(args[1:])) ArgCountSyntaxError: ['unexpected', 'args'] >>> Template().parse("[if unmatched_end]foo[end]") Traceback (innermost last): File "", line 1, in ? File "ezt.py", line 206, in parse self.program = self._parse(text) File "ezt.py", line 266, in _parse raise UnmatchedEndError() UnmatchedEndError Directives ========== Several directives allow the use of dotted qualified names refering to objects or attributes of objects contained in the data dictionary given to the .generate() method. Qualified names --------------- Qualified names have two basic forms: a variable reference, or a string constant. References are a name from the data dictionary with optional dotted attributes (where each intermediary is an object with attributes, of course). Examples: [varname] [ob.attr] ["string"] Simple directives ----------------- [QUAL_NAME] This directive is simply replaced by the value of the qualified name. If the value is a number it's converted to a string before being outputted. If it is None, nothing is outputted. If it is a python file object (i.e. any object with a "read" method), it's contents are outputted. If it is a callback function (any callable python object is assumed to be a callback function), it is invoked and passed an EZT Context object as an argument. [QUAL_NAME QUAL_NAME ...] If the first value is a callback function, it is invoked with an EZT Context object as a first argument, and the rest of the values as additional arguments. Otherwise, the first value defines a substitution format, specifying constant text and indices of the additional arguments. The arguments are substituted and the result is inserted into the output stream. Example: ["abc %0 def %1 ghi %0" foo bar.baz] Note that the first value can be any type of qualified name -- a string constant or a variable reference. Use %% to substitute a percent sign. Argument indices are 0-based. [include "filename"] or [include QUAL_NAME] This directive is replaced by content of the named include file. Note that a string constant is more efficient -- the target file is compiled inline. In the variable form, the target file is compiled and executed at runtime. Block directives ---------------- [for QUAL_NAME] ... [end] The text within the [for ...] directive and the corresponding [end] is repeated for each element in the sequence referred to by the qualified name in the for directive. Within the for block this identifiers now refers to the actual item indexed by this loop iteration. [if-any QUAL_NAME [QUAL_NAME2 ...]] ... [else] ... [end] Test if any QUAL_NAME value is not None or an empty string or list. The [else] clause is optional. CAUTION: Numeric values are converted to string, so if QUAL_NAME refers to a numeric value 0, the then-clause is substituted! [if-index INDEX_FROM_FOR odd] ... [else] ... [end] [if-index INDEX_FROM_FOR even] ... [else] ... [end] [if-index INDEX_FROM_FOR first] ... [else] ... [end] [if-index INDEX_FROM_FOR last] ... [else] ... [end] [if-index INDEX_FROM_FOR NUMBER] ... [else] ... [end] These five directives work similar to [if-any], but are only useful within a [for ...]-block (see above). The odd/even directives are for example useful to choose different background colors for adjacent rows in a table. Similar the first/last directives might be used to remove certain parts (for example "Diff to previous" doesn't make sense, if there is no previous). [is QUAL_NAME STRING] ... [else] ... [end] [is QUAL_NAME QUAL_NAME] ... [else] ... [end] The [is ...] directive is similar to the other conditional directives above. But it allows to compare two value references or a value reference with some constant string. [define VARIABLE] ... [end] The [define ...] directive allows you to create and modify template variables from within the template itself. Essentially, any data between inside the [define ...] and its matching [end] will be expanded using the other template parsing and output generation rules, and then stored as a string value assigned to the variable VARIABLE. The new (or changed) variable is then available for use with other mechanisms such as [is ...] or [if-any ...], as long as they appear later in the template. [format STRING] ... [end] The format directive controls how the values substituted into templates are escaped before they are put into the output stream. It has no effect on the literal text of the templates, only the output from [QUAL_NAME ...] directives. STRING can be one of "raw" "html" "xml" or "uri". The "raw" mode leaves the output unaltered; the "html" and "xml" modes escape special characters using entity escapes (like " and >); the "uri" mode escapes characters using hexadecimal escape sequences (like %20 and %7e). [format CALLBACK] Python applications using EZT can provide custom formatters as callback variables. "[format CALLBACK][QUAL_NAME][end]" is in most cases equivalent to "[CALLBACK QUAL_NAME]" """ # # Copyright (C) 2001-2007 Greg Stein. All Rights Reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # # This software is maintained by Greg and is available at: # http://svn.webdav.org/repos/projects/ezt/trunk/ # import string import re from types import StringType, IntType, FloatType, LongType, TupleType import os import cgi import urllib try: import cStringIO except ImportError: import StringIO cStringIO = StringIO # # Formatting types # FORMAT_RAW = 'raw' FORMAT_HTML = 'html' FORMAT_XML = 'xml' FORMAT_URI = 'uri' # # This regular expression matches three alternatives: # expr: DIRECTIVE | BRACKET | COMMENT # DIRECTIVE: '[' ITEM (whitespace ITEM)* '] # ITEM: STRING | NAME # STRING: '"' (not-slash-or-dquote | '\' anychar)* '"' # NAME: (alphanum | '_' | '-' | '.')+ # BRACKET: '[[]' # COMMENT: '[#' not-rbracket* ']' # # When used with the split() method, the return value will be composed of # non-matching text and the two paren groups (DIRECTIVE and BRACKET). Since # the COMMENT matches are not placed into a group, they are considered a # "splitting" value and simply dropped. # _item = r'(?:"(?:[^\\"]|\\.)*"|[-\w.]+)' _re_parse = re.compile(r'\[(%s(?: +%s)*)\]|(\[\[\])|\[#[^\]]*\]' % (_item, _item)) _re_args = re.compile(r'"(?:[^\\"]|\\.)*"|[-\w.]+') # block commands and their argument counts _block_cmd_specs = { 'if-index':2, 'for':1, 'is':2, 'define':1, 'format':1 } _block_cmds = _block_cmd_specs.keys() # two regular expresssions for compressing whitespace. the first is used to # compress any whitespace including a newline into a single newline. the # second regex is used to compress runs of whitespace into a single space. _re_newline = re.compile('[ \t\r\f\v]*\n\\s*') _re_whitespace = re.compile(r'\s\s+') # this regex is used to substitute arguments into a value. we split the value, # replace the relevant pieces, and then put it all back together. splitting # will produce a list of: TEXT ( splitter TEXT )*. splitter will be '%' or # an integer. _re_subst = re.compile('%(%|[0-9]+)') class Template: def __init__(self, fname=None, compress_whitespace=1, base_format=FORMAT_RAW): self.compress_whitespace = compress_whitespace if fname: self.parse_file(fname, base_format) def parse_file(self, fname, base_format=FORMAT_RAW): "fname -> a string object with pathname of file containg an EZT template." self.parse(_FileReader(fname), base_format) def parse(self, text_or_reader, base_format=FORMAT_RAW): """Parse the template specified by text_or_reader. The argument should be a string containing the template, or it should specify a subclass of ezt.Reader which can read templates. The base format for printing values is given by base_format. """ if not isinstance(text_or_reader, Reader): # assume the argument is a plain text string text_or_reader = _TextReader(text_or_reader) self.program = self._parse(text_or_reader, base_format=base_format) def generate(self, fp, data): if hasattr(data, '__getitem__') or callable(getattr(data, 'keys', None)): # a dictionary-like object was passed. convert it to an # attribute-based object. class _data_ob: def __init__(self, d): vars(self).update(d) data = _data_ob(data) ctx = Context(fp) ctx.data = data ctx.for_iterators = { } ctx.defines = { } self._execute(self.program, ctx) def _parse(self, reader, for_names=None, file_args=(), base_format=None): """text -> string object containing the template. This is a private helper function doing the real work for method parse. It returns the parsed template as a 'program'. This program is a sequence made out of strings or (function, argument) 2-tuples. Note: comment directives [# ...] are automatically dropped by _re_parse. """ # parse the template program into: (TEXT DIRECTIVE BRACKET)* TEXT parts = _re_parse.split(reader.text) program = [ ] stack = [ ] if not for_names: for_names = [ ] if base_format: program.append((self._cmd_format, _formatters[base_format])) for i in range(len(parts)): piece = parts[i] which = i % 3 # discriminate between: TEXT DIRECTIVE BRACKET if which == 0: # TEXT. append if non-empty. if piece: if self.compress_whitespace: piece = _re_whitespace.sub(' ', _re_newline.sub('\n', piece)) program.append(piece) elif which == 2: # BRACKET directive. append '[' if present. if piece: program.append('[') elif piece: # DIRECTIVE is present. args = _re_args.findall(piece) cmd = args[0] if cmd == 'else': if len(args) > 1: raise ArgCountSyntaxError(str(args[1:])) ### check: don't allow for 'for' cmd idx = stack[-1][1] true_section = program[idx:] del program[idx:] stack[-1][3] = true_section elif cmd == 'end': if len(args) > 1: raise ArgCountSyntaxError(str(args[1:])) # note: true-section may be None try: cmd, idx, args, true_section = stack.pop() except IndexError: raise UnmatchedEndError() else_section = program[idx:] if cmd == 'format': program.append((self._cmd_end_format, None)) else: func = getattr(self, '_cmd_' + re.sub('-', '_', cmd)) program[idx:] = [ (func, (args, true_section, else_section)) ] if cmd == 'for': for_names.pop() elif cmd in _block_cmds: if len(args) > _block_cmd_specs[cmd] + 1: raise ArgCountSyntaxError(str(args[1:])) ### this assumes arg1 is always a ref unless cmd is 'define' if cmd != 'define': args[1] = _prepare_ref(args[1], for_names, file_args) # handle arg2 for the 'is' command if cmd == 'is': args[2] = _prepare_ref(args[2], for_names, file_args) elif cmd == 'for': for_names.append(args[1][0]) # append the refname elif cmd == 'format': if args[1][0]: # argument is a variable reference formatter = args[1] else: # argument is a string constant referring to built-in formatter formatter = _formatters.get(args[1][1]) if not formatter: raise UnknownFormatConstantError(str(args[1:])) program.append((self._cmd_format, formatter)) # remember the cmd, current pos, args, and a section placeholder stack.append([cmd, len(program), args[1:], None]) elif cmd == 'include': if args[1][0] == '"': include_filename = args[1][1:-1] f_args = [ ] for arg in args[2:]: f_args.append(_prepare_ref(arg, for_names, file_args)) program.extend(self._parse(reader.read_other(include_filename), for_names, f_args)) else: if len(args) != 2: raise ArgCountSyntaxError(str(args)) program.append((self._cmd_include, (_prepare_ref(args[1], for_names, file_args), reader))) elif cmd == 'if-any': f_args = [ ] for arg in args[1:]: f_args.append(_prepare_ref(arg, for_names, file_args)) stack.append(['if-any', len(program), f_args, None]) else: # implied PRINT command f_args = [ ] for arg in args: f_args.append(_prepare_ref(arg, for_names, file_args)) program.append((self._cmd_print, f_args)) if stack: ### would be nice to say which blocks... raise UnclosedBlocksError() return program def _execute(self, program, ctx): """This private helper function takes a 'program' sequence as created by the method '_parse' and executes it step by step. strings are written to the file object 'fp' and functions are called. """ for step in program: if isinstance(step, StringType): ctx.fp.write(step) else: step[0](step[1], ctx) def _cmd_print(self, valrefs, ctx): value = _get_value(valrefs[0], ctx) args = map(lambda valref, ctx=ctx: _get_value(valref, ctx), valrefs[1:]) try: _write_value(value, args, ctx) except TypeError: raise Exception("Unprintable value type for '%s'" % (str(valrefs[0][0]))) def _cmd_format(self, formatter, ctx): if type(formatter) is TupleType: formatter = _get_value(formatter, ctx) ctx.formatters.append(formatter) def _cmd_end_format(self, valref, ctx): ctx.formatters.pop() def _cmd_include(self, (valref, reader), ctx): fname = _get_value(valref, ctx) ### note: we don't have the set of for_names to pass into this parse. ### I don't think there is anything to do but document it. self._execute(self._parse(reader.read_other(fname)), ctx) def _cmd_if_any(self, args, ctx): "If any value is a non-empty string or non-empty list, then T else F." (valrefs, t_section, f_section) = args value = 0 for valref in valrefs: if _get_value(valref, ctx): value = 1 break self._do_if(value, t_section, f_section, ctx) def _cmd_if_index(self, args, ctx): ((valref, value), t_section, f_section) = args iterator = ctx.for_iterators[valref[0]] if value == 'even': value = iterator.index % 2 == 0 elif value == 'odd': value = iterator.index % 2 == 1 elif value == 'first': value = iterator.index == 0 elif value == 'last': value = iterator.is_last() else: value = iterator.index == int(value) self._do_if(value, t_section, f_section, ctx) def _cmd_is(self, args, ctx): ((left_ref, right_ref), t_section, f_section) = args value = _get_value(right_ref, ctx) value = string.lower(_get_value(left_ref, ctx)) == string.lower(value) self._do_if(value, t_section, f_section, ctx) def _do_if(self, value, t_section, f_section, ctx): if t_section is None: t_section = f_section f_section = None if value: section = t_section else: section = f_section if section is not None: self._execute(section, ctx) def _cmd_for(self, args, ctx): ((valref,), unused, section) = args list = _get_value(valref, ctx) if isinstance(list, StringType): raise NeedSequenceError("The value of '%s' is not a sequence" % (valref[0])) refname = valref[0] ctx.for_iterators[refname] = iterator = _iter(list) for unused in iterator: self._execute(section, ctx) del ctx.for_iterators[refname] def _cmd_define(self, args, ctx): ((name,), unused, section) = args origfp = ctx.fp ctx.fp = cStringIO.StringIO() if section is not None: self._execute(section, ctx) ctx.defines[name] = ctx.fp.getvalue() ctx.fp = origfp def boolean(value): "Return a value suitable for [if-any bool_var] usage in a template." if value: return 'yes' return None def _prepare_ref(refname, for_names, file_args): """refname -> a string containing a dotted identifier. example:"foo.bar.bang" for_names -> a list of active for sequences. Returns a `value reference', a 3-tuple made out of (refname, start, rest), for fast access later. """ # is the reference a string constant? if refname[0] == '"': return None, refname[1:-1], None parts = string.split(refname, '.') start = parts[0] rest = parts[1:] # if this is an include-argument, then just return the prepared ref if start[:3] == 'arg': try: idx = int(start[3:]) except ValueError: pass else: if idx < len(file_args): orig_refname, start, more_rest = file_args[idx] if more_rest is None: # the include-argument was a string constant return None, start, None # prepend the argument's "rest" for our further processing rest[:0] = more_rest # rewrite the refname to ensure that any potential 'for' processing # has the correct name ### this can make it hard for debugging include files since we lose ### the 'argNNN' names if not rest: return start, start, [ ] refname = start + '.' + string.join(rest, '.') if for_names: # From last to first part, check if this reference is part of a for loop for i in range(len(parts), 0, -1): name = string.join(parts[:i], '.') if name in for_names: return refname, name, parts[i:] return refname, start, rest def _get_value((refname, start, rest), ctx): """(refname, start, rest) -> a prepared `value reference' (see above). ctx -> an execution context instance. Does a name space lookup within the template name space. Active for blocks take precedence over data dictionary members with the same name. """ if rest is None: # it was a string constant return start # get the starting object if ctx.for_iterators.has_key(start): ob = ctx.for_iterators[start].last_item elif ctx.defines.has_key(start): ob = ctx.defines[start] elif hasattr(ctx.data, start): ob = getattr(ctx.data, start) else: raise UnknownReference(refname) # walk the rest of the dotted reference for attr in rest: try: ob = getattr(ob, attr) except AttributeError: raise UnknownReference(refname) # make sure we return a string instead of some various Python types if isinstance(ob, IntType) \ or isinstance(ob, LongType) \ or isinstance(ob, FloatType): return str(ob) if ob is None: return '' # string or a sequence return ob def _print_formatted(formatters, ctx, chunk): # print chunk to ctx.fp after running it sequentially through formatters for formatter in formatters: chunk = formatter(chunk) ctx.fp.write(chunk) def _write_value(value, args, ctx): # value is a callback function, generates its own output if callable(value): apply(value, [ctx] + list(args)) return # squirrel away formatters in case one of them recursively calls # _write_value() -- we'll use them (in reverse order) to format our # output. formatters = ctx.formatters[:] formatters.reverse() try: # if the value has a 'read' attribute, then it is a stream: copy it if hasattr(value, 'read'): while 1: chunk = value.read(16384) if not chunk: break _print_formatted(formatters, ctx, chunk) # value is a substitution pattern elif args: parts = _re_subst.split(value) for i in range(len(parts)): piece = parts[i] if i%2 == 1 and piece != '%': idx = int(piece) if idx < len(args): piece = args[idx] else: piece = '' _print_formatted(formatters, ctx, piece) # plain old value, write to output else: _print_formatted(formatters, ctx, value) finally: # restore our formatters formatters.reverse() ctx.formatters = formatters class TemplateData: """A custom dictionary-like object that allows one-time definition of keys, and only value fetches and changes, and key deletions, thereafter. EZT doesn't require the use of this special class -- a normal dict-type data dictionary works fine. But use of this class will assist those who want the data sent to their templates to have a consistent set of keys.""" def __init__(self, initial_data={}): self._items = initial_data def __getitem__(self, key): return self._items.__getitem__(key) def __setitem__(self, key, item): assert self._items.has_key(key) return self._items.__setitem__(key, item) def __delitem__(self, key): return self._items.__delitem__(key) def keys(self): return self._items.keys() def merge(self, template_data): """Merge the data in TemplataData instance TEMPLATA_DATA into this instance. Avoid the temptation to use this conditionally in your code -- it rather defeats the purpose of this class.""" assert isinstance(template_data, TemplateData) self._items.update(template_data._items) class Context: """A container for the execution context""" def __init__(self, fp): self.fp = fp self.formatters = [] def write(self, value, args=()): _write_value(value, args, self) class Reader: "Abstract class which allows EZT to detect Reader objects." class _FileReader(Reader): """Reads templates from the filesystem.""" def __init__(self, fname): self.text = open(fname, 'rb').read() self._dir = os.path.dirname(fname) def read_other(self, relative): return _FileReader(os.path.join(self._dir, relative)) class _TextReader(Reader): """'Reads' a template from provided text.""" def __init__(self, text): self.text = text def read_other(self, relative): raise BaseUnavailableError() class _Iterator: """Specialized iterator for EZT that counts items and can look ahead Implements standard iterator interface and provides an is_last() method and two public members: index - integer index of the current item last_item - last item returned by next()""" def __init__(self, sequence): self._iter = iter(sequence) def next(self): if hasattr(self, '_next_item'): self.last_item = self._next_item del self._next_item else: self.last_item = self._iter.next() # may raise StopIteration if hasattr(self, 'index'): self.index = self.index + 1 else: self.index = 0 return self.last_item def is_last(self): """Return true if the current item is the last in the sequence""" # the only way we can tell if the current item is last is to call next() # and store the return value so it doesn't get lost if not hasattr(self, '_next_item'): try: self._next_item = self._iter.next() except StopIteration: return 1 return 0 def __iter__(self): return self class _OldIterator: """Alternate implemention of _Iterator for old Pythons without iterators This class implements the sequence protocol, instead of the iterator interface, so it's really not an iterator at all. But it can be used in python "for" loops as a drop-in replacement for _Iterator. It also provides the is_last() method and "last_item" and "index" members described in the _Iterator docstring.""" def __init__(self, sequence): self._seq = sequence def __getitem__(self, index): self.last_item = self._seq[index] # may raise IndexError self.index = index return self.last_item def is_last(self): return self.index + 1 >= len(self._seq) try: iter except NameError: _iter = _OldIterator else: _iter = _Iterator class EZTException(Exception): """Parent class of all EZT exceptions.""" class ArgCountSyntaxError(EZTException): """A bracket directive got the wrong number of arguments.""" class UnknownReference(EZTException): """The template references an object not contained in the data dictionary.""" class NeedSequenceError(EZTException): """The object dereferenced by the template is no sequence (tuple or list).""" class UnclosedBlocksError(EZTException): """This error may be simply a missing [end].""" class UnmatchedEndError(EZTException): """This error may be caused by a misspelled if directive.""" class BaseUnavailableError(EZTException): """Base location is unavailable, which disables includes.""" class UnknownFormatConstantError(EZTException): """The format specifier is an unknown value.""" def _raw_formatter(s): return s def _html_formatter(s): return cgi.escape(s) def _xml_formatter(s): s = s.replace('&', '&') s = s.replace('<', '<') s = s.replace('>', '>') return s def _uri_formatter(s): return urllib.quote(s) _formatters = { FORMAT_RAW : _raw_formatter, FORMAT_HTML : _html_formatter, FORMAT_XML : _xml_formatter, FORMAT_URI : _uri_formatter, } # --- standard test environment --- def test_parse(): assert _re_parse.split('[a]') == ['', '[a]', None, ''] assert _re_parse.split('[a] [b]') == \ ['', '[a]', None, ' ', '[b]', None, ''] assert _re_parse.split('[a c] [b]') == \ ['', '[a c]', None, ' ', '[b]', None, ''] assert _re_parse.split('x [a] y [b] z') == \ ['x ', '[a]', None, ' y ', '[b]', None, ' z'] assert _re_parse.split('[a "b" c "d"]') == \ ['', '[a "b" c "d"]', None, ''] assert _re_parse.split(r'["a \"b[foo]" c.d f]') == \ ['', '["a \\"b[foo]" c.d f]', None, ''] def _test(argv): import doctest, ezt verbose = "-v" in argv return doctest.testmod(ezt, verbose=verbose) if __name__ == "__main__": # invoke unit test for this module: import sys sys.exit(_test(sys.argv)[0]) viewvc-1.1.22/viewvc-install0000755000175000017500000004405212071623704016206 0ustar cmpilatocmpilato#!/usr/bin/env python # -*- Mode: python -*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # Install script for ViewVC # # ----------------------------------------------------------------------- import os import sys import string import re import traceback import py_compile import getopt import StringIO # Get access to our library modules. sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), 'lib')) import compat import viewvc import compat_ndiff version = viewvc.__version__ ## Installer defaults. DESTDIR = None ROOT_DIR = None CLEAN_MODE = None ## List of files for installation. ## tuple (source path, ## destination path, ## mode, ## boolean -- search-and-replace? ## boolean -- prompt before replacing? ## boolean -- compile?) FILE_INFO_LIST = [ ("bin/cgi/viewvc.cgi", "bin/cgi/viewvc.cgi", 0755, 1, 0, 0), ("bin/cgi/query.cgi", "bin/cgi/query.cgi", 0755, 1, 0, 0), ("bin/wsgi/viewvc.wsgi", "bin/wsgi/viewvc.wsgi", 0755, 1, 0, 0), ("bin/wsgi/viewvc.fcgi", "bin/wsgi/viewvc.fcgi", 0755, 1, 0, 0), ("bin/wsgi/query.wsgi", "bin/wsgi/query.wsgi", 0755, 1, 0, 0), ("bin/wsgi/query.fcgi", "bin/wsgi/query.fcgi", 0755, 1, 0, 0), ("bin/mod_python/viewvc.py", "bin/mod_python/viewvc.py", 0755, 1, 0, 0), ("bin/mod_python/query.py", "bin/mod_python/query.py", 0755, 1, 0, 0), ("bin/mod_python/handler.py", "bin/mod_python/handler.py", 0755, 1, 0, 0), ("bin/mod_python/.htaccess", "bin/mod_python/.htaccess", 0755, 0, 0, 0), ("bin/standalone.py", "bin/standalone.py", 0755, 1, 0, 0), ("bin/loginfo-handler", "bin/loginfo-handler", 0755, 1, 0, 0), ("bin/cvsdbadmin", "bin/cvsdbadmin", 0755, 1, 0, 0), ("bin/svndbadmin", "bin/svndbadmin", 0755, 1, 0, 0), ("bin/make-database", "bin/make-database", 0755, 1, 0, 0), ("conf/viewvc.conf.dist", "viewvc.conf.dist", 0644, 0, 0, 0), ("conf/viewvc.conf.dist", "viewvc.conf", 0644, 0, 1, 0), ("conf/cvsgraph.conf.dist", "cvsgraph.conf.dist", 0644, 0, 0, 0), ("conf/cvsgraph.conf.dist", "cvsgraph.conf", 0644, 0, 1, 0), ("conf/mimetypes.conf.dist", "mimetypes.conf.dist", 0644, 0, 0, 0), ("conf/mimetypes.conf.dist", "mimetypes.conf", 0644, 0, 1, 0), ] if sys.platform == "win32": FILE_INFO_LIST.extend([ ("bin/asp/viewvc.asp", "bin/asp/viewvc.asp", 0755, 1, 0, 0), ("bin/asp/query.asp", "bin/asp/query.asp", 0755, 1, 0, 0), ]) ## List of directories for installation. ## type (source path, ## destination path, ## boolean -- optional item?, ## boolean -- prompt before replacing?) TREE_LIST = [ ("lib", "lib", 0, 0), ("templates", "templates", 0, 1), ("templates-contrib", "templates-contrib", 1, 1), ] ## List of file extensions we can't show diffs for. BINARY_FILE_EXTS = [ '.png', '.gif', '.jpg', ] def _escape(str): """Callback function for re.sub(). re.escape() is no good because it blindly puts backslashes in front of anything that is not a number or letter regardless of whether the resulting sequence will be interpreted.""" return string.replace(str, "\\", "\\\\") def _actual_src_path(path): """Return the real on-disk location of PATH, which is relative to the ViewVC source directory.""" return os.path.join(os.path.dirname(sys.argv[0]), string.replace(path, '/', os.sep)) def error(text, etype=None, evalue=None): """Print error TEXT to stderr, pretty printing the optional exception type and value (ETYPE and EVALUE, respective), and then exit the program with an errorful code.""" sys.stderr.write("\n[ERROR] %s\n" % (text)) if etype: traceback.print_exception(etype, evalue, None, file=sys.stderr) sys.exit(1) def replace_var(contents, var, value): """Replace instances of the variable VAR as found in file CONTENTS with VALUE.""" pattern = re.compile('^' + var + r'\s*=\s*.*$', re.MULTILINE) repl = '%s = r"%s"' % (var, os.path.join(ROOT_DIR, value)) return re.sub(pattern, _escape(repl), contents) def replace_paths(contents): """Replace all ViewVC path placeholders found in file CONTENTS.""" if contents[:2] == '#!': shbang = '#!' + sys.executable contents = re.sub('^#![^\n]*', _escape(shbang), contents) contents = replace_var(contents, 'LIBRARY_DIR', 'lib') contents = replace_var(contents, 'CONF_PATHNAME', 'viewvc.conf') return contents def install_file(src_path, dst_path, mode, subst_path_vars, prompt_replace, compile_it): """Install a single file whose source is at SRC_PATH (which is relative to the ViewVC source directory) into the location DST_PATH (which is relative both to the global ROOT_DIR and DESTDIR settings), and set the file's MODE. If SUBST_PATH_VARS is set, substitute path variables in the file's contents. If PROMPT_REPLACE is set (and is not overridden by global setting CLEAN_MODE), prompt the user for how to deal with already existing files that differ from the to-be-installed version. If COMPILE_IT is set, compile the file as a Python module.""" src_path = _actual_src_path(src_path) dst_path = os.path.join(ROOT_DIR, string.replace(dst_path, '/', os.sep)) destdir_path = DESTDIR + dst_path overwrite = None if not (prompt_replace and os.path.exists(destdir_path)): # If the file doesn't already exist, or we've been instructed to # replace it without prompting, then drop in the new file and get # outta here. overwrite = 1 else: # If we're here, then the file already exists, and we've possibly # got to prompt the user for what to do about that. # Collect ndiff output from ndiff sys.stdout = StringIO.StringIO() compat_ndiff.main([destdir_path, src_path]) ndiff_output = sys.stdout.getvalue() # Return everything to normal sys.stdout = sys.__stdout__ # Collect the '+ ' and '- ' lines. diff_lines = [] looking_at_diff_lines = 0 for line in string.split(ndiff_output, '\n'): # Print line if it is a difference line if line[:2] == "+ " or line[:2] == "- " or line[:2] == "? ": diff_lines.append(line) looking_at_diff_lines = 1 else: # Compress lines that are the same to print one blank line if looking_at_diff_lines: diff_lines.append("") looking_at_diff_lines = 0 # If there are no differences, we're done here. if not diff_lines: overwrite = 1 else: # If we get here, there are differences. if CLEAN_MODE == 'true': overwrite = 1 elif CLEAN_MODE == 'false': overwrite = 0 else: print "File %s exists and is different from source file." \ % (destdir_path) while 1: name, ext = os.path.splitext(src_path) if ext in BINARY_FILE_EXTS: temp = raw_input("Do you want to [O]verwrite or " "[D]o not overwrite? ") else: temp = raw_input("Do you want to [O]verwrite, [D]o " "not overwrite, or [V]iew " "differences? ") if len(temp) == 0: continue temp = string.lower(temp[0]) if temp == "v" and ext not in BINARY_FILE_EXTS: print """ ---------------------------------------------------------------------------""" print string.join(diff_lines, '\n') + '\n' print """ LEGEND A leading '- ' indicates line to remove from installed file A leading '+ ' indicates line to add to installed file A leading '? ' shows intraline differences. ---------------------------------------------------------------------------""" elif temp == "d": overwrite = 0 elif temp == "o": overwrite = 1 if overwrite is not None: break assert overwrite is not None if not overwrite: print " preserved %s" % (dst_path) return ### If we get here, we're creating or overwriting the existing file. # Read the source file's contents. try: contents = open(src_path, "rb").read() except IOError, e: error(str(e)) # (Optionally) substitute ViewVC path variables. if subst_path_vars: contents = replace_paths(contents) # Ensure the existence of the containing directories. dst_parent = os.path.dirname(destdir_path) if not os.path.exists(dst_parent): try: compat.makedirs(dst_parent) print " created %s%s" % (dst_parent, os.sep) except os.error, e: if e.errno == 17: # EEXIST: file exists return if e.errno == 13: # EACCES: permission denied error("You do not have permission to create directory %s" \ % (dst_parent)) error("Unknown error creating directory %s" \ % (dst_parent, OSError, e)) # Now, write the file contents to their destination. try: exists = os.path.exists(destdir_path) open(destdir_path, "wb").write(contents) print " %s %s" \ % (exists and 'replaced ' or 'installed', dst_path) except IOError, e: if e.errno == 13: # EACCES: permission denied error("You do not have permission to write file %s" % (dst_path)) error("Unknown error writing file %s" % (dst_path, IOError, e)) # Set the files's mode. os.chmod(destdir_path, mode) # (Optionally) compile the file. if compile_it: py_compile.compile(destdir_path, destdir_path + "c" , dst_path) def install_tree(src_path, dst_path, is_optional, prompt_replace): """Install a tree whose source is at SRC_PATH (which is relative to the ViewVC source directory) into the location DST_PATH (which is relative both to the global ROOT_DIR and DESTDIR settings). If PROMPT_REPLACE is set (and is not overridden by global setting CLEAN_MODE), prompt the user for how to deal with already existing files that differ from the to-be-installed version. If IS_OPTIONAL is set, don't fuss about a missing source item.""" orig_src_path = src_path orig_dst_path = dst_path src_path = _actual_src_path(src_path) dst_path = os.path.join(ROOT_DIR, string.replace(dst_path, '/', os.sep)) if not os.path.isdir(src_path): print " skipping %s" % (dst_path) return destdir_path = os.path.join(DESTDIR + dst_path) # Get a list of items in the directory. files = os.listdir(src_path) files.sort() for fname in files: # Ignore some stuff found in development directories, but not # intended for installation. if fname == 'CVS' or fname == '.svn' or fname == '_svn' \ or fname[-4:] == '.pyc' or fname[-5:] == '.orig' \ or fname[-4:] == '.rej' or fname[0] == '.' \ or fname[-1] == '~': continue orig_src_child = orig_src_path + '/' + fname orig_dst_child = orig_dst_path + '/' + fname # If the item is a subdirectory, recurse. Otherwise, install the file. if os.path.isdir(os.path.join(src_path, fname)): install_tree(orig_src_child, orig_dst_child, 0, prompt_replace) else: set_paths = 0 compile_it = fname[-3:] == '.py' install_file(orig_src_child, orig_dst_child, 0644, set_paths, prompt_replace, compile_it) # Check for .py and .pyc files that don't belong in installation. for fname in os.listdir(destdir_path): if not os.path.isfile(os.path.join(destdir_path, fname)) or \ not ((fname[-3:] == '.py' and fname not in files) or (fname[-4:] == '.pyc' and fname[:-1] not in files)): continue # If we get here, there's cruft. delete = None if CLEAN_MODE == 'true': delete = 1 elif CLEAN_MODE == 'false': delete = 0 else: print "File %s does not belong in ViewVC %s." \ % (dst_path, version) while 1: temp = raw_input("Do you want to [D]elete it, or [L]eave " "it as is? ") temp = string.lower(temp[0]) if temp == "l": delete = 0 elif temp == "d": delete = 1 if delete is not None: break assert delete is not None if delete: print " deleted %s" % (os.path.join(dst_path, fname)) os.unlink(os.path.join(destdir_path, fname)) else: print " preserved %s" % (os.path.join(dst_path, fname)) def usage_and_exit(errstr=None): stream = errstr and sys.stderr or sys.stdout stream.write("""Usage: %s [OPTIONS] Installs the ViewVC web-based version control repository browser. Options: --help, -h, -? Show this usage message and exit. --prefix=DIR Install ViewVC into the directory DIR. If not provided, the script will prompt for this information. --destdir=DIR Use DIR as the DESTDIR. This is generally only used by package maintainers. If not provided, the script will prompt for this information. --clean-mode= If 'true', overwrite existing ViewVC configuration files found in the target directory, and purge Python modules from the target directory that aren't part of the ViewVC distribution. If 'false', do not overwrite configuration files, and do not purge any files from the target directory. If not specified, the script will prompt for the appropriate action on a per-file basis. """ % (os.path.basename(sys.argv[0]))) if errstr: stream.write("ERROR: %s\n\n" % (errstr)) sys.exit(1) else: sys.exit(0) if __name__ == "__main__": # Option parsing. try: optlist, args = getopt.getopt(sys.argv[1:], "h?", ['prefix=', 'destdir=', 'clean-mode=', 'help']) except getopt.GetoptError, e: usage_and_exit(str(e)) for opt, arg in optlist: if opt == '--help' or opt == '-h' or opt == '-?': usage_and_exit() if opt == '--prefix': ROOT_DIR = arg if opt == '--destdir': DESTDIR = arg if opt == '--clean-mode': arg = arg.lower() if arg not in ('true', 'false'): usage_and_exit("Invalid value for --overwrite parameter.") CLEAN_MODE = arg # Print the header greeting. print """This is the ViewVC %s installer. It will allow you to choose the install path for ViewVC. You will now be asked some installation questions. Defaults are given in square brackets. Just hit [Enter] if a default is okay. """ % version # Prompt for ROOT_DIR if none provided. if ROOT_DIR is None: if sys.platform == "win32": pf = os.getenv("ProgramFiles", "C:\\Program Files") default = os.path.join(pf, "viewvc-" + version) else: default = "/usr/local/viewvc-" + version temp = string.strip(raw_input("Installation path [%s]: " \ % default)) print if len(temp): ROOT_DIR = temp else: ROOT_DIR = default # Prompt for DESTDIR if none provided. if DESTDIR is None: default = '' temp = string.strip(raw_input( "DESTDIR path (generally only used by package " "maintainers) [%s]: " \ % default)) print if len(temp): DESTDIR = temp else: DESTDIR = default # Install the files. print "Installing ViewVC to %s%s:" \ % (ROOT_DIR, DESTDIR and " (DESTDIR = %s)" % (DESTDIR) or "") for args in FILE_INFO_LIST: apply(install_file, args) for args in TREE_LIST: apply(install_tree, args) # Print some final thoughts. print """ ViewVC file installation complete. Consult the INSTALL document for detailed information on completing the installation and configuration of ViewVC on your system. Here's a brief overview of the remaining steps: 1) Edit the %s file. 2) Either configure an existing web server to run %s. Or, copy %s to an already-configured cgi-bin directory. Or, use the standalone server provided by this distribution at %s. """ % (os.path.join(ROOT_DIR, 'viewvc.conf'), os.path.join(ROOT_DIR, 'bin', 'cgi', 'viewvc.cgi'), os.path.join(ROOT_DIR, 'bin', 'cgi', 'viewvc.cgi'), os.path.join(ROOT_DIR, 'bin', 'standalone.py')) viewvc-1.1.22/docs/0000755000175000017500000000000012265242267014242 5ustar cmpilatocmpilatoviewvc-1.1.22/docs/upgrading-howto.html0000644000175000017500000015244011330570531020242 0ustar cmpilatocmpilato ViewVC: Upgrading

Upgrading ViewVC

Introduction

This document describes some of the things that you will need to consider, change, or handle when upgrading an existing ViewVC or ViewCVS installation to a newer version.

Upgrading from an ancient version to the latest version isn't necessarily a multi step process. The instructions are only organized that way. You can certainly upgrade in a single step.

It is always recommended to install the new version in a fresh directory and to carefully compare the configuration files. A possible approach is to name the directories /usr/local/viewvc-1.0, /usr/local/viewvc-1.1 and so on and than create a symbolic link viewvc pointing to the production version. This way you can easily test several versions and switch back if your users start to complain.

Upgrading From ViewVC 1.0

This section discusses how to upgrade ViewVC 1.0.x to ViewVC 1.1.x.

root_as_url_component Now Enabled by Default

In ViewVC 1.1, the root_as_url_component configuration option is now enabled by default. This option causes ViewVC URLs to be of the form …/root-name/path-in-root[?…] instead of …/path-in-root/?root=root-name[&…], and makes for a much more intuitive user experience, navigation-wise, when ViewVC is serving up multiple version control repositories. When in this mode, ViewVC will automatically perform the obvious redirection for URLs which have a root= CGI parameter.

Unfortunately, there's a catch. Older URLs for the default root (specified by the default_root configuration option) were optimized to not include the root= CGI parameter. This means they look unfortunately similar to the newer root-in-the-path URL format, and ViewVC will not attempt to redirect them. But ViewVC won't be able to handle them, either. So, old-style default-root URLs, when aimed at a ViewVC for which the root_as_url_component option has been subsequently enabled, will result in an error. If you need to preserve the functionality of those old URLs, you'll need to either disable root_as_url_component, or use some other mechanism (like server URL rewriting) to morph them into compliance with the new URL format.

Path-Based Authorization / Forbidden Modules

ViewVC 1.1 introduces a new pluggable authorization (authz) subsystem which gives administrators greater control over the accessibility of the information ViewVC displays in its output. ViewVC provides a number of working authz modules and a framework for configuring them. But of specific interest to folks upgrading from ViewVC 1.0 is that one of these new modules has replaced the handling of forbidden modules. As such, the forbidden configuration option now lives under the configuration section specific to that authz module.

Migrating your existing configuration of forbidden modules should be fairly straightforward:

  1. In the new "authz-forbidden" section of viewvc.conf, set the forbidden option to the same value as the forbidden option in your ViewVC 1.0.x configuration's "general" section.
  2. In the new "authz-forbiddenre" section of viewvc.conf, set the forbiddenre option to the same value as the forbiddenre option in your ViewVC 1.0.x configuration's "general" section.
  3. Finally, ensure that that the new authorizer option is set to either "forbidden" or "forbiddenre", depending on which of those you were using in ViewVC 1.0.x.

Of course, you might wish to take advantage of another of the provided authz modules. Or, you might wish to write a brand new one for your purposes. The flexibility is yours.

Known Issues:

  • ViewVC does not provide an authentication framework. It does, however, inherit authenticated usernames as determined by the HTTP server (Apache, e.g.) via the CGI environment. So, any authorization module that assigns privileges based on usernames will work only if ViewVC is deployed in a way that requires successful authentication (as opposed to allowing anonymous access).
  • Currently, the root listing view only honors the global or vhost-specific configurations, not any root-specific configuration. In the event that ViewVC is using root-specific configuration for its authorization stuffs, this may cause either the unintended leak of root names to users or the inability to see roots at all. However, for root-specific ViewVC views, all configuration — include root-specific configuration — is honored. If you are concerned about leaking root names in the root listing view, you might consider disabling that view altogether by removing roots from the list of views specified in the allowed_views configuration option.
  • The experimental module which allows ViewVC to serve up views of remote Subversion repositories is not yet fully integrated with the authorization subsystem, and almost certainly will leak privileged data. Sorry. That's (one reason) why it's experimental.

Syntax Highlighting

ViewVC 1.0.x supports syntax highlighting provided by multiple third-party highlighting engines, including GNU enscript, GNU source-highlight, highlight, php, and py2html. Unfortunately, each of those integrations worked differently than the others. Some supported line numbers, some didn't; some were under active development and recognized newer languages; some weren't; each had its own set of CSS stylations that needed to be customized; etc.

In ViewVC 1.1, we've dropped support for all of those integations in favor of a single integration with Pygments, a Python-module-based syntax highlighting engine. As such, the configuration options for the various other engines (both those that enabled the integration and those that specified the locations of the third-party tools) have been removed from ViewVC, and have been replaced by a single new configuration option: enable_syntax_coloration.

The list of removed options is as follows:

  • options/enscript_path
  • options/highlight_convert_tabs
  • options/highlight_path
  • options/markup_line_numbers
  • options/php_exe
  • options/py2html_path
  • options/use_enscript
  • options/use_highlight
  • options/use_pagesize
  • options/use_php
  • options/use_py2html

Checkin Database

ViewVC 1.1 introduces to the cvsdbadmin and svndbadmin tools a new "purge" operation, which allows you to remove all the information related to a given root from your checkins database (without disturbing the information associated with other roots). Likewise, the "rebuild" command in those tools now implies a "purge" followed by an update.

As a related change, the svndbadmin program's "rebuild" subcommand has had its purpose become more defined. It no longer accepts a revision argument, and therefore can now only be used to completely rebuild the entirety of the checkin database information for a Subversion repository (instead of being able to only update the information related to single Subversion revision). For per-revision updating, use svndbadmin update and provide a revision (or revision range). And to get the previous rebuild-a-revision effect, pass the new --force option to svndbadmin update.

In other words, where you once did this:

$ svndbadmin rebuild /path/to/repository 1234

you now need to do this:

$ svndbadmin update /path/to/repository 1234 --force

To enhance the performance of the new "purge" operation, ViewVC 1.1 introduces some slight changes to the checkin database schema. If you use the make-database tool to (re)create your checkins database, it will by default employ the new database schema. This should cause the database to be virtually unusable by previous versions of ViewVC, and that's by design. If, however, you need to (re)create your checkins database and you require compatibility with previous versions of ViewVC or ViewCVS, simply pass the "--version=1.0" option to the make-database script. Note that your "purge" and "rebuild" operations could be abysmally slow, though, as that version of the database schema is not optimized for those operations.

Configuration Options

This section covers changes to configuration options not already discussed in other sections pertaining to this upgrade.

In ViewVC 1.1, a new "utilities" section was added to the viewvc.conf file. All the options used for configuring the locations of various helper applications that ViewVC uses which were previously scattered throughout the configuration file are now all centralized in this one new section. To accomplish this, the following options were added:

  • utilities/cvsgraph
  • utilities/cvsnt
  • utilities/diff
  • utilities/rcs_dir
  • utilities/svn

…and these were removed:

  • general/cvsnt_ext_path
  • general/rcs_path
  • general/svn_path
  • options/cvsgraph_path

All the options which governed which ViewVC views were enabled have been consolidated into a single new option. This new option:

  • options/allowed_views

…replaces these, which have been removed:

  • options/allow_annotate
  • options/allow_markup
  • options/allow_tar

ViewVC now honors the "svn:mime-type" property stored on Subversion-versioned files as the primary source of MIME type determination (before falling back to name-based MIME mappings and such). However, this can negatively affect the viewability of certain files — especially images — whose "svn:mime-type" properties are set incorrectly, such as will happen if Subversion itself merely determines that the file isn't human-readable and uses the "application/octet-stream" MIME type to record this determination. To make ViewVC not honor the "svn:mime-type" property value, set the svn_ignore_mimetype configuration option.

Speaking of MIME types, the option mime_types_file is now mime_types_files, as it now carries multiple paths to MIME mappings files, ordered by preference.

The use_rcsparse option was moved from the "general" section to the "options" section.

The log_sort option's value "cvs" has been renamed to "none" (for general application across all supported version control systems).

Custom sections which define per-virtual-host configuration option overrides must now have their names prefixed with "vhost-". Also, instead of a hyphen (-) between the virtual host name and the base configuration section being overridden, now there should be a forward slash character (/). For example, the following configuration which was valid in ViewVC 1.0 is no longer valid:

[vhosts]
all = viewvc.*

[all-options]
allowed_views = annotate, diff, markup, tar

This now needs to be written like so:

[vhosts]
all = viewvc.*

[vhost-all/options]
allowed_views = annotate, diff, markup, tar

The following is a grab-bag of additional new options:

  • options/hide_errorful_entries
  • options/mangle_email_addresses

Templates

This section describes template variable changes introduced in this release. See the Template Authoring Guide for the current set of variables available to each templates.

One notable change in ViewVC 1.1 is that the markup.ezt and annotate.ezt templates have been combined into a single file.ezt template.

Also, the configuration options under the [templates] section are now paths relative to the configured template directory (either the value of the options/template_dir option, or the default "templates" directory), instead of being relative to the configuration location.

Variable Location Changes
rootpath all templates added
change_root_action all templates removed
change_root_hidden_values all templates removed
roots all templates except roots.ezt removed
roots.path roots.ezt added
queryform_href diff.ezt, file.ezt, graph.ezt, log.ezt, log_table.ezt, query_form.ezt, revision.ezt, roots.ezt added
tarball_href diff.ezt, file.ezt, graph.ezt, log.ezt, log_table.ezt, query_form.ezt, query_results.ezt, revision.ezt, roots.ezt added
properties directory.ezt, file.ezt added
properties.name directory.ezt, file.ezt added
properties.undisplayable directory.ezt, file.ezt added
properties.value directory.ezt, file.ezt added
diff_format_hidden_values diff.ezt now is an iterable list of objects with .name and .value attributes
diff_type diff.ezt new value: f
date_left diff.ezt renamed to left.date
path_left diff.ezt renamed to left.path
rev_left diff.ezt renamed to left.rev
tag_left diff.ezt renamed to left.tag
date_right diff.ezt renamed to right.date
path_right diff.ezt renamed to right.path
rev_right diff.ezt renamed to right.rev
tag_right diff.ezt renamed to right.tag
annotate_href diff.ezt removed, use right.annotate_href instead
left.annotate_href diff.ezt added
left.download_href diff.ezt added
left.download_text_href diff.ezt added
left.prefer_markup diff.ezt added
left.revision_href diff.ezt added
left.view_href diff.ezt added
right.annotate_href diff.ezt added
right.download_href diff.ezt added
right.download_text_href diff.ezt added
right.prefer_markup diff.ezt added
right.revision_href diff.ezt added
right.view_href diff.ezt added
right.view_href diff.ezt added
dir_paging_hidden_values directory.ezt, , dir_alternate.ezt now is an iterable list of objects with .name and .value attributes
entries.log directory.ezt, dir_alternate.ezt now always contains untruncated log message
entries.short_log directory.ezt, dir_alternate.ezt added
search_re_hidden_values directory.ezt, dir_alternate.ezt now is an iterable list of objects with .name and .value attributes
search_tag_hidden_values directory.ezt, dir_alternate.ezt now is an iterable list of objects with .name and .value attributes
pathrev_clear_hidden_values log.ezt, log_table.ezt, directory.ezt, dir_alternate.ezt now is an iterable list of objects with .name and .value attributes
pathrev_hidden_values log.ezt, log_table.ezt, directory.ezt, dir_alternate.ezt now is an iterable list of objects with .name and .value attributes
annotate_href log.ezt, log_table.ezt renamed to head_annotate_href
diff_form_hidden_values log.ezt, log_table.ezt now is an iterable list of objects with .name and .value attributes
download_href log.ezt, log_table.ezt renamed to head_download_href
download_text_href log.ezt, log_table.ezt renamed to head_download_text_href
log_paging_hidden_values log.ezt, log_table.ezt now is an iterable list of objects with .name and .value attributes
logsort_hidden_values log.ezt, log_table.ezt now is an iterable list of objects with .name and .value attributes
prefer_markup log.ezt, log_table.ezt renamed to head_prefer_markup
view_href log.ezt, log_table.ezt renamed to head_view_href
rss_link_href query.ezt, rss.ezt added
query_hidden_values query_form.ezt now is an iterable list of objects with .name and .value attributes
jump_rev_hidden_values revision.ezt now is an iterable list of objects with .name and .value attributes
num_changes revision.ezt added

Upgrading From ViewCVS 0.9

This section discusses how to upgrade ViewCVS 0.9 to ViewVC 1.0.

CGI Stubs

The CGI stub scripts haved been moved from <VIEWVC_INSTALLATION_DIRECTORY>/cgi/ to <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/, so you will need update any ScriptAlias directives pointing to them in your apache configuration. Also, the contents of these scripts have changed, so you may need to replace copies of the old scripts you put in other directories.

Checkin Database

ViewVC 1.0 reads and writes commit times in the MySQL database in UTC time rather than local time. This can cause times displayed on the query page to be a few hours off if an old database is being used with a new version of ViewVC. The best way to fix this is to rebuild the database with the new version of cvsdbadmin, but it is also possible to enable a backwards compatibility mode by setting utc_time = 0 at the top of lib/dbi.py

"checkout_magic" Option

In ViewVC 1.0, the checkout_magic option has been disabled by default to provide a simpler URL scheme that works safely with URL authorization. Most users will not notice any difference in behavior, but users who had been using ViewCVS to browse the contents of static HTML pages stored in a repository may notice that links and images in those pages targetted at other files in the repository no longer display correctly. The new default_file_view option can be used to solve this problem and, if neccessary, checkout_magic can also be re-enabled. The viewcvs.conf file describes these options in detail.

Configuration Options

The following options have been added:

  • general/svn_roots
  • general/root_parents
  • general/use_rcsparse
  • general/cvsnt_exe_path
  • options/template_dir
  • options/docroot
  • options/http_expiration_time
  • options/generate_etags
  • options/root_as_url_component
  • options/default_file_view
  • options/sort_group_dirs
  • options/dir_pagesize
  • options/log_pagesize
  • options/limit_changes
  • options/use_localtime
  • options/cross_copies
  • options/use_highlight
  • options/highlight_path
  • options/highlight_line_numbers
  • options/highlight_convert_tabs
  • options/use_php
  • options/php_path
  • options/svn_path
  • templates/error
  • templates/query_form
  • templates/query_results
  • cvsdb/port
  • cvsdb/rss_row_limit

The following options have been removed:

  • general/main_title
  • options/diff_font_face
  • options/diff_font_size
  • options/disable_enscript_lang
  • templates/footer

Templates

The templates have changed drastically in this version of ViewVC. If you are using customized templates from 0.9 or earlier, you will want to port your old customizations to the new template files instead of trying to get the old template files to work with the new ViewVC.

There is a new Template Authoring Guide for ViewVC 1.0 templates that can help you with your customizations. And the chart below lists all 0.9 template variables and shows what's become of them in 1.0.

Variable Location Changes
ago markup.ezt unchanged
author markup.ezt unchanged
back_url log.ezt, log_table.ezt replaced by up_href, which doesn't include the current #file anchor
branch log.ezt, log_table.ezt replaced by default_branch, which is a list instead of a string
branch query.ezt unchanged
branch_names log.ezt, log_table.ezt renamed to branch_tags
branch_points markup.ezt unchanged
branch_tags dir_alternate.ezt, directory.ezt unchanged
branches markup.ezt unchanged
cfg.general.address footer.ezt unchanged
cfg.general.main_title dir_alternate.ezt, directory.ezt, query.ezt removed, used to be a string from the configuration file that was shown in the title of the root directory page.
cfg.options.use_cvsgraph dir_alternate.ezt, directory.ezt unchanged
cfg.options.use_re_search dir_alternate.ezt, directory.ezt unchanged
changed markup.ezt unchanged
changes diff.ezt attributes changed, see below
changes.extra diff.ezt renamed to changes.line_info_extra
changes.have_left diff.ezt unchanged
changes.have_right diff.ezt unchanged
changes.left diff.ezt unchanged
changes.line1 diff.ezt renamed to changes.line_info_left
changes.line2 diff.ezt renamed to changes.line_info_right
changes.right diff.ezt unchanged
changes.type diff.ezt new values binary_diff and error
commits query.ezt unchanged
commits.desc query.ezt unchanged
commits.files query.ezt unchanged
commits.files.author query.ezt unchanged
commits.files.branch query.ezt unchanged
commits.files.date query.ezt unchanged
commits.files.link query.ezt unchanged
commits.files.minus query.ezt unchanged
commits.files.plus query.ezt unchanged
commits.files.rev query.ezt unchanged
current_root dir_alternate.ezt, directory.ezt renamed to rootname
date query.ezt unchanged
date1 diff.ezt renamed to date_left
date2 diff.ezt renamed to date_right
diff_format diff.ezt, log.ezt, log_table.ezt unchanged
directory query.ezt unchanged
entries log.ezt, log_table.ezt attributes changed, see below
entries.ago log.ezt, log_table.ezt unchanged
entries.author log.ezt, log_table.ezt unchanged
entries.branch_names log.ezt unchanged
entries.branch_point log.ezt, log_table.ezt unchanged
entries.branch_points log.ezt, log_table.ezt unchanged
entries.branch_points.href log.ezt, log_table.ezt unchanged
entries.branch_points.name log.ezt, log_table.ezt unchanged
entries.branches log.ezt, log_table.ezt unchanged
entries.branches.href log.ezt, log_table.ezt unchanged
entries.branches.name log.ezt, log_table.ezt unchanged
entries.changed log.ezt unchanged
entries.href log.ezt, log_table.ezt renamed to entries.download_href
entries.html_log log.ezt, log_table.ezt renamed to entries.log
entries.next_main log.ezt, log_table.ezt unchanged
entries.prev log.ezt, log_table.ezt unchanged
entries.rev log.ezt, log_table.ezt unchanged
entries.state log.ezt, log_table.ezt unchanged
entries.tag_names log.ezt unchanged
entries.tags log.ezt, log_table.ezt unchanged
entries.tags.href log.ezt unchanged
entries.tags.name log.ezt, log_table.ezt unchanged
entries.text_href log.ezt, log_table.ezt renamed to entries.download_text_href
entries.to_selected log.ezt, log_table.ezt combined into diff_to_sel_href variable
entries.utc_date log.ezt, log_table.ezt renamed to entries.date
entries.vendor_branch log.ezt, log_table.ezt unchanged
entries.view_href log.ezt, log_table.ezt unchanged
file query.ezt unchanged
file_url header.ezt renamed to log_href
filename header.ezt removed, used to be set to the name of the file being shown
files_shown dir_alternate.ezt, directory.ezt unchanged
graph_href log.ezt, log_table.ezt unchanged
has_tags dir_alternate.ezt, directory.ezt removed, used to be a boolean that was true when either tag information was available from the current directory or a tag was set. Determined whether or not to show a tag selector box on the bottom of the directory page.
have_logs dir_alternate.ezt, directory.ezt removed, used to be a boolean that was true whenever any logs were being shown in a directory listing. When it was false the template code would omit the log column from the directory table.
head_abs_href log.ezt, log_table.ezt renamed to download_href
head_href log.ezt, log_table.ezt replaced by view_href and download_href
hidden_values diff.ezt, log.ezt, log_table.ezt combined into *_hidden_values variables
hide_attic_href dir_alternate.ezt, directory.ezt unchanged
hours query.ezt unchanged
href log.ezt, log_table.ezt combined into *_href variables
href markup.ezt renamed to download_href
human_readable log.ezt, log_table.ezt unchanged
imagemap graph.ezt unchanged
log markup.ezt unchanged
logsort log.ezt, log_table.ezt unchanged
mime_type log.ezt, log_table.ezt, markup.ezt unchanged
nav_file markup.ezt replaced with nav_path
nav_path dir_alternate.ezt, directory.ezt, header.ezt, log.ezt, log_table.ezt changed from a block of HTML to a list of path components
no_match dir_alternate.ezt, directory.ezt removed, used to be a boolean that was true when a directory contained files, but none of them could be displayed due to regular expression or view tag filtering. Would trigger an error message.
num_commits query.ezt unchanged
num_files dir_alternate.ezt, directory.ezt removed, used to be a count of files in a directory, including dead and filtered files. This number was only shown in the no_match error message.
params dir_alternate.ezt, directory.ezt replaced by search_re_hidden_values
path header.ezt removed, used to be set to the directory path of the file being shown
plain_tags dir_alternate.ezt, directory.ezt unchanged
prev markup.ezt unchanged
qquery header.ezt combined into log_href variable
qquery log.ezt, log_table.ezt replaced by diff_select_hidden_values
query log.ezt, log_table.ezt combined into *_href variables
query query.ezt unchanged
repository query.ezt unchanged
request.amp_query graph.ezt combined into imagesrc variable
request.script_name dir_alternate.ezt, directory.ezt, log.ezt, log_table.ezt combined into *_href variables
request.url diff.ezt combined into diff_format_action variable
request.url graph.ezt combined into imagesrc variable
request.where graph.ezt renamed to just where
rev graph.ezt removed, used to be set to the value of the "graph" parameter in CvsGraph page urls. The value was passed on through the rev parameter to CvsGraph image URLs, where, oddly enough, it was ignored. It'd be set to a file revision number in directory page graph links, and just "1" in log page graph links.
rev graph.ezt, header.ezt, markup.ezt unchanged
rev1 diff.ezt renamed to rev_left
rev2 diff.ezt renamed to rev_right
rev_selected log.ezt, log_table.ezt unchanged
roots dir_alternate.ezt, directory.ezt changed to a list of objects instead of a list of strings
rows dir_alternate.ezt, directory.ezt replaced by entries
rows.anchor dir_alternate.ezt, directory.ezt renamed to entries.anchor
rows.author dir_alternate.ezt, directory.ezt renamed to entries.author
rows.cvs dir_alternate.ezt, directory.ezt replaced by entries.errors
rows.graph_href dir_alternate.ezt, directory.ezt renamed to entries.graph_href
rows.href dir_alternate.ezt, directory.ezt renamed to entries.log_href
rows.log dir_alternate.ezt, directory.ezt renamed to entries.short_log
rows.log_file dir_alternate.ezt, directory.ezt renamed to entries.log_file
rows.log_rev dir_alternate.ezt, directory.ezt renamed to entries.log_rev
rows.name dir_alternate.ezt, directory.ezt renamed to entries.name
rows.rev dir_alternate.ezt, directory.ezt renamed to entries.rev
rows.rev_href dir_alternate.ezt, directory.ezt replaced by entries.view_href and entries.download_href
rows.show_log dir_alternate.ezt, directory.ezt removed, used to be a boolean that was true whenever a log message was present for the directory entry.
rows.state dir_alternate.ezt, directory.ezt renamed to entries.state
rows.time dir_alternate.ezt, directory.ezt renamed to entries.ago
rows.type dir_alternate.ezt, directory.ezt renamed to entries.pathtype
search_re dir_alternate.ezt, directory.ezt unchanged
selection_form dir_alternate.ezt, directory.ezt renamed to search_re_form
show_attic_href dir_alternate.ezt, directory.ezt unchanged
sortby dir_alternate.ezt, directory.ezt, query.ezt unchanged
sortby_author_href dir_alternate.ezt, directory.ezt unchanged
sortby_date_href dir_alternate.ezt, directory.ezt unchanged
sortby_file_href dir_alternate.ezt, directory.ezt unchanged
sortby_log_href dir_alternate.ezt, directory.ezt unchanged
sortby_rev_href dir_alternate.ezt, directory.ezt unchanged
state markup.ezt unchanged
tag markup.ezt renamed to pathrev
tag1 diff.ezt renamed to tag_left
tag2 diff.ezt renamed to tag_right
tags log.ezt, log_table.ezt unchanged
tags markup.ezt unchanged
tags.name log.ezt, log_table.ezt unchanged
tags.rev log.ezt, log_table.ezt unchanged
tags markup.ezt unchanged
tarball_href dir_alternate.ezt, directory.ezt unchanged
text_href markup.ezt renamed to download_text_href
tr1 log.ezt, log_table.ezt removed, used to be a default value for the first text field in the diff selector form. In 1.0, the default value is computed in the templates.
tr2 log.ezt, log_table.ezt removed, used to be a default value for the second text field in the diff selector form. In 1.0, the default value is computed in the templates.
unreadable dir_alternate.ezt, directory.ezt removed, used to be a boolean that was true whenever any of the files in the directory listing were 'unreadable.' It would trigger a generic error message at the bottom of the page.
utc_date markup.ezt renamed to date
vendor_branch markup.ezt unchanged
view_tag dir_alternate.ezt, directory.ezt, log.ezt, log_table.ezt renamed to pathrev
viewable log.ezt, log_table.ezt renamed to prefer_markup
vsn footer.ezt unchanged
where diff.ezt, dir_alternate.ezt, directory.ezt, log.ezt, log_table.ezt unchanged
who query.ezt unchanged

Template Arrangement

The default templates have been rearranged a bit in ViewVC 1.0. Specifically, "header.ezt" and "footer.ezt" have moved into the "templates/include/" subdirectory. Also, "directory.ezt" and "dir_alternate.ezt" now reference new template files "dir_header.ezt" and "dir_footer.ezt", also found in the "templates/include/" subdirectory.

Notably, the "markup.ezt" and "annotate.ezt" templates are now fully self-contained. That is, the markup and annotation data generated by ViewVC is now accessible in those templates just like other template variables. As a result, ViewVC now has no more internal need for the templates.footer configuration variable, so that variable has been removed from the default configuration file.

Upgrading From ViewCVS 0.8

This section discusses how to upgrade ViewCVS 0.8 to version 0.9 or a later version of the software.

NOTE: these changes will bring you up to the requirements of version 0.9. You must also follow the directions for upgrading from 0.9.

Configuration Options

More templates were introduced in version 0.8 of the software, which made many of the configuration options obsolete. This section covers which options were removed. If you made any changes to these options, then you will need to make corresponding changes in the templates.

Colors: diff_heading, diff_empty, diff_remove, diff_change, diff_add, and diff_dark_change
These options have been incorporated into the diff.ezt template.

markup_log
This option has been incorporated into the markup.ezt template.

Colors: nav_header and alt_background
These options have been incorporated into the header.ezt template.

Images: back_icon, dir_icon, and file_icon
These options have been incorporated into the directory.ezt, header.ezt, log.ezt, log_table.ezt, and query.ezt templates.

use_java_script and open_extern_window
The templates now use JavaScript in all applicable places, and open external windows for most downloading and viewing of files. If you wish to not use JavaScript and/or external windows, then remove the feature(s) from the templates.

show_author
Changing this option would be quite strange and rare. If you do not want to show the author for the revisions, then you should remove it from the various templates.

hide_non_readable
This option was never used, so it has been removed.

flip_links_in_dirview
This option is no longer available. If you want the links in your directory view flipped, then you may use the dir_alternate.ezt template.

Template Variables

Some template variables that were available in 0.8 have been removed in 0.9. If you have custom templates that refer to these variables, then you will need to modify your templates.

directory.ezt: headers
The headers are now listed explicitly in the template, rather than made available through a list.

directory.ezt: rows.cols, and rows.span
These variables were used in conjunction with the headers variable to control the column displays. This is now controlled explicitly within the templates.

directory.ezt: rev_in_front
This was used to indicate that revision links should be used in the first column, rather than in their standard place in the second column. Changing the links should now be done in the template, rather than according to this variable. You may want to look at the dir_alternate.ezt template, which has the revision in front.

directory.ezt: rows.attic and rows.hide_attic_href
These variable were used to manage the hide and showing of the contents of the Attic/ subdirectory. Several new variables were introduced which can be used to replace this functionality: show_attic_href, hide_attic_href, and rows.state.

viewvc-1.1.22/docs/url-reference.html0000644000175000017500000012553411553372025017673 0ustar cmpilatocmpilato ViewVC 1.1 URL Reference

ViewVC 1.1 URL Reference

Introduction

This document describes the format of URLs accepted by ViewVC 1.1 and is intended to be useful to users and outside software developers who want to create links to ViewVC pages. It should also be useful to ViewVC developers as a way of tracking and documenting the many quirks of ViewVC's URL handling logic.

Future releases of ViewVC will support the URL formats described in this document. Certain URLs currently accepted by ViewVC, such as redirecting URLs that result from the submission of form elements on ViewVC pages, are not documented here and therefore may not be supported by future releases of ViewVC.

URL Components

A ViewVC URL consists of 3 components: a Script Location, a Repository Path, and some optional Query Parameters. Some examples:

ViewVC Script Location Repository Path Query Parameters
http://example.org/viewvc.cgi /some/file ?view=log&revision=1.34
http://example.org/viewvc.cgi /*checkout*/some/other/file ?revision=1.10.2.5
http://example.org/viewvc.cgi /some/dir/ ?pathrev=BRANCH_2_3
http://example.org/viewvc.cgi /some/dir.tar.gz ?view=tar&pathrev=BRANCH_2_3
http://example.org/viewvc.cgi /yet/another/file ?view=diff?r1=1.12&r2=1.14

ViewVC Script Location

The script location is a common base for all ViewVC URL's pertaining to a particular installation. It's whatever location the web server is configured to serve ViewVC pages from.

Repository Path

Since ViewVC is essentially a file system browser for repositories, repository paths referring to the files and directories being browsed get their own section in ViewVC URLs immediately following the script location. Repository paths are always case sensitive and separated by forward slashes, regardless of the underlying filesystem.

Repository paths can be given certain "magic" prefixes and suffixes. For example, a "/*checkout*" prefix can be added to file views to force a checkout even without an explicit "view=co" query parameter. And a ".tar.gz" suffix is added to download tarball URLs so downloaded tarballs will be saved with sensible default names.

If the root_as_url_component configuration option is enabled, the first directory name in a repository path (after any magic prefix) is taken to be the name (from the ViewVC configuration) of the repository to browse rather than the name of an actual directory. The repository name can also be specified in a "root=" query parameter instead (see root parameter below).

Paths beginning with "/*docroot*/" are an exception the rules above. These paths are used to provide access to files in the ViewVC template directory: image files, static HTML pages, and the ViewVC stylesheet. For more information, see the Docroot View syntax below.

Query Parameters

Following the ViewVC and repository locations in URLs is an optional query string, a list of parameter/value pairs written like "?param1=value1&param2=value2..." Some parameters, like "revision", "pathrev", and "root", augment repository path information, specifying revisions and repositories. Other parameters like "logsort" and "diff_format" control ViewVC display options. Some parameters are also "sticky" and get embedded into links inside the generated pages, sticking around for future page views.

ViewVC provides a number of different views of repository data including a directory listing view, a log view, a file diff view, and others. (The views are listed and described in the help_rootview.html ViewVC help page). URLs for each of these views accept specific parameters described in the URL Syntax section, but some parameters are used by multiple views and they are described below:

Parameter Description
view The name of the view to display, does not need to be explicitly specified in many URLs. Possible values are shown in the URL Syntax section below.
revision The revision or tag to display information about, used by several different views.
pathrev The current sticky revision (Subversion) or sticky tag (CVS), as described in the help_rootview.html ViewVC help page. In Subversion, because path information is revision controlled, this value is also used to look up paths in the repository, providing a means of accessing paths that no longer exist in HEAD.
root The name of the repository to browse. When the default_root configuration option is set or the root_as_url_component option is enabled, it is not neccessary to to specify this parameter. When the root_as_url_component option is enabled, ViewVC URLs with root parameters redirect to locations with the root values embedded in the repository paths.

URL Syntax

This section lists URL syntax for each ViewVC view. Parts of URLs which may vary shown as variables in UPPERCASE.

Annotate View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/PATH required file path to annotate

Query Parameters
Parameter Opt/Req Description
view=annotate depends view parameter. Not required when an annotate parameter is present
annotate=REVISION optional revision or tag to annotate
pathrev=PATHREV optional pathrev parameter
root=ROOT depends root parameter

Checkout View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/*checkout* optional magic prefix. If specified when the checkout_magic configuration option is disabled, ViewVC will redirect to a URL without the prefix.
/PATH required file path to check out

Query Parameters
Parameter Opt/Req Description
view=co depends view parameter, not needed if the default_file_view configuration variable is set to co, since that makes the checkout view the default view for file paths. Also not needed if the /*checkout* magic prefix or the revision parameter is present.
content-type=TYPE optional MIME type to send with checked out file, default is a guess based on file extension
revision=REVISION optional revision parameter
pathrev=PATHREV optional pathrev parameter
root=ROOT depends root parameter

Diff View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/PATH required file path to display diff of

Query Parameters
Parameter Opt/Req Description
view=diff optional view parameter
r1=R1 required starting revision or tag or the string "text" to indicate that TR1 value (below) should override this one
r2=R2 required ending revision or tag or the string "text" to indicate that TR2 value (below) should override this one
tr1=TR1 depends starting revision or tag, used if r1 parameter is present and set to "text"
tr2=TR2 depends ending revision or tag, used if r2 parameter is present and set to "text"
p1=P1 optional starting file path that can override the PATH value to allow files at two different paths to be compared
p2=P2 optional ending file path that can override the PATH value to allow files at two different paths to be compared
diff_format=DIFF_FORMAT optional value specifying the type of diff to display. Can be "u" for unified diff, "c" for context diff, "s" for side by side diff, "h" for human readable diff, "l" for long human readable diff, and "f" for a full human readable diff. If no value is specified the default depends on the diff_format configuration option.
pathrev=PATHREV optional pathrev parameter
root=ROOT depends root parameter

Directory Listing View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/PATH/ required directory path to view. If the trailing slash is omitted, ViewVC will redirect to a URL that has a trailing slash.

view parameter
Query Parameters
Parameter Opt/Req Description
view=dir optional
hideattic=HIDEATTIC optional "0" to show dead files in CVS directory listings or "1" to hide dead files. Default depends on the hide_attic configuration value.
search=SEARCH optional regular expression to search files in the directory with if use_re_search configuration option is enabled
sortby=SORTBY optional "file" "rev" "date" "author" or "log" to indicate how the directory listing should be sorted. Default depends on sortby configuration option.
sortdir=SORTBY optional "up" to sort directory in ascending order or "down" for descending order. Default is "up".
dir_pagestart=PAGE optional item number to start listing at if paging is enabled
pathrev=PATHREV optional pathrev parameter
root=ROOT depends root parameter

Docroot View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/*docroot* required magic prefix
/PATH required file path to retrieve. ViewVC will return the contents of the file located at PATH, relative to the docroot subdirectory of the directory specified in the template_dir configuration option.

Query Parameters
Parameter Opt/Req Description

Graph View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/PATH required file path to generate CvsGraph page for

Query Parameters
Parameter Opt/Req Description
view=graph required view parameter
root=ROOT depends root parameter

Graph Image View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/PATH required file path to generate CvsGraph image for

Query Parameters
Parameter Opt/Req Description
view=graphimg required view parameter
root=ROOT depends root parameter

Log View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/PATH required file or directory path to generate log for

Query Parameters
Parameter Opt/Req Description
view=log depends view parameter, does not need to be specified for file paths when the default_file_view configuration option is set to log. However it is recommended that the parameter be passed anyway for consistency with directory log URLs and compatibility with ViewVC installations that set default_file_view to co.
logsort=SORT optional "rev" to sort log entries by revision number or "date" to sort by date. Default depends on the log_sort configuration value.
log_pagestart=PAGE optional item number to start listing at if paging is enabled
r1=R1 optional current revision selected for diffs
diff_format=DIFF_FORMAT optional currently selected diff format
pathrev=PATHREV optional pathrev parameter
root=ROOT depends root parameter

Markup View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/PATH required file path to mark up

Query Parameters
Parameter Opt/Req Description
view=markup required view parameter
revision=REVISION optional revision parameter
pathrev=PATHREV optional pathrev parameter
root=ROOT depends root parameter

Patch View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/PATH required file path to display patch of

Query Parameters
Parameter Opt/Req Description
view=patch required view parameter
r1=R1 required starting revision or tag or the string "text" to indicate that TR1 value (below) should override this one
r2=R2 required ending revision or tag or the string "text" to indicate that TR2 value (below) should override this one
tr1=TR1 depends starting revision or tag, only used if r1 parameter is present and set to "text"
tr2=TR2 depends ending revision or tag, only used if r2 parameter is present and set to "text"
p1=P1 optional starting file path that can override the PATH value to allow files at two different paths to be compared
p2=P2 optional ending file path that can override the PATH value to allow files at two different paths to be compared
diff_format=DIFF_FORMAT optional value specifying the type of patch to display. Can be "u" for unified diff or "c" for context diff. If no value is specified the default depends on the diff_format configuration option.
pathrev=PATHREV optional pathrev parameter
root=ROOT depends root parameter

Query Form View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/PATH required file path to display query results from

Query Parameters
Parameter Opt/Req Description
view=queryform required view parameter
branch=BRANCH optional branch query string
branch_match=BRANCH_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of branch match
dir=DIR optional directory query string
file=FILE optional file query string
file_match=FILE_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of file match
who=WHO optional author query string
who_match=WHO_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of author match
comment=COMMENT optional log message query string
comment_match=COMMENT_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of log message match
querysort=SORT optional "date" "author" or "file" determining order of query results
date=DATE optional "hours" "day" "week" "month" "all" or "explicit" to filter query results by date
hours=HOURS optional number of hours back to include results from when DATE is "hours"
mindate=MINDATE optional earliest date to include results from when DATE is "explicit"
maxdate=MAXDATE optional latest date to include results from when DATE is "explicit"
limit_changes=LIMIT_CHANGES optional maximum number of files to list per commit in query results. Default is value of limit_changes configuration option
root=ROOT depends root parameter

Query View

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/PATH required file path to display query results from

Query Parameters
Parameter Opt/Req Description
view=query required view parameter
branch=BRANCH optional branch query string
branch_match=BRANCH_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of branch match
dir=DIR optional directory query string
file=FILE optional file query string
file_match=FILE_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of file match
who=WHO optional author query string
who_match=WHO_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of author match
comment=COMMENT optional log message query string
comment_match=COMMENT_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of log message match
querysort=SORT optional "date" "author" or "file" determining order of query results
date=DATE optional "hours" "day" "week" "month" "all" or "explicit" to filter query results by date
hours=HOURS optional number of hours back to include results from when DATE is "hours"
mindate=MINDATE optional earliest date to include results from when DATE is "explicit"
maxdate=MAXDATE optional latest date to include results from when DATE is "explicit"
format=FORMAT optional "rss" or "backout" values to generate an rss feed or list of commands to back out changes instead showing a normal query result page
limit_changes=LIMIT_CHANGES optional maximum number of files to list per commit in query results. Default is value of limit_changes configuration option
root=ROOT depends root parameter

Revision View

Path Components (in order of appearance in the URL)
Component Opt/Req Description

Query Parameters
Parameter Opt/Req Description
view=revision required view parameter
revision=REVISION optional revision parameter
limit_changes=LIMIT_CHANGES optional maximum number of files to list per commit. Default is value of limit_changes configuration option
root=ROOT depends root parameter

Repository Listing

Path Components (in order of appearance in the URL)
Component Opt/Req Description

Query Parameters
Parameter Opt/Req Description
view=roots depends view parameter. Not required if the root_as_url_component configuration is enabled or the default_root option is not set.

Tarball Download

Path Components (in order of appearance in the URL)
Component Opt/Req Description
/PATH required directory path to download
.tar.gz depends magic suffix. Only required when the name of the directory being downloaded ends in ".tar.gz" and the parent parameter not is present. But it is recommended to add the magic suffix to all tarball URLs to avoid this special case and give the downloaded files sensible default names.

Query Parameters
Parameter Opt/Req Description
view=tar required view parameter
parent=1 optional If the parent parameter is specified, the last component of the PATH is discarded before it is ever looked up in the repository. This feature is used when the root_as_url_component configuration option is disabled to allow root tarball URLs to be saved with names like "ROOT-root.tar.gz".
pathrev=PATHREV optional pathrev parameter
root=ROOT depends root parameter

Backwards Compatibility

ViewVC's URL format has changed a lot over time, but ViewVC goes out of its way to support URLs using older formats so there aren't broken links when an installation of ViewVC is upgraded. The support is implemented as a set of URL transformations that recognize elements of old-style URLs and convert them to newer equivalents. If any transformations are applied (with some exceptions, mentioned below), ViewVC will issue a single redirect to the transformed URL. Descriptions of the transformations follow.

'view=rev' Parameter ⇒ 'view=revision'

URLs with a view=rev parameter will automatically be redirected to URLs with a view=revision parameter instead.

'cvsroot' Parameter ⇒ 'root'

URLs with a cvsroot parameter will automatically be redirected to URLs with a root parameter instead.

'only_with_tag' Parameter ⇒ 'pathrev'

URLs with an only_with_tag parameter will automatically be redirected to URLs with a pathrev parameter instead.

'~checkout~' Magic Path Prefix ⇒ '*checkout*'

URLs with a ~checkout~ path prefix get interpreted just like URLs with a '*checkout*' prefix. There is currently no redirect, but there could be in the future.

'*checkout*' Magic Path Prefix ⇒ 'view=co'

When the checkout_magic configuration option is disabled, URLs with a *checkout* magic prefix will redirect to an equivalent URL that does not use the prefix.

'root' Parameter ⇒ Root Path Component

When the root_as_url_component configuration option is enabled, URLs with a root parameter will redirect to an equivalent URL with the root name embedded in the path.

'rev' Parameter ⇒ 'revision' and 'pathrev'

CVS URLs with a rev parameter will redirect to URLs with a revision parameter instead. Subversion URLs with a rev parameter will redirect to URLs with a pathrev parameter, in order to account for the how the Subversion backend used to look up paths before pathrev was introduced.

'.diff' Suffix ⇒ Diff View

When ViewVC encounters a invalid repository path that ends in .diff, and stripping that ending yields a valid file path, it will redirect to a diff view of the file.

'.tar.gz' Suffix ⇒ 'view=tar'

When ViewVC encounters a invalid repository path that ends in .tar.gz, /root.tar.gz, or /REPOS-root.tar.gz, and stripping the ending yields a valid directory path, it will redirect to a URL to download a tarball of the directory.

'tarball=1' Parameter ⇒ 'view=tar'

A tarball=1 parameter is treated pretty much like a view=tar parameter. There is no redirect when it is encountered, but there could be in the future.

'graph=1' Parameter ⇒ 'view=graph'

A graph=1 parameter is treated like a view=graph parameter. There is currently no redirect when it is encountered, but there could be one in the future.

'graph=1&makeimage=1' Parameters ⇒ 'view=graphimg'

A graph=1&makeimage=1 parameter is treated like a view=graph parameter. There is currently no redirect when it is encountered, but there could be one in the future.

'content_type=text/vnd.viewcvs-markup' and 'content_type=text/x-cvsweb-markup' Parameters⇒ 'view=markup'

content-type=text/vnd.viewcvs-markup and content-type=text/x-cvsweb-markup parameters are treated like a view=markup parameter. There is currently no redirect when it is encountered, but there could be one in the future. Other values of the content-type parameter, which were used to dictate the MIME type of files displayed in the checkout/download view prior to ViewVC 1.0.6, are ignored.

'Attic/FILE' Paths ⇒ 'FILE'

When ViewVC encounters an invalid repository path whose last or second-to-last component is named Attic, and stripping the component yields a valid path, it will redirect to a URL with that path.

viewvc-1.1.22/docs/template-authoring-guide.html0000644000175000017500000020550511654521641022040 0ustar cmpilatocmpilato ViewVC 1.1 Template Authoring Guide

ViewVC 1.1 Template Authoring Guide

Introduction

This document represents an (unfinished) attempt at providing instructions for how to customize ViewVC's HTML output via modification of its templates.

Using EZT

### TODO ###

Variables Available to ViewVC Templates

Common Template Variable Set (COMMON)

Variable Type Description
annotate_href String URL of the ViewVC annotation view for the current resource. Valid only when pathtype is file.
cfg Object Representation of the object used by ViewVC for runtime configuration parameters such as those parsed from viewvc.conf. Dot-qualified children of this object map to configuration sections and option keys. For example, cfg.options.show_logs contains the value of the show_logs variable in the options section of the configuration file.
docroot String URL of the static documents directory, generally used for referencing stylesheets and images stored in and under that directory (which is typically relative to the template location).
download_href String ViewVC file contents download URL for the current resource. Valid only when pathtype is file.
download_text_href String ViewVC file contents as-text download URL for the current resource. Valid only when pathtype is file.
graph_href String URL of the ViewVC revision graph view for the current resource. Valid only when pathtype is file.
kv Object Representation of the object used by ViewVC for user-defined key/value mappings. Dot-qualified children of this object map to named key/value files and the sections and keys within them. For example, kv.l10n.labels.directory maps to the value of the directory key under the labels section of the key/value file whose configured abstract name is l10n.
lockinfo String Information about the lock status of the current resource.
log_href String URL of the ViewVC revision log view for the current resource. Valid only when pathtype is file or (for Subversion roots) dir.
nav_path List Ordered list of path components from the repository root to the current resource.
nav_path.href String URL of the default ViewVC view for the path component.
nav_path.name String Name of the path component.
pathtype String Path kind of the current resource. Valid values: file (file), dir (directory); may be empty.
prefer_markup Boolean Indicates whether to make the default file link a link to the markup page instead of the checkout page. Valid only when pathtype is file.
queryform_href String URL for a query form returning results from this directory. Valid only when pathtype is dir.
rev String Revision of the current resource.
revision_href String URL of the Subversion revision view for the current revision.
rootname String Name of the current repository (root).
roots_href String URL of ViewVC root listing view. Valid only when ViewVC is configured in roots-as-url-components mode.
rootpath String Server-local location of the current repository. WARNING: Revealing information to untrusted guests about the details of your server configuration can have negative security implications. Use this token at your own risk.
roottype String Version control type of the current repository (root). Valid values: cvs (CVS), svn (Subversion).
rss_href String URL of RSS feed for current location.
tarball_href String URL to download tarball of the current directory.
up_href String Link to the current object's parent directory view.
username String Authenticated username of the requesting user.
view String Name of the current view. Valid values: annotate (annotation view), diff (file difference view), roots (root listing view), dir (directory listing view), graph (revision graph view), log (revision log view), markup (file contents view), query (revision history query results view), queryform (revision history query form view), rev (revision/changeset view).
view_href String URL of the ViewVC file contents view for the current resource. Valid only when pathtype is file or dir.
vsn String ViewVC version identifier.
where String Path (relative to the current repository root) of the current resource.

Path Revision Form Variable Set (PATHREV)

Variable Type Description
lastrev String If the current path is deleted in a future revision, last revision where the path is available. (Subversion only)
pathrev String Current sticky revision (Subversion) or sticky tag (CVS)
pathrev_action String Form action URL for the sticky revision/tag selection form.
pathrev_hidden_values List Hidden field name/value pairs for the revision/tag selection form.
pathrev_clear_action String Form action URL for the path revision clear button.
pathrev_clear_hidden_values List Hidden field name/value pairs for the path revision clear button.

Paging Form Variable Set (PAGING)

Variable Type Description
picklist List List of pages that make up the current directory or log view.
picklist.count String Number of the first item on the page (indexed from 0)
picklist.end String Name of last item on the page
picklist.more Boolean If set, indicates that this picklist item is a placeholder for an unspecified number of additional pages. In this case, picklist.end is undefined.
picklist.page String Page number (indexed from 1)
picklist.start String Name of first item on the page
picklist_len String Number of pages in picklist

Property Listing Variable Set (PROPERTIES)

Variable Type Description
properties List List of item properties set on the current directory, minus those with undisplayable names.
properties.name String Name of an item property.
properties.undisplayable Boolean Indicates whether or not the value of this property is undisplayable (by virtue of not being UTF-8 text).
properties.value String Value of this property.

File Contents View (file.ezt)

Variable Type Description
Includes all variables from the COMMON and PROPERTIES variable sets
ago String Text description of the time elapsed since date.
annotation String Valid values are "none" (no annotations were attempted), "annotated" (annotation was successful), "binary" (file contents are not line-based and human-readable), and "error" (something went wrong during annotation).
author String Author of the revision being viewed.
branch_points String List of branch tag names which branch off of the revision being viewed (CVS only).
branches List If revision currently being viewed is on a branch, list of names for the branch.
changed String Numbers of lines added and removed since the previous revision.
date String Date (in UTC if not otherwise configured) of the revision currently being viewed.
image_src_href String URL used to display the current revision of the file as an embedded image. (Set only if the file is not a web-viewable image.)
lines List Set of objects containing information about the most recent modification of a single line of file content in the current resource, naturally ordered by the line numbers they represent. Every line in the resource is represented in the set.
lines.author String Username of the most recent modifier of the line.
lines.date String Date (in UTC if not otherwise configured) of the modification of the line.
lines.diff_href String URL of the ViewVC file difference view which displays the modification of the line.
lines.line_number String Line number (1-based) of the line.
lines.prev_rev String Youngest revision of the resource prior to the line's modification.
lines.rev String Revision in which the modification of the line occured.
lines.text String Textual contents of the line.
log String Log message of the revision currently being viewed.
mime_type String MIME type of the current file.
orig_path String When viewing an old file revision through a copy of the file, this is the old file revision's original path.
orig_href String URL of a ViewVC log view for orig_path.
prev String Previous revision number.
size String Size of the file revision, in bytes. Subversion only.
state String State of the file revision. Possible values: dead, and the empty string.
tags List Names of tags that have been applied to the current file revision.
vendor_branch Boolean Indicates whether or not the current file revision is on a vendor branch.

Revision Graph View (graph.ezt)

Variable Type Description
Includes all variables from the COMMON variable set
imagemap String HTML markup containing the image map associated with the revision graph.
imagesrc String URL of the ViewVC revision graph image for the current resource.

File Difference View (diff.ezt)

Variable Type Description
Includes all variables from the COMMON variable set
changes List Set of objects which contain information about a single line of file difference data. Valid only when diff_format is h or l.
changes.have_left Boolean Specifies whether the left file has a line of content relevant to the difference data line. Valid only when changes.type is change.
changes.have_right Boolean Specifies whether the right file has a line of content relevant to the difference data line. Valid only when changes.type is change.
changes.left String Textual contents of the relevant line in the left file. Valid only when changes.type is change, context, or remove. When changes.type is change, valid only when changes.have_left is set (in order to delineate between missing lines and empty lines, which EZT does not support).
changes.right String Textual contents of the relevant line in the right file. Valid only when changes.type is add, change, or context. When changes.type is change, valid only when changes.have_left is set (in order to delineate between missing lines and empty lines, which EZT does not support).
changes.line_info_extra String Additional line information for the current difference hunk. Valid only when changes.type is header.
changes.line_info_left String First line number represented by the current hunk in the left file. Valid only when changes.type is header.
changes.line_info_right String First line number represented by the current hunk in the right file. Valid only when changes.type is header.
changes.line_number String Line number (1-based) of the line.
changes.type String The type of change. Value values: add, change, context, header, no-changes, remove.
diff_format String Difference dislay format: Valid values are c (context), f (full human-readable), h (human-readable, or colored), l (long human-readable), s (side-by-side), u (unified).
diff_format_action String Form action URL for the diff format selection form.
diff_format_hidden_values List Hidden field name/value pairs for the diff format selection form.
left Container Container object for grouping information about the left file.
left.ago String Text description of the time elapsed since left.date.
left.annotate_href String URL of the ViewVC annotation view for the left file. Valid only when entries.pathtype is file.
left.author String Author of the revision of the left file.
left.date String Date (in UTC if not otherwise configured) in which the left file revision was created.
left.download_href String URL to download the HEAD revision of the left file.
left.download_text_href String URL to download the HEAD revision of the left file as text/plain.
left.log String Log message of the left file revision.
left.path String Path of the left file.
left.prefer_markup Boolean Indicates whether to make the default file link a link to the markup page instead of the checkout page.
left.rev String Revision of the left file.
left.revision_href String URL of the Subversion revision view for the left file's current revision. Valid only when roottype is svn.
left.size String Size of the left file revision, in bytes. Subversion only.
left.tag String Tag of the left file.
left.view_href String This is a URL for the markup view of the left file.
patch_href String URL of the patch view for the file.
raw_diff String Raw difference text. Valid only when diff_format is c, s, or u.
right Container Container object for grouping information about the right file.
right.author String Author of the revision of the right file.
right.annotate_href String URL of the ViewVC annotation view for the right file. Valid only when entries.pathtype is file.
right.author String Author of the revision of the right file.
right.date String Date (in UTC if not otherwise configured) in which the right file revision was created.
right.download_href String URL to download the HEAD revision of the right file.
right.download_text_href String URL to download the HEAD revision of the right file as text/plain.
right.log String Log message of the right file revision.
right.path String Path of the right file.
right.prefer_markup Boolean Indicates whether to make the default file link a link to the markup page instead of the checkout page.
right.rev String Revision of the right file.
right.revision_href String URL of the Subversion revision view for the right file's current revision. Valid only when roottype is svn.
right.size String Size of the right file revision, in bytes. Subversion only.
right.tag String Tag of the right file.
right.view_href String This is a URL for the markup view of the right file.

Directory Listing View (directory.ezt)

Variable Type Description
Includes all variables from the COMMON, PATHREV, PAGING, and PROPERTIES variable sets
attic_showing Boolean Indicates whether or not the directory list include "dead" files (files not available in, perhaps deleted from, the current tag). CVS only.
branch_tags List Set of branch tag names in use by files in the current directory. CVS only.
dir_pagestart String Item number (zero-based) of the first directory entry requested to be shown on the page. Corresponds to the dir_pagestart CGI parameter.
dir_paging_action String Form action URL for the page selection form.
dir_paging_hidden_values List Hidden field name/value pairs for the page selection form.
entries List Set of objects which represent the entries of this directory.
entries.ago String Textual description of the time since entries.date.
entries.annotate_href String URL of the ViewVC annotation view for the directory entry. Valid only when entries.pathtype is file.
entries.author String Username of the last modifier of the directory entry.
entries.date String Date (in UTC if not otherwise configured) of the last modification of the directory entry.
entries.download_href String URL to download the HEAD revision of the directory entry.
entries.download_text_href String URL to download the HEAD revision of the directory entry as text/plain.
entries.errors List List of strings containing error messages encountered by the version control backend as it attempted to harvest information about this directory entry. At this time the strings are somewhat freeform; in the future it would be nice to expose these as testable error code or somesuch.
entries.graph_href String URL of the ViewVC revision graph view for the directory entry.
entries.lockinfo String Information about the lock status of the directory entry.
entries.log String Log message of last modification to the directory entry.
entries.log_file String ViewVC optionally calculates the log message of a CVS directory as the log message associated with the most recently modified file in that directory. When that occurs, this is the name of that file. Valid only when entries.pathtype is dir. See also entries.log_rev.
entries.log_href String URL of the ViewVC revision log view for the directory entry.
entries.log_rev String ViewVC optionally calculates the log message of a CVS directory as the log message associated with the most recently modified file in that directory. When that occurs, this is the revision of that file. Valid only when entries.pathtype is dir. See also entries.log_file.
entries.mime_type String MIME type of the directory entry.
entries.name String Name of the directory entry.
entries.pathtype String Path kind of the directory entry. Valid values: file (file), dir (directory); may be empty.
entries.prefer_markup Boolean Indicates whether to make the default file link a link to the markup page instead of the checkout page. Valid only when entries.pathtype is file.
entries.rev String Revision of the directory entry. For CVS repositories, this is a revision at the tip of the selected tag or branch; for Subversion, this is the youngest revision as of the revision of the directory being viewed.
entries.revision_href String URL of the Subversion revision view for the directory entry's current revision. Valid only when roottype is svn.
entries.short_log String Log message of last modification to the directory entry, truncated to contain no more than the number of characters specified by the short_log_len configuration option.
entries.size String Size (in bytes) of the directory entry. Valid only when roottype is svn and entries.pathtype is file.
entries.state String State of the directory entry. If the state is uninteresting (a typical, versioned object), this field is empty. Valid, non-empty states include: dead (the object is not available on, or possible removed from, this branch; CVS only).
entries.view_href String This is a URL for the markup view if the entry is a file, and a URL for a directory listing if the entry is a directory.
files_shown String Number of files displayed.
hide_attic_href String URL for the current view, but with "dead" files hidden. CVS only.
num_dead String Number of dead files in the current directory.
plain_tags List List of tag names in use by files in the current directory. CVS only.
search_re String Current search expression, if any.
search_re_action String Form action URL for the regular expression search form, if searching is available.
search_re_hidden_values List Hidden field name/value pairs for the regular expression search form.
show_attic_href String URL for the current view, but with "dead" files shown. CVS only.
sortby String Current sorting mode. Valid values: file, rev, date, author, and log.
sortby_author_href String URL for the current view, but sorted by author.
sortby_date_href String URL for the current view, but sorted by date.
sortby_file_href String URL for the current view, but sorted by filename.
sortby_log_href String URL for the current view, but sorted by log message.
sortby_rev_href String URL for the current view, but sorted by revision number.
sortdir String Current sorting mode. Valid values: up (ascending) and down (descending)
tree_rev String Last revision number where the current directory (or any path underneath it) was modified. Subversion only.
tree_rev_href String URL for revision view showing information about the tree_rev revision. Subversion only.
youngest_rev String Last revision number in the repository. Subversion only
youngest_rev_href String URL for revision view showing information about the youngest_rev revision. Subversion only.

Error View (error.ezt)

Variable Type Description
msg String Message describing the current error.
stacktrace String Python stack trace showing where the error occurred.
status String HTTP status code like "404 Not Found" that was sent to the browser with this error message.

Revision Log View (log.ezt)

Variable Type Description
Includes all variables from the COMMON, PATHREV, and PAGING variable sets
branch_tags List Names of branch tags in the file. CVS only.
default_branch List Default branch names (CVS only)
diff_format String Currently selected diff format in the diff selection form. Valid values are c (context), h (human-readable, or colored), l (long human-readable), s (side-by-side), u (unified).
diff_select_action String Form action URL for the diff selection form.
diff_select_hidden_values List Hidden field name/value pairs for the diff selection form.
entries List List of revisions where the file or directory was modified.
entries.ago String Text description of the time elapsed since entries.date.
entries.annotate_href String URL for the annotate view of the file revision.
entries.author String Author of the revision.
entries.branch_names List If this last revision on a branch, a list of names for that branch.
entries.branch_point String If the revision is on a branch, this is the revision number the branch branched off from. CVS only.
entries.branch_points String List of branch tags which branch off of this revision. CVS only.
entries.branch_points.name String Name of the branch tag.
entries.branch_points.href String URL for the current view, but with this tag set as the sticky tag.
entries.branches String List of branch tags that include this file revision. CVS only.
entries.branches.name String Name of the branch tag.
entries.branches.href String URL for the current view, but with this tag set as the sticky tag.
entries.changed String Numbers of lines added and removed since the previous revision. CVS only.
entries.copy_href String URL for log view of entries.copy_path.
entries.copy_path String If the file revision was copied from somewhere, this is the path it was copied from. Subversion only.
entries.copy_rev String If the file revision was copied from somewhere, this is the revision number of the path it was copied from. Subversion only.
entries.date String Date (in UTC if not otherwise configured) of the revision.
entries.diff_to_branch_href String URL for a diff view of this file revision showing the changes since the branch was created at (entries.branch_point). CVS only.
entries.diff_to_main_href String If this revision is at the tip of a branch, URL for a diff view of this file revision showing the differences between it and the latest revision on the parent branch (entries.next_main). CVS only.
entries.diff_to_prev_href String URL for a diff view of this file revision showing the changes since the previous revision (entries.prev).
entries.diff_to_sel_href String URL for a diff view of this file revision and the currently selected revision (rev_selected).
entries.download_href String URL to download the file revision.
entries.download_text_href String URL to download the file revision as text/plain.
entries.lockinfo String Information about the lock status of this revision.
entries.log String Revision log message.
entries.next_main String If this revision is on the tip of the branch, this is the latest revision of the parent branch, a likely merge candidate.
entries.orig_href String URL for log view of entries.orig_path
entries.orig_path String If this file revision is located at a different path than the newest file revision (because it precedes a copy or move), this is the path it was originally located at. Subversion only.
entries.prefer_markup Boolean Indicates whether to make the default file link a link to the markup page instead of the checkout page.
entries.prev String Previous revision number.
entries.rev String Revision number.
entries.revision_href String URL for revision view showing more information about the revision.
entries.sel_for_diff_href String URL for current view, but with this revision selected for diffs.
entries.size String Size of the file revision, in bytes. Subversion only.
entries.state String State of the file revision. Possible values: dead, and the empty string.
entries.tag_names List List of tag names which refer to the revision.
entries.tags List List of tags which refer to the revision
entries.tags.name String Name of the tag.
entries.tags.href String URL for the current page, but with this tag set as the sticky tag.
entries.vendor_branch Boolean Indicates if this revision is on a vendor branch.
entries.view_href String URL for markup view for a file revision, or directory listing view for a directory revision.
head_annotate_href String URL for annotate view of the HEAD revision of the file.
head_download_href String URL to download the HEAD revision of the file.
head_download_text_href String URL to download the HEAD revision of the file as text/plain.
head_prefer_markup Boolean Indicates whether to make the default HEAD file link a link to the markup page instead of the checkout page.
head_view_href String URL for markup view of the HEAD revision of the file.
human_readable Boolean Indicates whether or not currently selected diff format (diff_format) is human readable (colored).
log_pagestart String Item number (zero based) of the first directory entry requested to be shown on the page. Corresponds to the log_pagestart query parameter.
log_paging_action String Form action URL for the page selection form.
log_paging_hidden_values List Hidden field name/value pairs for the page selection form.
logsort String Current sorting mode. Possible values: date and rev.
logsort_action String Form action URL for log sort drop down box.
logsort_hidden_values List Hidden field name/value pairs for the log sort drop down box
mime_type String MIME type of current file.
plain_tags List Names of non-branch in the file. CVS only.
rev_selected String Revision number currently selected for diffs.
tag_annotate_href String URL for annotate view of the file at currently selected sticky tag.
tag_download_href String URL to download the file at currently selected sticky tag. CVS only.
tag_download_text_href String URL to download the file as text/plain at the currently selected sticky tag. CVS only
tag_prefer_markup Boolean Indicates whether to make the default sticky tag file link a link to the markup page instead of the checkout page.
tag_view_href String URL for markup view of the file at the currently selected sticky tag. CVS only.
tags String List of tags that in the current file. CVS only.
tags.rev String Revision number for a non-branch tag, or the latest revision number on the branch for a branch tag.
tags.name String Tag name

Revision History Query Results View (query_results.ezt, rss.ezt)

Variable Type Description
Includes all variables from the COMMON variable set
backout_href String URL for a page that shows a list of commands that can be applied to a working copy to revert all the changes returned by the query.
commits List List of commits to files under the current directory that meet the query criteria.
commits.author String Author of the commit.
commits.files List List of files under the current directory affected by the commit.
commits.files.author String Author of the commit.
commits.files.date String Date (in UTC if not otherwise configured) the change to the file was committed.
commits.files.dir String Path of the directory containing the file.
commits.files.dir_href String URL for directory listing of commits.files.dir.
commits.files.file String File name.
commits.files.rev String Revision number of the file.
commits.files.branch File Branch the commit occurred on.
commits.files.diff_href String URL to diff page showing changes since previous file revision.
commits.files.log_href String URL for file's log page.
commits.files.minus String Number of lines removed from the file by the commit.
commits.files.plus String Number of lines added to the file by the commit.
commits.files.type String Type of change made to the file by the commit. Possible values: Change, Add, Remove
commits.log String Commit log message.
commits.limited_files Boolean True if files list was cut short due to limit_changes.
commits.minus String Total number of lines removed from files in this commit.
commits.num_files String Total number of files in the commits.files list.
commits.plus String Number of lines added to files in this commit.
commits.rev String Commit revision number. Subversion only.
commits.rss_date String Date of the commit formatted for RSS.
commits.rss_url String Absolute URL of the revision page for the commit. Subversion only.
commits.short_log String Truncated commit log message.
english_query String Text description of the current query criteria.
limit_changes String Current limit_changes value, maximum number of changed files to show per commit.
limit_changes_href String URL for the current view but with limit_changes disabled.
minus_count String Total number of lines removed from all files across all returned commits.
plus_count String Total number of lines added to all files across all returned commits.
querysort String Indicates how query results are being sorted. Possible values: date, author, and file.
row_limit_reached Boolean Indicates whether the internal database row limit threshold (set via the cvsdb.row_limit and cvsdb.rss_row_limit configuration options) was reached by the query.
show_branch Boolean Indicates whether or not to list branch names in the results. True when query results can include more than a single branch.
sql String SQL string used to query database. Included for debugging purposes.

Revision History Query Form View (query_form.ezt)

Variable Type Description
Includes all variables from the COMMON variable set
branch String Query string for filtering results by branch.
branch_match String Type of match to perform with branch query string. Valid values: exact, like, glob, regex, notregex.
comment String Query string for filtering results by log message.
comment_match String Type of match to perform with comment query string. Possible values: exact, like, glob, regex, notregex.
date String Option for filtering results by date. Possible values: hours, day, week, month, all, explicit.
dir String Query string for filtering results by subdirectory.
dir_href String URL for directory list of current directory.
file String Query string for filtering results by file name.
file_match String Type of match to perform with file query string. Valid values: exact, like, glob, regex, notregex.
hours String If date is hours, number of hours back to include results from.
limit_changes String Current limit_changes value, maximum number of changed files to show per commit.
maxdate String If date is explicit, latest date to include results from.
mindate String If date is explicit, earliest date to include results from.
query_action String Form action URL for query form.
query_hidden_values List Hidden field name/value pairs for query form.
querysort String Option for sorting query results. Possible values: date, author, and file.
who String Query string for filtering results by author.
who_match String Type of match to perform with who query string. Possible values: exact, like, glob, regex, notregex.

Revision/ChangeSet View (revision.ezt)

Variable Type Description
Includes all variables from the COMMON variable set
ago String Text description of the time elapsed since date.
author String Author of the revision.
changes List List of paths changed in this revision.
changes.action String Indicates what happened to the path in this revision. Valid values are: added, modified, replaced, and deleted.
changes.copy_path String Original path if path was copied from somewhere in this revision.
changes.copy_rev String Revision number of original path if path was copied from somewhere in this revision
changes.diff_href String URL for diff of changed path against previous revision.
changes.is_copy Boolean Indicates whether path was copied from another path in this revision.
changes.log_href String URL for log view of changed path.
changes.path String Changed path.
changes.pathtype String Type of changed path. Valid values: file or dir
changes.prop_mods Boolean Indicates whether the path's properties changed in this revision
changes.text_mods Boolean Indicates whether the path's file contents changed in this revision.
changes.view_href String URL for markup view of changed path.
date String Revision date (in UTC if not otherwise configured).
first_changes String Configured value for limit_changes.
first_changes_href String URL for the current view but with limit_changes set to the configured value.
jump_rev_action String Form action URL for revision jump form.
jump_rev_hidden_values List Hidden field name/value pairs for revision jump form.
limit_changes String Current limit_changes value, maximum number of changed files to show per commit.
log String Revision log message.
more_changes String Number of changes not being shown due to limit_changes.
more_changes_href String URL for the current view but with limit_changes disabled.
num_changes String Number of paths changed in this revision.
next_href String URL for revision page of the next revision.
prev_href String URL for revision page of the previous revision.
rev String Revision number.

Root Listing View (roots.ezt)

Variable Type Description
Includes all variables from the COMMON variable set
roots List Set of configured viewable repositories.
roots.ago String Textual description of the time since roots.date.
roots.author String Username of the last modifier of the root.
root.date String Date (in UTC if not otherwise configured) of the last modification of the root.
roots.href String URL of root directory view for a configured repository.
roots.log String Log message of last modification to the root.
roots.log_href String URL of log revision view for the top-most (root) directory of the root (repository).
roots.name String Name of a configured repository.
roots.path String Server-local location of a configured repository. WARNING: Revealing information to untrusted guests about the details of your server configuration can have negative security implications. Use this token at your own risk.
roots.rev String Youngest revision of the root.
roots.short_log String Log message of last modification to the root, truncated to contain no more than the number of characters specified by the short_log_len configuration option.
roots.type String Version control type of a configured repository. Valid values: cvs, svn.
viewvc-1.1.22/LICENSE.html0000644000175000017500000000536112071623704015261 0ustar cmpilatocmpilato ViewVC: License v1

The following text constitutes the license agreement for the ViewVC software (formerly known as ViewCVS). It is an agreement between The ViewCVS Group and the users of ViewVC.

Copyright © 1999-2013 The ViewCVS Group. All rights reserved.

By using ViewVC, you agree to the terms and conditions set forth below:

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


The following changes have occured to this license over time:

  • May 12, 2001 — copyright years updated
  • September 5, 2002 — copyright years updated
  • March 17, 2006 — software renamed from "ViewCVS"
  • April 10, 2007 — copyright years updated
  • February 22, 2008 — copyright years updated
  • March 18, 2009 — copyright years updated
  • March 29, 2010 — copyright years updated
  • February 18, 2011 — copyright years updated
  • January 23, 2012 — copyright years updated
  • January 04, 2013 — copyright years updated
viewvc-1.1.22/COMMITTERS0000644000175000017500000000176610717264225014733 0ustar cmpilatocmpilatoThe following people have commit access to the ViewVC sources. Note that this is not a full list of ViewVC's authors, however -- for that, you'd need to look over the log messages to see all the patch contributors. If you have a question or comment, it's probably best to mail dev@viewvc.tigris.org, rather than mailing any of these people directly. gstein Greg Stein jpaint Jay Painter akr Tanaka Akira timcera Tim Cera pefu Peter Funk lbruand Lucas Bruand cmpilato C. Michael Pilato rey4 Russell Yanofsky mharig Mark Harig northeye Takuo Kitame jamesh James Henstridge maxb Max Bowsher eh Erik Hülsmann mhagger Michael Haggerty ## Local Variables: ## coding:utf-8 ## End: ## vim:encoding=utf8 viewvc-1.1.22/windows/0000755000175000017500000000000012265242267015004 5ustar cmpilatocmpilatoviewvc-1.1.22/windows/README0000644000175000017500000005414410775462664015705 0ustar cmpilatocmpilato- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DESCRIPTION This file contains special instructions for setting up ViewVC on Windows. It will take you through a basic installation and tell you how to set up optional features like code colorizing and the MySQL commit database. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - REQUIREMENTS ViewVC requires the Python interpreter which you can download from http://python.org/ and the Python for Windows Extensions which are at http://sourceforge.net/projects/pywin32/ For CVS support, ViewVC also requires that the CVSNT client (cvs.exe) OR the RCS tools (rlog.exe, rcsdiff.exe, and co.exe) be installed on your computer. CVSNT is available from http://www.cvsnt.org/wiki and RCS can be obtained from: http://www.cs.purdue.edu/homes/trinkle/RCS/ For Subversion support, you'll need to have the Subversion Python bindings installed. Binaries are available from the Subversion website at: http://subversion.tigris.org/servlets/ProjectDocumentList?folderID=91 Note that if you use binaries, you have to be running the same version of python as the binaries were built for. For example, you cannot use Subversion bindings built for Python 2.3 with Python 2.4. Instructions for building the binaries from source are available here: http://svn.collab.net/repos/svn/trunk/subversion/bindings/swig/INSTALL The Subversion bindings also require you to have diff.exe installed in a directory on your system PATH. diff.exe is available as part of the GnuWin32 project's DiffUtils package at http://gnuwin32.sf.net/. Once you've got Python and CVSNT or RCS or the Subversion bindings installed, you can test out ViewVC before you install it by running: python bin\standalone.py -r The standalone server has a number of features (including a GUI interface) which you can find out about by running python bin\standalone.py --help - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - BASIC INSTALLATION Run the ViewVC install script with python viewvc-install The script will copy the source files into an installation directory that you specify, store some path information, and compile the ViewVC library files into Python bytecode. After the installation is finished you will need to edit the viewvc.conf file in the folder you installed to. The comments in that file tell you exactly what to do. With the config file set up you should be able to double-click standalone.py and access your repository with a web browser. See the sections below for information on setting up optional features and troubleshooting. From here on will stand for the Python root directory (usually something like C:\Python22) and will represent the directory where ViewVC has been installed to (default is C:\Program Files\viewvc-VERSION). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SERVER CONFIGURATION If you want to make ViewVC available to a network (rather than using it on a standalone machine), you will need to configure a web server to host it. This section includes instructions for setting up ViewVC on two commonly used Windows web servers, IIS and Apache. With IIS, you can run ViewVC in CGI mode or ASP mode (or both modes). ASP mode gives better performance (faster page loads), but is harder to set up and may be less stable. CGI mode is stable and easy to set up but slower. On Apache, you can use CGI mode or Mod_Python mode or both modes at once. Mod_Python mode is faster than CGI mode. CGI Mode On IIS 1) Copy viewvc.cgi and query.cgi from \bin\cgi to a folder that is accessible through your web server. 2) Start up the IIS "Internet Services Manager" and right click a virtual server or virtual directory that contains the files you just copied. Choose "Properties" from the context menu that appears. 3) On the properties dialog that appears, navigate to [Home | Virtual] Directory -> Application Settings -> Configuration. This will bring up another dialog box called "Application Configuration". 4) On the "App Mappings" tab choose "Add". Fill in the following information: Executable: \python.exe "%s" Extension: cgi Script Engine: checked Check that file exists: unchecked That is all. Assuming you've set up viewvc.conf to point to your repositories, the CGI pages should run. See the Troubleshooting section below if there are any problems. ASP Mode On IIS In order to run ViewVC with ASP, you will need to enable Python ActiveX scripting and to install the included Aspfool ISAPI filter on whatever virtual server is being used to serve the viewvc pages. Step by step instructions follow below. Aspfool is located in the windows\aspfool folder. To set up ASP mode, follow these steps: 1) Run \Lib\site-packages\win32comext\axscript\client\pyscript.py to register Python as an ASP scripting language. (More documentation on this is at http://www.python.org/windows/win32com/ActiveXScripting.html) 2) Copy the viewvc.asp and query.asp files from \bin\asp to a folder that is accessible through your web server. 3) Start up the IIS "Internet Services Manager" and right click on the virtual server that contains the files you just copied. Choose "Properties" from the context menu that appears. 4) On the properties dialog that appears, click the "ISAPI Filters" tab. Click the "add" button and enter the following information: Filter Name: aspfool Executable: aspfool.dll After you save these changes, the ViewVC ASP pages should begin to work. CGI Mode on Apache Follow the instructions under "Apache Configuration" in the ViewVC INSTALL file. Mod_Python Mode on Apache There are probably ten thousand different ways to set up Apache, mod_python, and ViewVC together. Here are some instructions that work for me using Mod_Python 3.0.3 and Apache 2.0.46. If any Apache gurus want to contribute better instructions, I'd be happy to include them here. 1) Run the win32 mod_python installer from www.modpython.org. 2) Add the following line to the "Global Environment" section of httpd.conf: LoadModule python_module modules/mod_python.so 3) Copy viewvc.py, query.py, handler.py, and .htaccess from \bin\mod_python to a folder being served by apache. Make sure overrides are allowed in this folder. The relevant parent directory in httpd.conf should have "AllowOverride All" set, or at least "AllowOverride FileInfo Options". - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ENSCRIPT HIGHLIGHTING To use enscript, you'll have to install the enscript, libintl, libiconv, and sed packages from the gnuwin32 project (http://gnuwin32.sourceforge.net/). Detailed instructions are on their site, but here is the basic procedure. 1) Extract all packages to a folder on your hard drive, for example c:\gnuwin32 2) Add "c:\gnuwin32\bin" to the system "PATH" environment variable. If ViewVC is running as part of a system service like IIS you will need to reboot the computer so it is able to see the value. See the "Troubleshooting" section below for specific information on when a reboot is neccessary. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - BONSAI-LIKE CHECKIN DATABASE To use the checkin database, you'll need to install MySQL and the MySQL-Python interface. MySQL can be downloaded from www.mysql.com. The MySql-Python adapter is available from http://sf.net/projects/mysql-python/. Make sure to grab the the latest version from the "Files" section. (The "Home Page" link takes you to an outdated page that only links to very old versions.) Both packages come with GUI installers. Once you have MySQL running and set up with a username and password, follow these instructions to set up ViewVC. 1) Open a command prompt and type these commands: cd /d python bin\make-database The script that comes up will prompt you for the MySQL username and password (you should have created these during the MySQL installation), and the name of the database to create. The default database name "ViewVC" should be fine unless for some reason a database with that name already exists. 2) Enter the username, password, and database name into the [cvsdb] section of the \viewvc.conf file. 3) At the command prompt run python bin\cvsdbadmin rebuild where is the path to your CVS repository. 4) If you want the checkin database to be dynamically updated with every checkin, add the following line to your CVSROOT/loginfo file: ALL python "\bin\loginfo-handler" %{sVv} If you decide not to enable dynamic updates, you can periodically refresh the database with "python bin\cvsdbadmin update " - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CVSGRAPH To use CvsGraph with ViewVC, just put cvsgraph.exe in a directory on your system PATH and set the use_cvsgraph option to 1 in your viewvc.conf file. The CvsGraph home page is http://www.akhphd.au.dk/~bertho/cvsgraph/. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DOCROOT OPTIMIZATION By default ViewVC serves up image and stylesheet files in \templates\docroot\ on its own instead of relying on the webserver to deliver them. This simplifies web server configuration, but is inefficient because it means the Python interpreter has to run each time one of these files is downloaded. This causes ViewVC pages to load more slowly, especially when ViewVC is running under CGI on Windows. To make things more efficient, you can make the \templates\docroot directory available on your web server and then set the "docroot" value in viewvc.conf to point to the web address of the directory. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TROUBLESHOOTING - By far the most common cause of errors in ViewVC is failure to successfully execute the programs it depends on (cvs, rlog, rcsdiff, co, enscript, sed, and cvsgraph). To help deal with this problem, ViewVC includes a special debugging mode that displays output from the programs it executes on every web page. This allows you to see error messages and other information that isn't normally visible. To enable the debugging mode, change line 23 of \lib\debug.py from: SHOW_CHILD_PROCESSES = 0 to: SHOW_CHILD_PROCESSES = 1 Important: You may need to restart your web server before this change takes effect. See "Changes made to..." later in this section. - If you see the following error: error: (2, 'CreateProcess', The system cannot find the file specified.') it means that a program ViewVC has tried to execute could not be found by Windows. The fix to this is usually to install the program if it isn't already installed or to update the path to the program in viewvc.conf. Enabling the SHOW_CHILD_PROCESSES mode as described above can provide helpful diagnostic information such as the command line ViewVC is using to invoke the program and the value of the PATH environment variable in the environment ViewVC is running under. - A common cause of server errors under IIS is permissions problems. You need to make sure that the virtual directory containing the CGI or ASP files has script execution enabled. You also need to make sure that the web server user accounts (IUSR_machine_name and IWAM_machine_name, where machine_name is your computer name) have read and execute access to the .asp or .cgi stub scripts, the ViewVC lib/ folder, the paths where external tools like cvs, rcs, enscript, sed, and cvsgraph live, and the CVS repositories. NTFS auditing makes it very easy to track down permissions problems. Also look for IIS messages in the event log. - Certain Apache configurations may hide some environment variables from the ViewVC CGI scripts and the programs they launch. You can see whether an environment variable is visible from the CGI environment by enabling the SHOW_CHILD_PROCESSES debug mode described above. You can force Apache to let variables through with the PassEnv directive (http://httpd.apache.org/docs/mod/mod_env.html#passenv). - Changes made to environment variables, ViewVC source files and the ViewVC configuration file do not always take effect immediately. The table below shows what actions you need to take after changing any of these things before they will have an effect. +----------------+----------------+----------------+-------------------------+ | | Environment | ViewVC | ViewVC | | | Variables | Source | Configuration | +----------------+----------------+----------------+-------------------------+ | Standalone | restart | restart | restart | | Server | standalone.py* | standalone.py | standalone.py | +----------------+----------------+----------------+-------------------------+ | CGI mode under | reboot | nothing | nothing | | apache or IIS | computer | | | +----------------+----------------+----------------+-------------------------+ | mod_python or | reboot | restart Apache | restart Apache | | under apache | computer | | OR | | | | | reload(viewvc) in stub | +----------------+----------------+----------------+-------------------------+ | asp mode under | reboot | restart IIS | restart IIS | | IIS | computer | OR | OR | | | | Unload ASP App | Unload ASP App | | | | | OR | | | | | reload(viewvc) in stub | +----------------+----------------+----------------+-------------------------+ * If standalone.py was launched from a command prompt and you set the environment variable through the control panel, you'll need to open a new command prompt. Notes: Under ASP, changes made to the stub scripts inside the web root do take effect immediately, you only need to take additional action when you make changes to the main source files in \lib To "Unload ASP App", go to the IIS properties dialog for the application directory containing the ViewVC .asp files (in Internet Services Manager). Switch to the [Home] | [Virtual] Directory tab and click the "Unload" button under "Application Settings". To "reload(viewvc) in stub", put these lines in one of the ASP or Mod_Python stub scripts: import viewvc reload (viewvc) then load the page in a web browser. - If you have problems getting ViewVC to work with mod_python, you can first make sure mod_python works standalone with the testing instructions at http://www.modpython.org/live/current/doc-html/inst-testing.html. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - KNOWN ISSUES - If you see ViewVC errors like Error parsing rlog output. Expected RCS file "c:\cvsroot\dir\file,v", found "c:\cvsroot\dir\RCS/file,v" it's because your RCS utilities don't recognize the RCS file suffix and are treating all files specified on the command line like working copies even when they end in ",v". You can fix this by including the following string in your RCSINIT environment variable: -x,v Important: You may need to reboot your computer before the environment variable has an effect. See "Changes made to..." in the TROUBLESHOOTING section. - The GNU RCS utilities won't work with repository files that use CVSNT's unicode expansion mode (-ku). Files that use this mode will show up with an "rlog error: unknown expand mode u" error message in ViewVC directory listings. To work around this, you can set up ViewVC to use the CVSNT executable (cvs.exe) or CVSNT RCS tools (co.exe, rlog.exe, and rcsdiff.exe) instead of the GNU tools. - The standalone server will not run under Cygwin Python because it does not support threads. ASP pages can't be run with Cygwin Python because it does not support ActiveX. To use either of these features you should install a native Python interpreter. - On Windows XP and Windows 2003 Server under IIS, enscript might give an error like: enscript: couldn't open input filter "states -f "K:/gnuwin32/share/enscript/hl/enscript.st" -p "C://.enscript;K:/gnuwin32/share/enscript/hl" -shtml -Dcolor=1 -Dstyle=emacs -Dlanguage=html -Dnum_input_files=1 -Ddocument_title="Enscript Output" -Dtoc=0 -" for file "": No error no output generated The solution is to give read & execute permissions on cmd.exe to the IUSR_computername and IWAM_computername user accounts. (Enscript uses cmd.exe internally to launch its little helper program, states.exe). - By default, ASP will set session cookies at each page load. ViewVC does not use these cookies and they can be safely disabled. You can do this by opening the IIS properties dialog for the application directory containing the ViewVC .asp files. Go to the [Home] | [Virtual] Directory tab and click the "Configuration" button under "Application Settings". On the dialog that comes up, uncheck "Enable Session State" under "App Options" -> "Application Configuration". - Python support for ASP can be a little flaky. If you get strange errors, it can sometimes help to uninstall and reinstall it with pyscript.py. A number of people have also encountered a problem in ActivePython 2.2 where the first loads of any Python ASP page would work, but subsequent loads of the same page would always return nothing (leaving the screen blank). There were a number of workarounds for this problem, but the fix is to download and install the latest python win32 extensions from http://sourceforge.net/projects/pywin32/ - ViewVC can't convert timestamps on diff pages to local time when it is used with CVSNT. This is caused by a CVSNT bug, which is described at http://www.cvsnt.org/mantis/bug_view_page.php?bug_id=0000110 - Old versions of CVSNT (2.0.11 and earlier) have a bug in their rlog emulation which causes them to output truncated paths to RCS files. In ViewVC, this causes errors like Error parsing rlog output. Expected RCS file "c:\cvsroot\dir\file,v", found "file,v" - Old versions of CVSNT (2.0.11 and earlier) have a bug in their standalone RCS tools (rlog.exe, co.exe, and rcsdiff.exe) which causes them not to properly interpret arguments with spaces. This can result in ViewVC errors in repositories that have spaces in file or directory names. This bug only occurs when ViewVC is configured to use the standalone utilities, not when it uses cvs.exe directly as it does by default. - Old versions of CVSNT (1.11.1.3-76 and earlier) don't have any RCS emulation, so they can't be used as RCS parsers for ViewVC. - Very old versions of CVSNT (1.11.1.3-57g and earlier) won't work reliably with our loginfo handler because they have a bug which makes them escape spaces and other special characters in filenames twice. This bug can result in loginfo errors or invalid data being inserted into the database. - Old versions of Highlight (2.4.3 and earlier) will not show line numbers for .txt files or files of unknown type even when the "highlight_line_numbers" option is enabled. - Highlight versions 2.4.2 and 2.4.3 start line numbering for all file types at 0 instead of 1 by default. A workaround is to make ViewVC pass an explicit --line-number-start=1 option to Highlight - Highlight version 2.4.4 starts line numbering for .txt files at 0 instead of 1. It also misinterprets the --line-number-start option for those files, starting numbering one number before whatever number is specified. Since this behavior does not affect unknown file types, a simple workaround is just to not pass a --syntax option to Highlight for plain text files (instead of passing --syntax=txt). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - THANKS - Bryan T. Vold for improving the original ViewCVS patch by adding support for enscript and tarball generation. - David Resnick for tracking down the cause of re_search failures in repositories with non-rcs files and for bringing a bug in sapi.AspFile.header() to my attention - Matt Bunch for finding a better way to address the ASP blank page problem, and Keith D. Zimmerman for finding another workaround. - Rüdiger Koch for reporting a bug in viewvc PATH_INFO parsing code with Apache for Windows as well as Jelle Ouwerkerk and Steffen Yount for providing fixes. - Nick Minutello and Rüdiger Koch for providing workarounds for setting enscript_library environment variable with apache. David Duminy for providing the first bug report on this. - Gyula Faller and Tony Cook for independently coming up with CVSNT loginfo handlers that accept spaces and don't rely on unix-style echo commands. Tony Cook's patch also eliminated the dependency on cat.exe. - Mathieu Mazerolle for making the unix loginfo handler handle spaces in filenames. - Paul Russell for analyzing problems with new fields in CVSNT RCS files. Terry.Ninnis@pgen.com for coming up with a partial solution - Bo Berglund for tracking down the cause of a case-sensitivity issue that could lead to problems in the commit database and for patiently working with me to finally fix the CVSNT RCS fields problem and another problem with enscript. - Ivo Roessling for finding and fixing a bug in the query page's commit grouping code. - Keith D. Zimmerman for experimenting with enscript and finding some new ways to make it work. viewvc-1.1.22/windows/aspfool/0000755000175000017500000000000012265242267016447 5ustar cmpilatocmpilatoviewvc-1.1.22/windows/aspfool/Makefile0000644000175000017500000000033507621055033020101 0ustar cmpilatocmpilatoaspfool.dll : aspfool.o aspfool.def g++ -shared -o aspfool.dll aspfool.o --def aspfool.def -Wl,--add-stdcall-alias distribution.o : aspfool.cpp g++ -O3 -o aspfool.o -c aspfool.cpp clean : rm -f aspfool.o aspfool.dll viewvc-1.1.22/windows/aspfool/aspfool.dsp0000644000175000017500000000770507621055033020624 0ustar cmpilatocmpilato# Microsoft Developer Studio Project File - Name="aspfool" - Package Owner=<4> # Microsoft Developer Studio Generated Build File, Format Version 6.00 # ** DO NOT EDIT ** # TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102 CFG=aspfool - Win32 Debug !MESSAGE This is not a valid makefile. To build this project using NMAKE, !MESSAGE use the Export Makefile command and run !MESSAGE !MESSAGE NMAKE /f "aspfool.mak". !MESSAGE !MESSAGE You can specify a configuration when running NMAKE !MESSAGE by defining the macro CFG on the command line. For example: !MESSAGE !MESSAGE NMAKE /f "aspfool.mak" CFG="aspfool - Win32 Debug" !MESSAGE !MESSAGE Possible choices for configuration are: !MESSAGE !MESSAGE "aspfool - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library") !MESSAGE "aspfool - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library") !MESSAGE # Begin Project # PROP AllowPerConfigDependencies 0 # PROP Scc_ProjName "" # PROP Scc_LocalPath "" CPP=cl.exe MTL=midl.exe RSC=rc.exe !IF "$(CFG)" == "aspfool - Win32 Release" # PROP BASE Use_MFC 0 # PROP BASE Use_Debug_Libraries 0 # PROP BASE Output_Dir "Release" # PROP BASE Intermediate_Dir "Release" # PROP BASE Target_Dir "" # PROP Use_MFC 0 # PROP Use_Debug_Libraries 0 # PROP Output_Dir "Release" # PROP Intermediate_Dir "Release" # PROP Ignore_Export_Lib 0 # PROP Target_Dir "" # ADD BASE CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CUBMOVE_EXPORTS" /YX /FD /c # ADD CPP /nologo /MD /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CUBMOVE_EXPORTS" /YX /FD /c # ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 # ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 # ADD BASE RSC /l 0x409 /d "NDEBUG" # ADD RSC /l 0x409 /d "NDEBUG" BSC32=bscmake.exe # ADD BASE BSC32 /nologo # ADD BSC32 /nologo LINK32=link.exe # ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 # ADD LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 !ELSEIF "$(CFG)" == "aspfool - Win32 Debug" # PROP BASE Use_MFC 0 # PROP BASE Use_Debug_Libraries 1 # PROP BASE Output_Dir "Debug" # PROP BASE Intermediate_Dir "Debug" # PROP BASE Target_Dir "" # PROP Use_MFC 0 # PROP Use_Debug_Libraries 1 # PROP Output_Dir "Debug" # PROP Intermediate_Dir "Debug" # PROP Target_Dir "" # ADD BASE CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CUBMOVE_EXPORTS" /YX /FD /GZ /c # ADD CPP /nologo /MDd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CUBMOVE_EXPORTS" /YX /FD /GZ /c # ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32 # ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 # ADD BASE RSC /l 0x409 /d "_DEBUG" # ADD RSC /l 0x409 /d "_DEBUG" BSC32=bscmake.exe # ADD BASE BSC32 /nologo # ADD BSC32 /nologo LINK32=link.exe # ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept # ADD LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept !ENDIF # Begin Target # Name "aspfool - Win32 Release" # Name "aspfool - Win32 Debug" # Begin Group "Source Files" # PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" # Begin Source File SOURCE=.\aspfool.CPP # End Source File # Begin Source File SOURCE=.\aspfool.def # End Source File # End Group # Begin Group "Header Files" # PROP Default_Filter "h;hpp;hxx;hm;inl" # End Group # Begin Group "Resource Files" # PROP Default_Filter "ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe" # End Group # End Target # End Project viewvc-1.1.22/windows/aspfool/README0000644000175000017500000000143610415243526017325 0ustar cmpilatocmpilatoSome script interpreters for IIS (like ASP and PHP) fail when they recieve requests like /script.asp/extra /script.php/fake/path Aspfool is an ISAPI filter dll that intercepts requests with fake paths and maps them to valid local paths that the script interpreters should be able to handle more readily. The extended path information is still available to the actual scripts through the server variables, which are not altered. Aspfool does not currently distinguish between scripts and other types of files. As a result, requests like /page.html/blah/blah /image.jpg/ha ha ha will return actual files, instead of 404 not found errors. Aspfool is known to compile with Visual C++ 6.0 and GCC 2.95 (mingw). Makefiles and project files are included. Russ Yanofsky rey4@columbia.edu viewvc-1.1.22/windows/aspfool/aspfool.dsw0000644000175000017500000000077607621055033020634 0ustar cmpilatocmpilatoMicrosoft Developer Studio Workspace File, Format Version 6.00 # WARNING: DO NOT EDIT OR DELETE THIS WORKSPACE FILE! ############################################################################### Project: "aspfool"=".\aspfool.dsp" - Package Owner=<4> Package=<5> {{{ }}} Package=<4> {{{ }}} ############################################################################### Global: Package=<5> {{{ }}} Package=<3> {{{ }}} ############################################################################### viewvc-1.1.22/windows/aspfool/aspfool.cpp0000644000175000017500000000267707677144357020646 0ustar cmpilatocmpilato#define WIN32_LEAN_AND_MEAN #include #include #include // Returns 0 if doesn't exist, 1 if it is a file, 2 if it is a directory int inline file_exists(TCHAR const * filename) { WIN32_FIND_DATA fd; HANDLE fh = FindFirstFile(filename, &fd); if (fh == INVALID_HANDLE_VALUE) return 0; else { FindClose(fh); return fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ? 2 : 1; } } BOOL WINAPI GetFilterVersion(HTTP_FILTER_VERSION * pVer) { pVer->dwFilterVersion = HTTP_FILTER_REVISION; pVer->dwFlags = SF_NOTIFY_URL_MAP | SF_NOTIFY_ORDER_DEFAULT; return TRUE; } DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pn) { switch(notificationType) { case SF_NOTIFY_URL_MAP: HTTP_FILTER_URL_MAP & um = *((HTTP_FILTER_URL_MAP *)pn); if (!file_exists(um.pszPhysicalPath)) { size_t pathlen = _tcslen(um.pszPhysicalPath); size_t m = pathlen - _tcslen(um.pszURL); for(size_t i = pathlen - 1; i > m; --i) { TCHAR c = um.pszPhysicalPath[i]; if (c == '\\') { um.pszPhysicalPath[i] = 0; int r = file_exists(um.pszPhysicalPath); if (r == 1) break; else { um.pszPhysicalPath[i] = c; if (r == 2) break; } } } } } return SF_STATUS_REQ_NEXT_NOTIFICATION; }viewvc-1.1.22/windows/aspfool/aspfool.dll0000644000175000017500000002007510352601563020604 0ustar cmpilatocmpilatoMZÿÿ¸@€º´ Í!¸LÍ!This program cannot be run in DOS mode. $PEL¡5ƒ<¸à" 8 ` 0Î@ÈP<.text `.data @À.edataÎ0 À.idataÈ@ @À.reloc<PÀU‰åƒìVS‹] ƒûuèû‹EƒÄüPS‹EPèR‰ÆƒÄ…ÛuèTeè‰ð[^‰ì] vU1À‰å‰ì]ÃvU‰å‹EÇ@‹EÇ€ ¸ëvÉÂU‰åƒì4S‹E =téÖ‰ö‹]‰]üƒÄô‹Eü‹PRèçƒÄ‰À…À…²ƒÄô‹Eü‹PR賃ĉÀ‰EøƒÄô‹Eü‹RèƒÄ‰À‹Uø)‰Uô‹EøH‰Eð‹Eð;Eôwën‰ö‹Eü‹P‹MðŠˆUï€}ï\uM‹Eü‹P‹MðƃÄô‹Eü‹PRè_ƒÄ‰À‰Eèƒ}èuë'ë‹Eü‹P‹MðŠU}èuë vÿMðë‹v¸ëëë‹]ÈÉ U¸‰å‰ì] ‰öU‰å¡ ƒìƒ8t¡ ‹ÿС P‰ ƒxuã‰ì]ÃvU‰åƒìS¡ƒøÿu1Àƒ=tºvƒÂ@ƒ:u÷‰Ã…Ût ‹ÿÐKuôƒÄôhˆè;þÿÿ‹]è‰ì]ÃU‰åƒìSƒ= uMÇ ¡ƒøÿu1Àƒ=tºƒÂ@ƒ:u÷‰Ã…Ût ‹ÿÐKuôƒÄôhˆè×ýÿÿ‹]è‰ì]Ãÿ%l@ÿ%`@ÿ%\@U‰åìdS‹]ƒÄø…ÀþÿÿPSèÓÿÿÿƒÄ‰À‰…¼þÿÿƒ½¼þÿÿÿu1Àë7ë5ƒÄô‹…¼þÿÿPè±ÿÿÿƒÄ ‹…Àþÿÿƒà…Àt ¸ë‰ö¸‰Àëv‹˜þÿÿÉÃÿÿÿÿÿÿÿÿ ¡5ƒ<d0(0@0X0PPxxDŒp00”0£0µ0¼0aspfool.dllGetFilterVersionGetFilterVersion@4HttpFilterProcHttpFilterProc@12atexitfile_exists__FPCc@@¨@\@P@¼@l@t@€@”@t@€@”@”FindClose˜FindFirstFileAˆstrlen@@KERNEL32.dll@msvcrt.dll0Œ1™1¢1«1Ä1Ñ1Ù1ó122"2+282@2W2d2v2~2†2 0.file þÿgdllcrt1.c% _atexitD .textP.data.bss.fileþÿgaspfool.cppP;PUP ix |Œt”Œ .textP(.data.bss.file)þÿgdllmain.cxx§x .textx.data.bss.fileHþÿggccmain.cˆˆ_p.3³Àˆ Ó¼ ___main .textˆì.data.bsstt.textt.data.bss.textt.data.bss.idata$7¸.idata$5l.idata$4P.idata$6”.fileXþÿgfakehnamePfthunkl.text|.data.bss.idata$2.idata$5h.idata$4L.filetþÿgfake.text|.data.bss.idata$4T.idata$5p.idata$7¼ .text|.data.bss.idata$7¤.idata$5`.idata$4D.idata$6€.text„.data.bss.idata$7 .idata$5\.idata$4@.idata$6t.file„þÿgfakehname@fthunk\.textŒ.data.bss.idata$2.idata$5X.idata$4<.file’þÿgfake.textŒ.data.bss.idata$4H.idata$5d.idata$7¨æ`þ|.¼Bÿÿ[ ÿÿuÿÿ‘Ÿÿÿ__dll__ÿÿ·ÿÿÌßÿÿîÿÿend„etext,8GÿÿZÿÿ__end__o}ÿÿ–ÿÿ¤l²ÿÿ_endÊÿÿÛðÿÿ \ÿÿ_strlent 7¨Mgcc2_compiled.___gnu_compiled_c_DllMainCRTStartup@12___gnu_compiled_cplusplus_GetFilterVersion@4_HttpFilterProc@12.text$file_exists__FPCc_file_exists__FPCc_DllMain@12_initialized___do_global_dtors___do_global_ctors__imp__FindFirstFileA@8_FindFirstFileA@8__data_start_____DTOR_LIST____libmsvcrt_a_iname__size_of_stack_commit____size_of_stack_reserve____major_subsystem_version____bss_start____size_of_heap_commit____minor_os_version____head_libmsvcrt_a__image_base____section_alignment____data_end___FindClose@4__CTOR_LIST____bss_end_____CTOR_LIST____file_alignment____major_os_version____DTOR_LIST____size_of_heap_reserve____subsystem____imp__strlen__major_image_version____loader_flags____head_libkernel32_a__minor_subsystem_version____imp__FindClose@4__minor_image_version____libkernel32_a_inameviewvc-1.1.22/windows/aspfool/aspfool.def0000644000175000017500000000020207621055033020555 0ustar cmpilatocmpilatoLIBRARY "aspfool" DESCRIPTION 'aspfool' EXPORTS ; Explicit exports can go here GetFilterVersion @1 HttpFilterProc @2 viewvc-1.1.22/INSTALL0000644000175000017500000005650011642617561014352 0ustar cmpilatocmpilatoCONTENTS -------- TO THE IMPATIENT SECURITY INFORMATION INSTALLING VIEWVC APACHE CONFIGURATION UPGRADING VIEWVC SQL CHECKIN DATABASE ENABLING SYNTAX COLORATION CVSGRAPH CONFIGURATION IF YOU HAVE PROBLEMS... TO THE IMPATIENT ---------------- Congratulations on getting this far. :-) Required Software And Configuration Needed To Run ViewVC: For CVS Support: * Python 1.5.2 or later (sorry, no 3.x support yet) (http://www.python.org/) * RCS, Revision Control System (http://www.cs.purdue.edu/homes/trinkle/RCS/) * GNU-diff to replace diff implementations without the -u option (http://www.gnu.org/software/diffutils/diffutils.html) * read-only, physical access to a CVS repository (See http://www.cvshome.org/ for more information) For Subversion Support: * Python 2.0 or later (sorry, no 3.x support yet) (http://www.python.org/) * Subversion, Version Control System, 1.3.1 or later (binary installation and Python bindings) (http://subversion.apache.org/) Optional: * a web server capable of running CGI programs (for example, Apache at http://httpd.apache.org/) * MySQL 3.22 and MySQLdb 0.9.0 or later to create a commit database (http://www.mysql.com/) (http://sourceforge.net/projects/mysql-python) * Pygments 0.9 or later, syntax highlighting engine (http://pygments.org) * CvsGraph 1.5.0 or later, graphical CVS revision tree generator (http://www.akhphd.au.dk/~bertho/cvsgraph/) Quick sanity check: If you just want to see what your repository looks like when seen through ViewVC, type: $ bin/standalone.py -r /PATH/TO/REPOSITORY This will start a tiny ViewVC server at http://localhost:49152/viewvc/, to which you can connect with your browser. Standard operation: To start installing right away (on UNIX): type "./viewvc-install" in the current directory and answer the prompts. When it finishes, edit the file viewvc.conf in the installation directory to tell ViewVC the paths to your CVS and Subversion repositories. Next, configure your web server (in the way appropriate to that browser) to run /bin/cgi/viewvc.cgi. The section `INSTALLING VIEWVC' below is still recommended reading. SECURITY INFORMATION -------------------- ViewVC provides a feature which allows version controlled content to be served to web browsers just like static web server content. So, if you have a directory full of interrelated HTML files that is housed in your version control repository, ViewVC can serve those files as HTML. You'll see in your web browser what you'd see if the files were part of your website, with working references to stylesheets and images and links to other pages. It is important to realize, however, that as useful as that feature is, there is some risk security-wise in its use. Essentially, anyone with commit access to the CVS or Subversion repositories served by ViewVC has the ability to affect site content. If a discontented or ignorant user commits malicious HTML to a version controlled file (perhaps just by way of documenting examples of such), that malicious HTML is effectively published and live on your ViewVC instance. Visitors viewing those versioned controlled documents get the malicious code, too, which might not be what the original author intended. For this reason, ViewVC's "checkout" view is disabled by default. If you wish to enable it, simply add "co" to the list of views enabled in the allowed_views configuration option. INSTALLING VIEWVC ------------------ NOTE: Windows users can refer to windows/README for Windows-specific installation instructions. 1) To get viewvc.cgi to work, make sure that you have Python installed and a webserver which is capable of executing CGI scripts (either based on the .cgi extension, or by placing the script within a specific directory). Note that to browse CVS repositories, the viewvc.cgi script needs to have READ-ONLY, physical access to the repository (or a copy of it). Therefore, rsh/ssh or pserver access to the repository will not work. And you need to have the RCS utilities installed, specifically "rlog", "rcsdiff", and "co". 2) Installation is handled by the ./viewvc-install script. Run this script and you will be prompted for a installation root path. The default is /usr/local/viewvc-VERSION, where VERSION is the version of this ViewVC release. The installer sets the install path in some of the files, and ViewVC cannot be moved to a different path after the install. Note: while 'root' is usually required to create /usr/local/viewvc, ViewVC does not have to be installed as root, nor does it run as root. It is just as valid to place ViewVC in a home directory, too. Note: viewvc-install will create directories if needed. It will prompt before overwriting files that may have been modified (such as viewvc.conf), thus making it safe to install over the top of a previous installation. It will always overwrite program files, however. 3) Edit /viewvc.conf for your specific configuration. In particular, examine the following configuration options: cvs_roots (for CVS) svn_roots (for Subversion) root_parents (for CVS or Subversion) default_root root_as_url_component rcs_dir mime_types_files There are some other options that are usually nice to change. See viewvc.conf for more information. ViewVC provides a working, default look. However, if you want to customize the look of ViewVC then edit the files in /templates. You need knowledge about HTML to edit the templates. 4) The CGI programs are in /bin/cgi/. You can symlink to this directory from somewhere in your published HTTP server path if your webserver is configured to follow symbolic links. You can also copy the installed /bin/cgi/*.cgi scripts after the install (unlike the other files in ViewVC, the scripts under bin/ can be moved). If you are using Apache, then see below at the section titled APACHE CONFIGURATION. NOTE: for security reasons, it is not advisable to install ViewVC directly into your published HTTP directory tree (due to the MySQL passwords in viewvc.conf). That's it for repository browsing. Instructions for getting the SQL checkin database working are below. APACHE CONFIGURATION -------------------- 1) Locate your Apache configuration file(s). Typical locations are /etc/httpd/httpd.conf, /etc/httpd/conf/httpd.conf, and /etc/apache/httpd.conf. Depending on how Apache was installed, you may also look under /usr/local/etc or /etc/local. Use the vendor documentation or the find utility if in doubt. 2) Depending on how your Apache configuration is setup by default, you might need to explicitly allow high-level access to the ViewVC install location. > Order allow,deny Allow from all For example, if ViewVC is installed in /usr/local/viewvc-1.0 on your system: Order allow,deny Allow from all 3) Configure Apache to expose ViewVC to users at the URL of your choice. ViewVC provides several different ways to do this. Choose one of the following methods: ----------------------------------- METHOD A: CGI mode via ScriptAlias ----------------------------------- The ScriptAlias directive is very useful for pointing directly to the viewvc.cgi script. Simply insert a line containing ScriptAlias /viewvc /bin/cgi/viewvc.cgi into your httpd.conf file. Choose the location in httpd.conf where also the other ScriptAlias lines reside. Some examples: ScriptAlias /viewvc /usr/local/viewvc-1.0/bin/cgi/viewvc.cgi ScriptAlias /query /usr/local/viewvc-1.0/bin/cgi/query.cgi ---------------------------------------- METHOD B: CGI mode in cgi-bin directory ---------------------------------------- Copy the CGI scripts from /bin/cgi/*.cgi to the /cgi-bin/ directory configured in your httpd.conf file. You can override configuration file location using: SetEnv VIEWVC_CONF_PATHNAME /etc/viewvc.conf ------------------------------------------ METHOD C: CGI mode in ExecCGI'd directory ------------------------------------------ Copy the CGI scripts from /bin/cgi/*.cgi to the directory of your choosing in the Document Root adding the following Apache directives for the directory in httpd.conf or an .htaccess file: Options +ExecCGI AddHandler cgi-script .cgi Note: For this to work mod_cgi has to be loaded. And for the .htaccess file to be effective, "AllowOverride All" or "AllowOverride Options FileInfo" needs to have been specified for the directory. ------------------------------------------ METHOD D: Using mod_python (if installed) ------------------------------------------ Copy the Python scripts and .htaccess file from /bin/mod_python/ to a directory being served by Apache. In httpd.conf, make sure that "AllowOverride All" or at least "AllowOverride FileInfo Options" are enabled for the directory you copied the files to. You can override configuration file location using: SetEnv VIEWVC_CONF_PATHNAME /etc/viewvc.conf Note: If you are using Mod_Python under Apache 1.3 the tarball generation feature may not work because it uses multithreading. This works fine under Apache 2. ---------------------------------------- METHOD E: Using mod_wsgi (if installed) ---------------------------------------- Copy the Python scripts file from /bin/mod_python/ to the directory of your choosing. Modify httpd.conf with the following directives: WSGIScriptAlias /viewvc /bin/wsgi/viewvc.wsgi WSGIScriptAlias /query /bin/wsgi/query.wsgi You'll probably also need the following directive because of the not-quite-sanctioned way that ViewVC manipulates Python objects. WSGIApplicationGroup %{GLOBAL} Note: WSGI support in ViewVC is at this time quite rudimentary, bordering on downright experimental. Your mileage may vary. ----------------------------------------- METHOD F: Using mod_fcgid (if installed) ----------------------------------------- This uses ViewVC's WSGI support (from above), but supports using FastCGI, and is a somewhat hybrid approach of several of the above methods. Especially if fcgi is already being used for other purposes, e.g. PHP, also using fcgi can prevent the need for including additional modules (e.g. mod_python or mod_wsgi) within Apache, which may help lessen Apache's memory usage and/or help improve performance. This depends on mod_fcgid: http://httpd.apache.org/mod_fcgid/ as well as the fcgi server from Python's flup package: http://pypi.python.org/pypi/flup http://trac.saddi.com/flup The following are some example httpd.conf fragments you can use to support this configuration: ScriptAlias /viewvc /usr/local/viewvc/bin/wsgi/viewvc.fcgi ScriptAlias /query /usr/local/viewvc/bin/wsgi/query.fcgi 4) [Optional] Add access control. In your httpd.conf you can control access to certain modules by adding directives like this: /"> AllowOverride None AuthUserFile /path/to/passwd/file AuthName "Client Access" AuthType Basic require valid-user WARNING: If you enable the "checkout_magic" or "allow_tar" options, you will need to add additional location directives to prevent people from sneaking in with URLs like: http:///viewvc/*checkout*/ http:///viewvc/~checkout~/ http:///viewvc/.tar.gz?view=tar 5) Restart Apache. The commands to do this vary. "httpd -k restart" and "apache -k restart" are two common variants. On RedHat Linux it is done using the command "/sbin/service httpd restart" and on SuSE Linux it is done with "rcapache restart". Other systems use "apachectl restart". 6) Optional: Protect your ViewVC instance from server-whacking webcrawlers. As ViewVC is a web-based application which each page containing various links to other pages and views, you can expect your server's performance to suffer if a webcrawler finds your ViewVC instance and begins traversing those links. We highly recommend that you add your ViewVC location to a site-wide robots.txt file. Visit the Wikipedia page for Robots.txt (http://en.wikipedia.org/wiki/Robots.txt) for more information. UPGRADING VIEWVC ----------------- Please read the file upgrading-howto.html in the docs/ subdirectory. SQL CHECKIN DATABASE -------------------- This feature is a clone of the Mozilla Project's Bonsai database. It catalogs every commit in the CVS or Subversion repository into a SQL database. In fact, the databases are 100% compatible. Various queries can be performed on the database. After installing ViewVC, there are some additional steps required to get the database working. 1) You need MySQL and MySQLdb (a Python DBAPI 2.0 module) installed. 2) You need to create a MySQL user who has permission to create databases. Optionally, you can create a second user with read-only access to the database. 3) Run the /bin/make-database script. It will prompt you for your MySQL user, password, and the name of database you want to create. The database name defaults to "ViewVC". This script creates the database and sets up the empty tables. If you run this on a existing ViewVC database, you will lose all your data! 4) Edit your /viewvc.conf file. There is a [cvsdb] section. You will need to set: enabled = 1 # Whether to enable query support in viewvc.cgi host = # MySQL database server host port = # MySQL database server port (default is 3306) database_name = # name of database you created with make-database user = # read/write database user passwd = # password for read/write database user readonly_user = # read-only database user readonly_passwd = # password for the read-only user Note that it's pretty safe in this instance for your read-only user and your read-write user to be the same. 5) At this point, you need to tell your version control system(s) to publish their commit information to the database. This is done using utilities that ViewVC provides. To publish CVS commits into the database: Two programs are provided for updating the checkin database from a CVS repository, cvsdbadmin and loginfo-handler. They serve two different purposes. The cvsdbadmin program walks through your CVS repository and adds every commit in every file. This is commonly used for initializing the database from a repository which has been in use. The loginfo-handler script is executed by the CVS server's CVSROOT/loginfo system upon each commit. It makes real-time updates to the checkin database as commits are made to the repository. To build a database of all the commits in the CVS repository /home/cvs, invoke: "./cvsdbadmin rebuild /home/cvs". If you want to update the checkin database, invoke: "./cvsdbadmin update /home/cvs". The update mode checks to see if a commit is already in the database, and only adds it if it is absent. To get real-time updates, you'll want to checkout the CVSROOT module from your CVS repository and edit CVSROOT/loginfo. For folks running CVS 1.12 or better, add this line: ALL /bin/loginfo-handler %p %{sVv} If you are running CVS 1.11 or earlier, you'll want a slightly different command line in CVSROOT/loginfo: ALL /bin/loginfo-handler %{sVv} If you have other scripts invoked by CVSROOT/loginfo, you will want to make sure to change any running under the "DEFAULT" keyword to "ALL" like the loginfo handler, and probably carefully read the execution rules for CVSROOT/loginfo from the CVS manual. If you are running the Unix port of CVS-NT, the handler script need to know about it. CVS-NT delivers commit information to loginfo scripts differently than the way mainstream CVS does. Your command line should look like this: ALL /bin/loginfo-handler %{sVv} cvsnt To publish Subversion commits into the database: To build a database of all the commits in the Subversion repository /home/svn, invoke: "./svndbadmin rebuild /home/svn". If you want to update the checkin database, invoke: "./svndbadmin update /home/svn". To get real time updates, you will need to add a post-commit hook (for the repository example above, the script should go in /home/svn/hooks/post-commit). The script should look something like this: #!/bin/sh REPOS="$1" REV="$2" /bin/svndbadmin update \ "$REPOS" "$REV" If you allow revision property changes in your repository, create a post-revprop-change hook script which uses the same 'svndbadmin update' command as the post-commit script, except with the addition of the --force option: #!/bin/sh REPOS="$1" REV="$2" /bin/svndbadmin update --force \ "$REPOS" "$REV" This will make sure that the checkin database stays consistent when you change the svn:log, svn:author or svn:date revision properties. You should be ready to go. Click one of the "Query revision history" links in ViewVC directory listings and give it a try. ENABLING SYNTAX COLORATION -------------------------- ViewVC uses Pygments (http://pygments.org) for syntax coloration. You need only install a suitable version of that module, and if ViewVC finds it in your Python module path, it will use it (unless you specifically disable the feature by setting use_pygments = 0 in your viewvc.conf file). CVSGRAPH CONFIGURATION ---------------------- CvsGraph is a program that can display a clickable, graphical tree of files in a CVS repository. WARNING: Under certain circumstances (many revisions of a file or many branches or both) CvsGraph can generate very huge images. Especially on thin clients these images may crash the Web-Browser. Currently there is no known way to avoid this behavior of CvsGraph. So you have been warned! Nevertheless, CvsGraph can be quite helpful on repositories with a reasonable number of revisions and branches. 1) Install CvsGraph using your system's package manager or downloading from the project home page. 2) Set the 'use_cvsgraph' options in viewvc.conf to 1. 3) You may also need to set the 'cvsgraph_path' option if the CvsGraph executable is not located on the system PATH. 4) There is a file /cvsgraph.conf that you may want to edit if desired to set color and font characteristics. See the cvsgraph.conf documentation. No edits are required in cvsgraph.conf for operation with ViewVC. SUBVERSION INTEGRATION ---------------------- Unlike the CVS integration, which simply wraps the RCS and CVS utility programs, the Subversion integration requires additional Python libraries. To use ViewVC with Subversion, make sure you have both Subversion itself and the Subversion Python bindings installed. These can be obtained through typical package distribution mechanisms, or may be build from source. (See the files 'INSTALL' and 'subversion/bindings/swig/INSTALL' in the Subversion source tree for more details on how to build and install Subversion and its Python bindings.) Generally speaking, you'll know when your installation of Subversion's bindings has been successful if you can import the 'svn.core' module from within your Python interpreter. Here's an example of doing so which doubles as a quick way to check what version of the Subversion Python binding you have: % python Python 2.2.2 (#1, Oct 29 2002, 02:47:30) [GCC 2.96 20000731 (Red Hat Linux 7.2 2.96-108.7.2)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> from svn.core import * >>> "%s.%s.%s" % (SVN_VER_MAJOR, SVN_VER_MINOR, SVN_VER_PATCH) '1.3.1' >>> Note that by default, Subversion installs its bindings in a location that is not in Python's default module search path (for example, on Linux systems the default is usually /usr/local/lib/svn-python). You need to remedy this, either by adding this path to Python's module search path, or by relocating the bindings to some place in that search path. For example, you might want to create .pth file in your Python installation directory's site-packages area which tells Python where to find additional modules (in this case, you Subversion Python bindings). You would do this as follows (and as root): $ echo "/path/to/svn/bindings" > /path/to/python/site-packages/svn.pth (Though, obviously, with the correct paths specified.) Configuration of the Subversion repositories happens in much the same way as with CVS repositories, only with the 'svn_roots' configuration variable instead of the 'cvs_roots' one. IF YOU HAVE PROBLEMS ... ------------------------ If nothing seems to work: * Check if you can execute CGI-scripts (Apache needs to have an ScriptAlias /cgi-bin or cgi-script Handler defined). Try to execute a simple CGI-script that often comes with the distribution of the webserver; locate the logfiles and try to find hints which explain the malfunction * View the entries in the webserver's error.log If ViewVC seems to work but doesn't show the expected result (Typical error: you can't see any files) * Check whether the CGI-script has read-permissions to your CVS-Repository. The CGI-script generally runs as the same user that the web server does, often user 'nobody' or 'httpd'. * Does ViewVC find your RCS utilities? (edit rcs_dir) If something else happens or you can't get it to work: * Check the ViewVC home page: http://viewvc.org/ * Review the ViewVC mailing list archive to see if somebody else had the same problem, and it was solved: http://viewvc.tigris.org/servlets/SummarizeList?listName=users * Check the ViewVC issue database to see if the problem you are seeing is the result of a known bug: http://viewvc.tigris.org/issues/query.cgi * Send mail to the ViewVC mailing list, users@viewvc.tigris.org. NOTE: make sure you provide an accurate description of the problem -- including the version of ViewVC you are using -- and any relevant tracebacks or error logs. viewvc-1.1.22/bin/0000755000175000017500000000000012265242271014055 5ustar cmpilatocmpilatoviewvc-1.1.22/bin/cvsdbadmin0000755000175000017500000001321012071623704016111 0ustar cmpilatocmpilato#!/usr/bin/env python # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # administrative program for CVSdb; this is primarily # used to add/rebuild CVS repositories to the database # # ----------------------------------------------------------------------- # ######################################################################### # # INSTALL-TIME CONFIGURATION # # These values will be set during the installation process. During # development, they will remain None. # LIBRARY_DIR = None CONF_PATHNAME = None # Adjust sys.path to include our library directory import sys import os if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) else: sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib"))) ######################################################################### import os import string import cvsdb import viewvc import vclib.ccvs def UpdateFile(db, repository, path, update, quiet_level): try: if update: commit_list = cvsdb.GetUnrecordedCommitList(repository, path, db) else: commit_list = cvsdb.GetCommitListFromRCSFile(repository, path) except cvsdb.error, e: print '[ERROR] %s' % (e) return file = string.join(path, "/") printing = 0 if update: if quiet_level < 1 or (quiet_level < 2 and len(commit_list)): printing = 1 print '[%s [%d new commits]]' % (file, len(commit_list)), else: if quiet_level < 2: printing = 1 print '[%s [%d commits]]' % (file, len(commit_list)), ## add the commits into the database for commit in commit_list: db.AddCommit(commit) if printing: sys.stdout.write('.') sys.stdout.flush() if printing: print def RecurseUpdate(db, repository, directory, update, quiet_level): for entry in repository.listdir(directory, None, {}): path = directory + [entry.name] if entry.errors: continue if entry.kind is vclib.DIR: RecurseUpdate(db, repository, path, update, quiet_level) continue if entry.kind is vclib.FILE: UpdateFile(db, repository, path, update, quiet_level) def RootPath(path, quiet_level): """Break os path into cvs root path and other parts""" root = os.path.abspath(path) path_parts = [] p = root while 1: if os.path.exists(os.path.join(p, 'CVSROOT')): root = p if quiet_level < 2: print "Using repository root `%s'" % root break p, pdir = os.path.split(p) if not pdir: del path_parts[:] if quiet_level < 1: print "Using repository root `%s'" % root print "Warning: CVSROOT directory not found." break path_parts.append(pdir) root = cvsdb.CleanRepository(root) path_parts.reverse() return root, path_parts def usage(): cmd = os.path.basename(sys.argv[0]) sys.stderr.write( """Administer the ViewVC checkins database data for the CVS repository located at REPOS-PATH. Usage: 1. %s [[-q] -q] rebuild REPOS-PATH 2. %s [[-q] -q] update REPOS-PATH 3. %s [[-q] -q] purge REPOS-PATH 1. Rebuild the commit database information for the repository located at REPOS-PATH, after first purging information specific to that repository (if any). 2. Update the commit database information for all unrecorded commits in the repository located at REPOS-PATH. 3. Purge information specific to the repository located at REPOS-PATH from the database. Use the -q flag to cause this script to be less verbose; use it twice to invoke a peaceful state of noiselessness. """ % (cmd, cmd, cmd)) sys.exit(1) ## main if __name__ == '__main__': args = sys.argv # check the quietness level (0 = verbose, 1 = new commits, 2 = silent) quiet_level = 0 while 1: try: index = args.index('-q') quiet_level = quiet_level + 1 del args[index] except ValueError: break # validate the command if len(args) <= 2: usage() command = args[1].lower() if command not in ('rebuild', 'update', 'purge'): sys.stderr.write('ERROR: unknown command %s\n' % command) usage() # get repository and path, and do the work root, path_parts = RootPath(args[2], quiet_level) rootpath = vclib.ccvs.canonicalize_rootpath(root) try: cfg = viewvc.load_config(CONF_PATHNAME) db = cvsdb.ConnectDatabase(cfg) if command in ('rebuild', 'purge'): if quiet_level < 2: print "Purging existing data for repository root `%s'" % root try: db.PurgeRepository(root) except cvsdb.UnknownRepositoryError, e: if command == 'purge': sys.stderr.write("ERROR: " + str(e) + "\n") sys.exit(1) if command in ('rebuild', 'update'): repository = vclib.ccvs.CVSRepository(None, rootpath, None, cfg.utilities, 0) RecurseUpdate(db, repository, path_parts, command == 'update', quiet_level) except KeyboardInterrupt: print print '** break **' sys.exit(0) viewvc-1.1.22/bin/svndbadmin0000755000175000017500000003104412071623704016131 0ustar cmpilatocmpilato#!/usr/bin/env python # -*-python-*- # # Copyright (C) 2004-2013 The ViewCVS Group. All Rights Reserved. # Copyright (C) 2004-2007 James Henstridge # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # administrative program for loading Subversion revision information # into the checkin database. It can be used to add a single revision # to the database, or rebuild/update all revisions. # # To add all the checkins from a Subversion repository to the checkin # database, run the following: # /path/to/svndbadmin rebuild /path/to/repo # # This script can also be called from the Subversion post-commit hook, # something like this: # REPOS="$1" # REV="$2" # /path/to/svndbadmin update "$REPOS" "$REV" # # If you allow changes to revision properties in your repository, you # might also want to set up something similar in the # post-revprop-change hook using "update" with the --force option to # keep the checkin database consistent with the repository. # # ----------------------------------------------------------------------- # ######################################################################### # # INSTALL-TIME CONFIGURATION # # These values will be set during the installation process. During # development, they will remain None. # LIBRARY_DIR = None CONF_PATHNAME = None # Adjust sys.path to include our library directory import sys import os if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) else: sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib"))) ######################################################################### import os import string import re import svn.core import svn.repos import svn.fs import svn.delta import cvsdb import viewvc import vclib class SvnRepo: """Class used to manage a connection to a SVN repository.""" def __init__(self, path): self.path = path self.repo = svn.repos.svn_repos_open(path) self.fs = svn.repos.svn_repos_fs(self.repo) self.rev_max = svn.fs.youngest_rev(self.fs) def __getitem__(self, rev): if rev is None: rev = self.rev_max elif rev < 0: rev = rev + self.rev_max + 1 assert 0 <= rev <= self.rev_max rev = SvnRev(self, rev) return rev _re_diff_change_command = re.compile('(\d+)(?:,(\d+))?([acd])(\d+)(?:,(\d+))?') def _get_diff_counts(diff_fp): """Calculate the plus/minus counts by parsing the output of a normal diff. The reasons for choosing Normal diff format are: - the output is short, so should be quicker to parse. - only the change commands need be parsed to calculate the counts. - All file data is prefixed, so won't be mistaken for a change command. This code is based on the description of the format found in the GNU diff manual.""" plus, minus = 0, 0 line = diff_fp.readline() while line: match = re.match(_re_diff_change_command, line) if match: # size of first range if match.group(2): count1 = int(match.group(2)) - int(match.group(1)) + 1 else: count1 = 1 cmd = match.group(3) # size of second range if match.group(5): count2 = int(match.group(5)) - int(match.group(4)) + 1 else: count2 = 1 if cmd == 'a': # LaR - insert after line L of file1 range R of file2 plus = plus + count2 elif cmd == 'c': # FcT - replace range F of file1 with range T of file2 minus = minus + count1 plus = plus + count2 elif cmd == 'd': # RdL - remove range R of file1, which would have been # at line L of file2 minus = minus + count1 line = diff_fp.readline() return plus, minus class SvnRev: """Class used to hold information about a particular revision of the repository.""" def __init__(self, repo, rev): self.repo = repo self.rev = rev self.rev_roots = {} # cache of revision roots # revision properties ... revprops = svn.fs.revision_proplist(repo.fs, rev) self.author = str(revprops.get(svn.core.SVN_PROP_REVISION_AUTHOR,'')) self.date = str(revprops.get(svn.core.SVN_PROP_REVISION_DATE, '')) self.log = str(revprops.get(svn.core.SVN_PROP_REVISION_LOG, '')) # convert the date string to seconds since epoch ... try: self.date = svn.core.svn_time_from_cstring(self.date) / 1000000 except: self.date = None # get a root for the current revisions fsroot = self._get_root_for_rev(rev) # find changes in the revision editor = svn.repos.ChangeCollector(repo.fs, fsroot) e_ptr, e_baton = svn.delta.make_editor(editor) svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton) self.changes = [] for path, change in editor.changes.items(): # skip non-file changes if change.item_kind != svn.core.svn_node_file: continue # deal with the change types we handle action = None base_root = None base_path = change.base_path if change.base_path: base_root = self._get_root_for_rev(change.base_rev) # figure out what kind of change this is, and get a diff # object for it. note that prior to 1.4 Subversion's # bindings didn't give us change.action, but that's okay # because back then deleted paths always had a change.path # of None. if hasattr(change, 'action') \ and change.action == svn.repos.CHANGE_ACTION_DELETE: action = 'remove' elif not change.path: action = 'remove' elif change.added: action = 'add' else: action = 'change' if action == 'remove': diffobj = svn.fs.FileDiff(base_root, base_path, None, None) else: diffobj = svn.fs.FileDiff(base_root, base_path, fsroot, change.path) diff_fp = diffobj.get_pipe() plus, minus = _get_diff_counts(diff_fp) self.changes.append((path, action, plus, minus)) def _get_root_for_rev(self, rev): """Fetch a revision root from a cache of such, or a fresh root (which is then cached for later use.""" if not self.rev_roots.has_key(rev): self.rev_roots[rev] = svn.fs.revision_root(self.repo.fs, rev) return self.rev_roots[rev] def handle_revision(db, command, repo, rev, verbose, force=0): """Adds a particular revision of the repository to the checkin database.""" revision = repo[rev] committed = 0 if verbose: print "Building commit info for revision %d..." % (rev), if not revision.changes: if verbose: print "skipped (no changes)." return for (path, action, plus, minus) in revision.changes: directory, file = os.path.split(path) commit = cvsdb.CreateCommit() commit.SetRepository(repo.path) commit.SetDirectory(directory) commit.SetFile(file) commit.SetRevision(str(rev)) commit.SetAuthor(revision.author) commit.SetDescription(revision.log) commit.SetTime(revision.date) commit.SetPlusCount(plus) commit.SetMinusCount(minus) commit.SetBranch(None) if action == 'add': commit.SetTypeAdd() elif action == 'remove': commit.SetTypeRemove() elif action == 'change': commit.SetTypeChange() if command == 'update': result = db.CheckCommit(commit) if result and not force: continue # already recorded # commit to database db.AddCommit(commit) committed = 1 if verbose: if committed: print "done." else: print "skipped (already recorded)." def main(command, repository, revs=[], verbose=0, force=0): cfg = viewvc.load_config(CONF_PATHNAME) db = cvsdb.ConnectDatabase(cfg) # Purge what must be purged. if command in ('rebuild', 'purge'): if verbose: print "Purging commit info for repository root `%s'" % repository try: db.PurgeRepository(repository) except cvsdb.UnknownRepositoryError, e: if command == 'purge': sys.stderr.write("ERROR: " + str(e) + "\n") sys.exit(1) # Record what must be recorded. if command in ('rebuild', 'update'): if not os.path.exists(repository): sys.stderr.write('ERROR: could not find repository %s\n' % (repository)) sys.exit(1) repo = SvnRepo(repository) if command == 'rebuild' or (command == 'update' and not revs): for rev in range(repo.rev_max+1): handle_revision(db, command, repo, rev, verbose) elif command == 'update': if revs[0] is None: revs[0] = repo.rev_max if revs[1] is None: revs[1] = repo.rev_max revs.sort() for rev in range(revs[0], revs[1]+1): handle_revision(db, command, repo, rev, verbose, force) def _rev2int(r): if r == 'HEAD': r = None else: r = int(r) if r < 0: raise ValueError, "invalid revision '%d'" % (r) return r def usage(): cmd = os.path.basename(sys.argv[0]) sys.stderr.write( """Administer the ViewVC checkins database data for the Subversion repository located at REPOS-PATH. Usage: 1. %s [-v] rebuild REPOS-PATH 2. %s [-v] update REPOS-PATH [REV[:REV2]] [--force] 3. %s [-v] purge REPOS-PATH 1. Rebuild the commit database information for the repository located at REPOS-PATH across all revisions, after first purging information specific to that repository (if any). 2. Update the commit database information for the repository located at REPOS-PATH across all revisions or, optionally, only for the specified revision REV (or revision range REV:REV2). This is just like rebuilding, except that, unless --force is specified, no commit information will be stored for commits already present in the database. If a range is specified, the revisions will be processed in ascending order, and you may specify "HEAD" to indicate "the youngest revision currently in the repository". 3. Purge information specific to the repository located at REPOS-PATH from the database. Use the -v flag to cause this script to give progress information as it works. """ % (cmd, cmd, cmd)) sys.exit(1) if __name__ == '__main__': verbose = 0 force = 0 args = sys.argv try: index = args.index('-v') verbose = 1 del args[index] except ValueError: pass try: index = args.index('--force') force = 1 del args[index] except ValueError: pass if len(args) < 3: usage() command = string.lower(args[1]) if command not in ('rebuild', 'update', 'purge'): sys.stderr.write('ERROR: unknown command %s\n' % command) usage() revs = [] if len(sys.argv) > 3: if command == 'rebuild': sys.stderr.write('ERROR: rebuild no longer accepts a revision ' 'number argument. Usage update --force.') usage() elif command != 'update': usage() try: revs = map(lambda x: _rev2int(x), sys.argv[3].split(':')) if len(revs) > 2: raise ValueError, "too many revisions in range" if len(revs) == 1: revs.append(revs[0]) except ValueError: sys.stderr.write('ERROR: invalid revision specification "%s"\n' \ % sys.argv[3]) usage() else: rev = None try: repository = vclib.svn.canonicalize_rootpath(args[2]) repository = cvsdb.CleanRepository(os.path.abspath(repository)) main(command, repository, revs, verbose, force) except KeyboardInterrupt: print print '** break **' sys.exit(0) viewvc-1.1.22/bin/asp/0000755000175000017500000000000012265242271014640 5ustar cmpilatocmpilatoviewvc-1.1.22/bin/asp/viewvc.asp0000644000175000017500000000323312071623704016650 0ustar cmpilatocmpilato<%@ LANGUAGE = Python %> <% # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # viewvc: View CVS/SVN repositories via a web browser # # ----------------------------------------------------------------------- # # This is a teeny stub to launch the main ViewVC app. It checks the load # average, then loads the (precompiled) viewvc.py file and runs it. # # ----------------------------------------------------------------------- # ######################################################################### # # INSTALL-TIME CONFIGURATION # # These values will be set during the installation process. During # development, they will remain None. # LIBRARY_DIR = None CONF_PATHNAME = None ######################################################################### # # Adjust sys.path to include our library directory # import sys if LIBRARY_DIR: if not LIBRARY_DIR in sys.path: sys.path.insert(0, LIBRARY_DIR) ######################################################################### ### add code for checking the load average ######################################################################### # go do the work import sapi import viewvc server = sapi.AspServer(Server, Request, Response, Application) try: cfg = viewvc.load_config(CONF_PATHNAME, server) viewvc.main(server, cfg) finally: s.close() %> viewvc-1.1.22/bin/asp/query.asp0000644000175000017500000000324112071623704016511 0ustar cmpilatocmpilato<%@ LANGUAGE = Python %> <% # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # query.asp: View CVS/SVN commit database by web browser # # ----------------------------------------------------------------------- # # This is a teeny stub to launch the main ViewVC app. It checks the load # average, then loads the (precompiled) query.py file and runs it. # # ----------------------------------------------------------------------- # ######################################################################### # # INSTALL-TIME CONFIGURATION # # These values will be set during the installation process. During # development, they will remain None. # LIBRARY_DIR = None CONF_PATHNAME = None ######################################################################### # # Adjust sys.path to include our library directory # import sys if LIBRARY_DIR: if not LIBRARY_DIR in sys.path: sys.path.insert(0, LIBRARY_DIR) ######################################################################### import sapi import viewvc import query server = sapi.AspServer(Server, Request, Response, Application) try: cfg = viewvc.load_config(CONF_PATHNAME, server) viewvc_base_url = cfg.query.viewvc_base_url if viewvc_base_url is None: viewvc_base_url = "viewvc.asp" query.main(server, cfg, viewvc_base_url) finally: s.close() %> viewvc-1.1.22/bin/loginfo-handler0000755000175000017500000002373512071623704017064 0ustar cmpilatocmpilato#!/usr/bin/env python # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # updates SQL database with new commit records # # ----------------------------------------------------------------------- # ######################################################################### # # INSTALL-TIME CONFIGURATION # # These values will be set during the installation process. During # development, they will remain None. # LIBRARY_DIR = None CONF_PATHNAME = None # Adjust sys.path to include our library directory import sys import os if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) else: sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib"))) ######################################################################### import os import string import getopt import re import cvsdb import viewvc import vclib.ccvs DEBUG_FLAG = 0 ## output functions def debug(text): if DEBUG_FLAG: if type(text) != (type([])): text = [text] for line in text: line = line.rstrip('\n\r') print 'DEBUG(viewvc-loginfo):', line def warning(text): print 'WARNING(viewvc-loginfo):', text def error(text): print 'ERROR(viewvc-loginfo):', text sys.exit(1) _re_revisions = re.compile( r",(?P(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and first revision r",(?P(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and second revision r"(?:$| )" # space or end of string ) def Cvs1Dot12ArgParse(args): """CVS 1.12 introduced a new loginfo format while provides the various pieces of interesting version information to the handler script as individual arguments instead of as a single string.""" if args[1] == '- New directory': return None, None elif args[1] == '- Imported sources': return None, None else: directory = args.pop(0) files = [] while len(args) >= 3: files.append(args[0:3]) args = args[3:] return directory, files def HeuristicArgParse(s, repository): """Older versions of CVS (except for CVSNT) do not escape spaces in file and directory names that are passed to the loginfo handler. Since the input to loginfo is a space separated string, this can lead to ambiguities. This function attempts to guess intelligently which spaces are separators and which are part of file or directory names. It disambiguates spaces in filenames from the separator spaces between files by assuming that every space which is preceded by two well-formed revision numbers is in fact a separator. It disambiguates the first separator space from spaces in the directory name by choosing the longest possible directory name that actually exists in the repository""" if (s[-16:] == ' - New directory' or s[:26] == ' - New directory,NONE,NONE'): return None, None if (s[-19:] == ' - Imported sources' or s[-29:] == ' - Imported sources,NONE,NONE'): return None, None file_data_list = [] start = 0 while 1: m = _re_revisions.search(s, start) if start == 0: if m is None: error('Argument "%s" does not contain any revision numbers' \ % s) directory, filename = FindLongestDirectory(s[:m.start()], repository) if directory is None: error('Argument "%s" does not start with a valid directory' \ % s) debug('Directory name is "%s"' % directory) else: if m is None: warning('Failed to interpret past position %i in the loginfo ' 'argument, leftover string is "%s"' \ % start, pos[start:]) filename = s[start:m.start()] old_version, new_version = m.group('old', 'new') file_data_list.append((filename, old_version, new_version)) debug('File "%s", old revision %s, new revision %s' % (filename, old_version, new_version)) start = m.end() if start == len(s): break return directory, file_data_list def FindLongestDirectory(s, repository): """Splits the first part of the argument string into a directory name and a file name, either of which may contain spaces. Returns the longest possible directory name that actually exists""" parts = string.split(s, " ") for i in range(len(parts)-1, 0, -1): directory = string.join(parts[:i]) filename = string.join(parts[i:]) if os.path.isdir(os.path.join(repository, directory)): return directory, filename return None, None _re_cvsnt_revisions = re.compile( r"(?P.*)" # comma and first revision r",(?P(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and first revision r",(?P(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and second revision r"$" # end of string ) def CvsNtArgParse(s, repository): """CVSNT escapes all spaces in filenames and directory names with backslashes""" if s[-18:] == r' -\ New\ directory': return None, None if s[-21:] == r' -\ Imported\ sources': return None, None file_data_list = [] directory, pos = NextFile(s) debug('Directory name is "%s"' % directory) while 1: fileinfo, pos = NextFile(s, pos) if fileinfo is None: break m = _re_cvsnt_revisions.match(fileinfo) if m is None: warning('Can\'t parse file information in "%s"' % fileinfo) continue file_data = m.group('filename', 'old', 'new') file_data_list.append(file_data) debug('File "%s", old revision %s, new revision %s' % file_data) return directory, file_data_list def NextFile(s, pos = 0): escaped = 0 ret = '' i = pos while i < len(s): c = s[i] if escaped: ret += c escaped = 0 elif c == '\\': escaped = 1 elif c == ' ': return ret, i + 1 else: ret += c i += 1 return ret or None, i def ProcessLoginfo(rootpath, directory, files): cfg = viewvc.load_config(CONF_PATHNAME) db = cvsdb.ConnectDatabase(cfg) repository = vclib.ccvs.CVSRepository(None, rootpath, None, cfg.utilities, 0) # split up the directory components dirpath = filter(None, string.split(os.path.normpath(directory), os.sep)) ## build a list of Commit objects commit_list = [] for filename, old_version, new_version in files: filepath = dirpath + [filename] ## XXX: this is nasty: in the case of a removed file, we are not ## given enough information to find it in the rlog output! ## So instead, we rlog everything in the removed file, and ## add any commits not already in the database if new_version == 'NONE': commits = cvsdb.GetUnrecordedCommitList(repository, filepath, db) else: commits = cvsdb.GetCommitListFromRCSFile(repository, filepath, new_version) commit_list.extend(commits) ## add to the database db.AddCommitList(commit_list) ## MAIN if __name__ == '__main__': ## get the repository from the environment try: repository = os.environ['CVSROOT'] except KeyError: error('CVSROOT not in environment') debug('Repository name is "%s"' % repository) ## parse arguments argc = len(sys.argv) debug('Got %d arguments:' % (argc)) debug(map(lambda x: ' ' + x, sys.argv)) # if we have more than 3 arguments, we are likely using the # newer loginfo format introduced in CVS 1.12: # # ALL /bin/loginfo-handler %p %{sVv} if argc > 3: directory, files = Cvs1Dot12ArgParse(sys.argv[1:]) else: if len(sys.argv) > 1: # the first argument should contain file version information arg = sys.argv[1] else: # if there are no arguments, read version information from # first line of input like old versions of ViewCVS did arg = string.rstrip(sys.stdin.readline()) if len(sys.argv) > 2: # if there is a second argument it indicates which parser # should be used to interpret the version information if sys.argv[2] == 'cvs': fun = HeuristicArgParse elif sys.argv[2] == 'cvsnt': fun = CvsNtArgParse else: error('Bad arguments') else: # if there is no second argument, guess which parser to use based # on the operating system. Since CVSNT now runs on Windows and # Linux, the guess isn't necessarily correct if sys.platform == "win32": fun = CvsNtArgParse else: fun = HeuristicArgParse directory, files = fun(arg, repository) debug('Discarded from stdin:') debug(map(lambda x: ' ' + x, sys.stdin.readlines())) # consume stdin repository = cvsdb.CleanRepository(repository) debug('Repository: %s' % (repository)) debug('Directory: %s' % (directory)) debug('Files: %s' % (str(files))) if files is None: debug('Not a checkin, nothing to do') else: ProcessLoginfo(repository, directory, files) sys.exit(0) viewvc-1.1.22/bin/wsgi/0000755000175000017500000000000012265242271015026 5ustar cmpilatocmpilatoviewvc-1.1.22/bin/wsgi/query.fcgi0000644000175000017500000000311612071623704017025 0ustar cmpilatocmpilato#!/usr/bin/python # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # viewvc: View CVS/SVN repositories via a web browser # # ----------------------------------------------------------------------- # # This is a fcgi entry point for the query ViewVC app. It's appropriate # for use with mod_fcgid and flup. It defines a single application function # that is a valid fcgi entry point. # # mod_fcgid: http://httpd.apache.org/mod_fcgid/ # flup: # http://pypi.python.org/pypi/flup # http://trac.saddi.com/flup # # ----------------------------------------------------------------------- import sys, os LIBRARY_DIR = None CONF_PATHNAME = None if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) else: sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../../lib"))) import sapi import viewvc import query from flup.server import fcgi def application(environ, start_response): server = sapi.WsgiServer(environ, start_response) cfg = viewvc.load_config(CONF_PATHNAME, server) viewvc_base_url = cfg.query.viewvc_base_url if viewvc_base_url is None: viewvc_base_url = "viewvc.fcgi" query.main(server, cfg, viewvc_base_url) return [] fcgi.WSGIServer(application).run() viewvc-1.1.22/bin/wsgi/viewvc.fcgi0000644000175000017500000000270012071623704017161 0ustar cmpilatocmpilato#!/usr/bin/python # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # viewvc: View CVS/SVN repositories via a web browser # # ----------------------------------------------------------------------- # # This is a fcgi entry point for the main ViewVC app. It's appropriate # for use with mod_fcgid and flup. It defines a single application function # that is a valid fcgi entry point. # # mod_fcgid: http://httpd.apache.org/mod_fcgid/ # flup: # http://pypi.python.org/pypi/flup # http://trac.saddi.com/flup # # ----------------------------------------------------------------------- import sys, os LIBRARY_DIR = None CONF_PATHNAME = None if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) else: sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../../lib"))) import sapi import viewvc from flup.server import fcgi def application(environ, start_response): server = sapi.WsgiServer(environ, start_response) cfg = viewvc.load_config(CONF_PATHNAME, server) viewvc.main(server, cfg) return [] fcgi.WSGIServer(application).run() viewvc-1.1.22/bin/wsgi/query.wsgi0000644000175000017500000000256312071623704017073 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # viewvc: View CVS/SVN repositories via a web browser # # ----------------------------------------------------------------------- # # This is a wsgi entry point for the query ViewVC app. It's appropriate # for use with mod_wsgi. It defines a single application function that # is a valid wsgi entry point. # # ----------------------------------------------------------------------- import sys, os LIBRARY_DIR = None CONF_PATHNAME = None if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) else: sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../../lib"))) import sapi import viewvc import query def application(environ, start_response): server = sapi.WsgiServer(environ, start_response) cfg = viewvc.load_config(CONF_PATHNAME, server) viewvc_base_url = cfg.query.viewvc_base_url if viewvc_base_url is None: viewvc_base_url = "viewvc.wsgi" query.main(server, cfg, viewvc_base_url) return [] viewvc-1.1.22/bin/wsgi/viewvc.wsgi0000644000175000017500000000234512071623704017227 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # viewvc: View CVS/SVN repositories via a web browser # # ----------------------------------------------------------------------- # # This is a wsgi entry point for the main ViewVC app. It's appropriate # for use with mod_wsgi. It defines a single application function that # is a valid wsgi entry point. # # ----------------------------------------------------------------------- import sys, os LIBRARY_DIR = None CONF_PATHNAME = None if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) else: sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../../lib"))) import sapi import viewvc def application(environ, start_response): server = sapi.WsgiServer(environ, start_response) cfg = viewvc.load_config(CONF_PATHNAME, server) viewvc.main(server, cfg) return [] viewvc-1.1.22/bin/cgi/0000755000175000017500000000000012265242271014617 5ustar cmpilatocmpilatoviewvc-1.1.22/bin/cgi/viewvc-strace.sh0000644000175000017500000000033410406652562017740 0ustar cmpilatocmpilato#!/bin/sh # # Set this script up with something like: # # ScriptAlias /viewvc-strace /home/gstein/src/viewvc/cgi/viewvc-strace.sh # thisdir="`dirname $0`" exec strace -q -r -o /tmp/v-strace.log "${thisdir}/viewvc.cgi" viewvc-1.1.22/bin/cgi/query.cgi0000644000175000017500000000327412071623704016455 0ustar cmpilatocmpilato#!/usr/bin/env python # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # query.cgi: View CVS/SVN commit database by web browser # # ----------------------------------------------------------------------- # # This is a teeny stub to launch the main ViewVC app. It checks the load # average, then loads the (precompiled) viewvc.py file and runs it. # # ----------------------------------------------------------------------- # ######################################################################### # # INSTALL-TIME CONFIGURATION # # These values will be set during the installation process. During # development, they will remain None. # LIBRARY_DIR = None CONF_PATHNAME = None ######################################################################### # # Adjust sys.path to include our library directory # import sys import os if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) else: sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../../lib"))) ######################################################################### import sapi import viewvc import query server = sapi.CgiServer() cfg = viewvc.load_config(CONF_PATHNAME, server) viewvc_base_url = cfg.query.viewvc_base_url if viewvc_base_url is None: viewvc_base_url = "viewvc.cgi" query.main(server, cfg, viewvc_base_url) viewvc-1.1.22/bin/cgi/viewvc.cgi0000644000175000017500000000327312071623704016612 0ustar cmpilatocmpilato#!/usr/bin/env python # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # viewvc: View CVS/SVN repositories via a web browser # # ----------------------------------------------------------------------- # # This is a teeny stub to launch the main ViewVC app. It checks the load # average, then loads the (precompiled) viewvc.py file and runs it. # # ----------------------------------------------------------------------- # ######################################################################### # # INSTALL-TIME CONFIGURATION # # These values will be set during the installation process. During # development, they will remain None. # LIBRARY_DIR = None CONF_PATHNAME = None ######################################################################### # # Adjust sys.path to include our library directory # import sys import os if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) else: sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../../lib"))) ######################################################################### ### add code for checking the load average ######################################################################### # go do the work import sapi import viewvc server = sapi.CgiServer() cfg = viewvc.load_config(CONF_PATHNAME, server) viewvc.main(server, cfg) viewvc-1.1.22/bin/standalone.py0000755000175000017500000007560612071623704016577 0ustar cmpilatocmpilato#!/usr/bin/env python # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- """Run "standalone.py -p " to start an HTTP server on a given port on the local machine to generate ViewVC web pages. """ __author__ = "Peter Funk " __date__ = "11 November 2001" __version__ = "$Revision: 2821 $" __credits__ = """Guido van Rossum, for an excellent programming language. Greg Stein, for writing ViewCVS in the first place. Ka-Ping Yee, for the GUI code and the framework stolen from pydoc.py. """ # INSTALL-TIME CONFIGURATION # # These values will be set during the installation process. During # development, they will remain None. # LIBRARY_DIR = None CONF_PATHNAME = None import sys import os import os.path import stat import string import urllib import rfc822 import socket import select import base64 import BaseHTTPServer if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) else: sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib"))) import sapi import viewvc import compat; compat.for_standalone() # The 'crypt' module is only available on Unix platforms. We'll try # to use 'fcrypt' if it's available (for more information, see # http://carey.geek.nz/code/python-fcrypt/). has_crypt = False try: import crypt has_crypt = True def _check_passwd(user_passwd, real_passwd): return real_passwd == crypt.crypt(user_passwd, real_passwd[:2]) except ImportError: try: import fcrypt has_crypt = True def _check_passwd(user_passwd, real_passwd): return real_passwd == fcrypt.crypt(user_passwd, real_passwd[:2]) except ImportError: def _check_passwd(user_passwd, real_passwd): return False class Options: port = 49152 # default TCP/IP port used for the server repositories = {} # use default repositories specified in config host = sys.platform == 'mac' and '127.0.0.1' or 'localhost' script_alias = 'viewvc' config_file = None htpasswd_file = None class StandaloneServer(sapi.CgiServer): """Custom sapi interface that uses a BaseHTTPRequestHandler HANDLER to generate output.""" def __init__(self, handler): sapi.CgiServer.__init__(self, inheritableOut = sys.platform != "win32") self.handler = handler def header(self, content_type='text/html', status=None): if not self.headerSent: self.headerSent = 1 if status is None: statusCode = 200 statusText = 'OK' else: p = string.find(status, ' ') if p < 0: statusCode = int(status) statusText = '' else: statusCode = int(status[:p]) statusText = status[p+1:] self.handler.send_response(statusCode, statusText) self.handler.send_header("Content-type", content_type) for (name, value) in self.headers: self.handler.send_header(name, value) self.handler.end_headers() class NotViewVCLocationException(Exception): """The request location was not aimed at ViewVC.""" pass class AuthenticationException(Exception): """Authentication requirements have not been met.""" pass class ViewVCHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): """Custom HTTP request handler for ViewVC.""" def do_GET(self): """Serve a GET request.""" self.handle_request('GET') def do_POST(self): """Serve a POST request.""" self.handle_request('POST') def handle_request(self, method): """Handle a request of type METHOD.""" try: self.run_viewvc() except NotViewVCLocationException: # If the request was aimed at the server root, but there's a # non-empty script_alias, automatically redirect to the # script_alias. Otherwise, just return a 404 and shrug. if (not self.path or self.path == "/") and options.script_alias: new_url = self.server.url + options.script_alias + '/' self.send_response(301, "Moved Permanently") self.send_header("Content-type", "text/html") self.send_header("Location", new_url) self.end_headers() self.wfile.write(""" Moved Temporarily

Redirecting to ViewVC

You will be automatically redirected to ViewVC. If this doesn't work, please click on the link above.

""" % (new_url, new_url)) else: self.send_error(404) except IOError: # ignore IOError: [Errno 32] Broken pipe pass except AuthenticationException: self.send_response(401, "Unauthorized") self.send_header("WWW-Authenticate", 'Basic realm="ViewVC"') self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(""" Authentication failed

Authentication failed

Authentication has failed. Please retry with the correct username and password.

""") def is_viewvc(self): """Check whether self.path is, or is a child of, the ScriptAlias""" if not options.script_alias: return 1 if self.path == '/' + options.script_alias: return 1 alias_len = len(options.script_alias) if self.path[:alias_len+2] == '/' + options.script_alias + '/': return 1 if self.path[:alias_len+2] == '/' + options.script_alias + '?': return 1 return 0 def validate_password(self, htpasswd_file, username, password): """Compare USERNAME and PASSWORD against HTPASSWD_FILE.""" try: lines = open(htpasswd_file, 'r').readlines() for line in lines: file_user, file_pass = string.split(line.rstrip(), ':', 1) if username == file_user: return _check_passwd(password, file_pass) except: pass return False def run_viewvc(self): """Run ViewVC to field a single request.""" ### Much of this is adapter from Python's standard library ### module CGIHTTPServer. # Is this request even aimed at ViewVC? If not, complain. if not self.is_viewvc(): raise NotViewVCLocationException() # If htpasswd authentication is enabled, try to authenticate the user. self.username = None if options.htpasswd_file: authn = self.headers.get('authorization') if not authn: raise AuthenticationException() try: kind, data = string.split(authn, ' ', 1) if kind == 'Basic': data = base64.b64decode(data) username, password = string.split(data, ':', 1) except: raise AuthenticationException() if not self.validate_password(options.htpasswd_file, username, password): raise AuthenticationException() self.username = username # Setup the environment in preparation of executing ViewVC's core code. env = os.environ scriptname = options.script_alias and '/' + options.script_alias or '' viewvc_url = self.server.url[:-1] + scriptname rest = self.path[len(scriptname):] i = string.rfind(rest, '?') if i >= 0: rest, query = rest[:i], rest[i+1:] else: query = '' # Since we're going to modify the env in the parent, provide empty # values to override previously set values for k in env.keys(): if k[:5] == 'HTTP_': del env[k] for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', 'HTTP_USER_AGENT', 'HTTP_COOKIE'): if env.has_key(k): env[k] = "" # XXX Much of the following could be prepared ahead of time! env['SERVER_SOFTWARE'] = self.version_string() env['SERVER_NAME'] = self.server.server_name env['GATEWAY_INTERFACE'] = 'CGI/1.1' env['SERVER_PROTOCOL'] = self.protocol_version env['SERVER_PORT'] = str(self.server.server_port) env['REQUEST_METHOD'] = self.command uqrest = urllib.unquote(rest) env['PATH_INFO'] = uqrest env['SCRIPT_NAME'] = scriptname if query: env['QUERY_STRING'] = query env['HTTP_HOST'] = self.server.address[0] host = self.address_string() if host != self.client_address[0]: env['REMOTE_HOST'] = host env['REMOTE_ADDR'] = self.client_address[0] if self.username: env['REMOTE_USER'] = self.username if self.headers.typeheader is None: env['CONTENT_TYPE'] = self.headers.type else: env['CONTENT_TYPE'] = self.headers.typeheader length = self.headers.getheader('content-length') if length: env['CONTENT_LENGTH'] = length accept = [] for line in self.headers.getallmatchingheaders('accept'): if line[:1] in string.whitespace: accept.append(string.strip(line)) else: accept = accept + string.split(line[7:], ',') env['HTTP_ACCEPT'] = string.joinfields(accept, ',') ua = self.headers.getheader('user-agent') if ua: env['HTTP_USER_AGENT'] = ua modified = self.headers.getheader('if-modified-since') if modified: env['HTTP_IF_MODIFIED_SINCE'] = modified etag = self.headers.getheader('if-none-match') if etag: env['HTTP_IF_NONE_MATCH'] = etag # AUTH_TYPE # REMOTE_IDENT # XXX Other HTTP_* headers # Preserve state, because we execute script in current process: save_argv = sys.argv save_stdin = sys.stdin save_stdout = sys.stdout save_stderr = sys.stderr # For external tools like enscript we also need to redirect # the real stdout file descriptor. # # FIXME: This code used to carry the following comment: # # (On windows, reassigning the sys.stdout variable is sufficient # because pipe_cmds makes it the standard output for child # processes.) # # But we no longer use pipe_cmds. So at the very least, the # comment is stale. Is the code okay, though? if sys.platform != "win32": save_realstdout = os.dup(1) try: try: sys.stdout = self.wfile if sys.platform != "win32": os.dup2(self.wfile.fileno(), 1) sys.stdin = self.rfile viewvc.main(StandaloneServer(self), cfg) finally: sys.argv = save_argv sys.stdin = save_stdin sys.stdout.flush() if sys.platform != "win32": os.dup2(save_realstdout, 1) os.close(save_realstdout) sys.stdout = save_stdout sys.stderr = save_stderr except SystemExit, status: self.log_error("ViewVC exit status %s", str(status)) else: self.log_error("ViewVC exited ok") class ViewVCHTTPServer(BaseHTTPServer.HTTPServer): """Customized HTTP server for ViewVC.""" def __init__(self, host, port, callback): self.address = (host, port) self.url = 'http://%s:%d/' % (host, port) self.callback = callback BaseHTTPServer.HTTPServer.__init__(self, self.address, self.handler) def serve_until_quit(self): self.quit = 0 while not self.quit: rd, wr, ex = select.select([self.socket.fileno()], [], [], 1) if rd: self.handle_request() def server_activate(self): BaseHTTPServer.HTTPServer.server_activate(self) if self.callback: self.callback(self) def server_bind(self): # set SO_REUSEADDR (if available on this platform) if hasattr(socket, 'SOL_SOCKET') and hasattr(socket, 'SO_REUSEADDR'): self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) BaseHTTPServer.HTTPServer.server_bind(self) def serve(host, port, callback=None): """Start an HTTP server for HOST on PORT. Call CALLBACK function when the server is ready to serve.""" ViewVCHTTPServer.handler = ViewVCHTTPRequestHandler try: # XXX Move this code out of this function. # Early loading of configuration here. Used to allow tinkering # with some configuration settings: handle_config(options.config_file) if options.repositories: cfg.general.default_root = "Development" for repo_name in options.repositories.keys(): repo_path = os.path.normpath(options.repositories[repo_name]) if os.path.exists(os.path.join(repo_path, "CVSROOT", "config")): cfg.general.cvs_roots[repo_name] = repo_path elif os.path.exists(os.path.join(repo_path, "format")): cfg.general.svn_roots[repo_name] = repo_path elif cfg.general.cvs_roots.has_key("Development") and \ not os.path.isdir(cfg.general.cvs_roots["Development"]): sys.stderr.write("*** No repository found. Please use the -r option.\n") sys.stderr.write(" Use --help for more info.\n") raise KeyboardInterrupt # Hack! os.close(0) # To avoid problems with shell job control # always use default docroot location cfg.options.docroot = None # if cvsnt isn't found, fall back to rcs if (cfg.conf_path is None and cfg.utilities.cvsnt): import popen cvsnt_works = 0 try: fp = popen.popen(cfg.utilities.cvsnt, ['--version'], 'rt') try: while 1: line = fp.readline() if not line: break if string.find(line, "Concurrent Versions System (CVSNT)") >= 0: cvsnt_works = 1 while fp.read(4096): pass break finally: fp.close() except: pass if not cvsnt_works: cfg.utilities.cvsnt = None ViewVCHTTPServer(host, port, callback).serve_until_quit() except (KeyboardInterrupt, select.error): pass print 'server stopped' def handle_config(config_file): global cfg cfg = viewvc.load_config(config_file or CONF_PATHNAME) # --- graphical interface: -------------------------------------------------- def nogui(missing_module): sys.stderr.write("""\ Sorry! Your Python was compiled without the %s module enabled. I'm unable to run the GUI part. Please omit the '-g' and '--gui' options, or install another Python interpreter. """ % (missing_module)) raise SystemExit, 1 def gui(host, port): """Graphical interface (starts web server and pops up a control window).""" class GUI: def __init__(self, window, host, port): self.window = window self.server = None self.scanner = None try: import Tkinter except ImportError: nogui("Tkinter") self.server_frm = Tkinter.Frame(window) self.title_lbl = Tkinter.Label(self.server_frm, text='Starting server...\n ') self.open_btn = Tkinter.Button(self.server_frm, text='open browser', command=self.open, state='disabled') self.quit_btn = Tkinter.Button(self.server_frm, text='quit serving', command=self.quit, state='disabled') self.window.title('ViewVC standalone') self.window.protocol('WM_DELETE_WINDOW', self.quit) self.title_lbl.pack(side='top', fill='x') self.open_btn.pack(side='left', fill='x', expand=1) self.quit_btn.pack(side='right', fill='x', expand=1) # Early loading of configuration here. Used to # allow tinkering with configuration settings through the gui: handle_config(options.config_file) if not LIBRARY_DIR: cfg.options.cvsgraph_conf = "../cgi/cvsgraph.conf.dist" self.options_frm = Tkinter.Frame(window) # cvsgraph toggle: self.cvsgraph_ivar = Tkinter.IntVar() self.cvsgraph_ivar.set(cfg.options.use_cvsgraph) self.cvsgraph_toggle = \ Tkinter.Checkbutton(self.options_frm, text="enable cvsgraph (needs binary)", var=self.cvsgraph_ivar, command=self.toggle_use_cvsgraph) self.cvsgraph_toggle.pack(side='top', anchor='w') # show_subdir_lastmod toggle: self.subdirmod_ivar = Tkinter.IntVar() self.subdirmod_ivar.set(cfg.options.show_subdir_lastmod) self.subdirmod_toggle = \ Tkinter.Checkbutton(self.options_frm, text="show subdir last mod (dir view)", var=self.subdirmod_ivar, command=self.toggle_subdirmod) self.subdirmod_toggle.pack(side='top', anchor='w') # use_re_search toggle: self.useresearch_ivar = Tkinter.IntVar() self.useresearch_ivar.set(cfg.options.use_re_search) self.useresearch_toggle = \ Tkinter.Checkbutton(self.options_frm, text="allow regular expr search", var=self.useresearch_ivar, command=self.toggle_useresearch) self.useresearch_toggle.pack(side='top', anchor='w') # use_localtime toggle: self.use_localtime_ivar = Tkinter.IntVar() self.use_localtime_ivar.set(cfg.options.use_localtime) self.use_localtime_toggle = \ Tkinter.Checkbutton(self.options_frm, text="use localtime (instead of UTC)", var=self.use_localtime_ivar, command=self.toggle_use_localtime) self.use_localtime_toggle.pack(side='top', anchor='w') # log_pagesize integer var: self.log_pagesize_lbl = \ Tkinter.Label(self.options_frm, text='number of items per log page (0 disables):') self.log_pagesize_lbl.pack(side='top', anchor='w') self.log_pagesize_ivar = Tkinter.IntVar() self.log_pagesize_ivar.set(cfg.options.log_pagesize) self.log_pagesize_entry = \ Tkinter.Entry(self.options_frm, width=10, textvariable=self.log_pagesize_ivar) self.log_pagesize_entry.bind('', self.set_log_pagesize) self.log_pagesize_entry.pack(side='top', anchor='w') # dir_pagesize integer var: self.dir_pagesize_lbl = \ Tkinter.Label(self.options_frm, text='number of items per dir page (0 disables):') self.dir_pagesize_lbl.pack(side='top', anchor='w') self.dir_pagesize_ivar = Tkinter.IntVar() self.dir_pagesize_ivar.set(cfg.options.dir_pagesize) self.dir_pagesize_entry = \ Tkinter.Entry(self.options_frm, width=10, textvariable=self.dir_pagesize_ivar) self.dir_pagesize_entry.bind('', self.set_dir_pagesize) self.dir_pagesize_entry.pack(side='top', anchor='w') # directory view template: self.dirtemplate_lbl = \ Tkinter.Label(self.options_frm, text='Choose HTML Template for the Directory pages:') self.dirtemplate_lbl.pack(side='top', anchor='w') self.dirtemplate_svar = Tkinter.StringVar() self.dirtemplate_svar.set(cfg.templates.directory) self.dirtemplate_entry = \ Tkinter.Entry(self.options_frm, width=40, textvariable=self.dirtemplate_svar) self.dirtemplate_entry.bind('', self.set_templates_directory) self.dirtemplate_entry.pack(side='top', anchor='w') self.templates_dir = \ Tkinter.Radiobutton(self.options_frm, text="directory.ezt", value="templates/directory.ezt", var=self.dirtemplate_svar, command=self.set_templates_directory) self.templates_dir.pack(side='top', anchor='w') self.templates_dir_alt = \ Tkinter.Radiobutton(self.options_frm, text="dir_alternate.ezt", value="templates/dir_alternate.ezt", var=self.dirtemplate_svar, command=self.set_templates_directory) self.templates_dir_alt.pack(side='top', anchor='w') # log view template: self.logtemplate_lbl = \ Tkinter.Label(self.options_frm, text='Choose HTML Template for the Log pages:') self.logtemplate_lbl.pack(side='top', anchor='w') self.logtemplate_svar = Tkinter.StringVar() self.logtemplate_svar.set(cfg.templates.log) self.logtemplate_entry = \ Tkinter.Entry(self.options_frm, width=40, textvariable=self.logtemplate_svar) self.logtemplate_entry.bind('', self.set_templates_log) self.logtemplate_entry.pack(side='top', anchor='w') self.templates_log = \ Tkinter.Radiobutton(self.options_frm, text="log.ezt", value="templates/log.ezt", var=self.logtemplate_svar, command=self.set_templates_log) self.templates_log.pack(side='top', anchor='w') self.templates_log_table = \ Tkinter.Radiobutton(self.options_frm, text="log_table.ezt", value="templates/log_table.ezt", var=self.logtemplate_svar, command=self.set_templates_log) self.templates_log_table.pack(side='top', anchor='w') # query view template: self.querytemplate_lbl = \ Tkinter.Label(self.options_frm, text='Template for the database query page:') self.querytemplate_lbl.pack(side='top', anchor='w') self.querytemplate_svar = Tkinter.StringVar() self.querytemplate_svar.set(cfg.templates.query) self.querytemplate_entry = \ Tkinter.Entry(self.options_frm, width=40, textvariable=self.querytemplate_svar) self.querytemplate_entry.bind('', self.set_templates_query) self.querytemplate_entry.pack(side='top', anchor='w') self.templates_query = \ Tkinter.Radiobutton(self.options_frm, text="query.ezt", value="templates/query.ezt", var=self.querytemplate_svar, command=self.set_templates_query) self.templates_query.pack(side='top', anchor='w') # pack and set window manager hints: self.server_frm.pack(side='top', fill='x') self.options_frm.pack(side='top', fill='x') self.window.update() self.minwidth = self.window.winfo_width() self.minheight = self.window.winfo_height() self.expanded = 0 self.window.wm_geometry('%dx%d' % (self.minwidth, self.minheight)) self.window.wm_minsize(self.minwidth, self.minheight) try: import threading except ImportError: nogui("thread") threading.Thread(target=serve, args=(host, port, self.ready)).start() def toggle_use_cvsgraph(self, event=None): cfg.options.use_cvsgraph = self.cvsgraph_ivar.get() def toggle_use_localtime(self, event=None): cfg.options.use_localtime = self.use_localtime_ivar.get() def toggle_subdirmod(self, event=None): cfg.options.show_subdir_lastmod = self.subdirmod_ivar.get() def toggle_useresearch(self, event=None): cfg.options.use_re_search = self.useresearch_ivar.get() def set_log_pagesize(self, event=None): cfg.options.log_pagesize = self.log_pagesize_ivar.get() def set_dir_pagesize(self, event=None): cfg.options.dir_pagesize = self.dir_pagesize_ivar.get() def set_templates_log(self, event=None): cfg.templates.log = self.logtemplate_svar.get() def set_templates_directory(self, event=None): cfg.templates.directory = self.dirtemplate_svar.get() def set_templates_query(self, event=None): cfg.templates.query = self.querytemplate_svar.get() def ready(self, server): """used as callback parameter to the serve() function""" self.server = server self.title_lbl.config(text='ViewVC standalone server at\n' + server.url) self.open_btn.config(state='normal') self.quit_btn.config(state='normal') def open(self, event=None, url=None): """opens a browser window on the local machine""" url = url or self.server.url try: import webbrowser webbrowser.open(url) except ImportError: # pre-webbrowser.py compatibility if sys.platform == 'win32': os.system('start "%s"' % url) elif sys.platform == 'mac': try: import ic ic.launchurl(url) except ImportError: pass else: rc = os.system('netscape -remote "openURL(%s)" &' % url) if rc: os.system('netscape "%s" &' % url) def quit(self, event=None): if self.server: self.server.quit = 1 self.window.quit() import Tkinter try: gui = GUI(Tkinter.Tk(), host, port) Tkinter.mainloop() except KeyboardInterrupt: pass # --- command-line interface: ---------------------------------------------- def usage(): clean_options = Options() cmd = os.path.basename(sys.argv[0]) port = clean_options.port host = clean_options.host script_alias = clean_options.script_alias sys.stderr.write("""Usage: %(cmd)s [OPTIONS] Run a simple, standalone HTTP server configured to serve up ViewVC requests. Options: --config-file=FILE (-c) Read configuration options from FILE. If not specified, ViewVC will look for a configuration file in its installation tree, falling back to built-in default values. (Not valid in GUI mode.) --daemon (-d) Background the server process. --gui (-g) Pop up a graphical configuration interface. Requires a valid X11 display connection. --help Show this usage message and exit. --host=HOSTNAME (-h) Listen on HOSTNAME. Required for access from a remote machine. [default: %(host)s] --htpasswd-file=FILE Authenticate incoming requests, validating against against FILE, which is an Apache HTTP Server htpasswd file. (CRYPT only; no DIGEST support.) --port=PORT (-p) Listen on PORT. [default: %(port)d] --repository=PATH (-r) Serve the Subversion or CVS repository located at PATH. This option may be used more than once. --script-alias=PATH (-s) Use PATH as the virtual script location (similar to Apache HTTP Server's ScriptAlias directive). For example, "--script-alias=repo/view" will serve ViewVC at "http://HOSTNAME:PORT/repo/view". [default: %(script_alias)s] """ % locals()) sys.exit(0) def badusage(errstr): cmd = os.path.basename(sys.argv[0]) sys.stderr.write("ERROR: %s\n\n" "Try '%s --help' for detailed usage information.\n" % (errstr, cmd)) sys.exit(1) def main(argv): """Command-line interface (looks at argv to decide what to do).""" import getopt short_opts = string.join(['c:', 'd', 'g', 'h:', 'p:', 'r:', 's:', ], '') long_opts = ['daemon', 'config-file=', 'gui', 'help', 'host=', 'htpasswd-file=', 'port=', 'repository=', 'script-alias=', ] opt_daemon = 0 opt_gui = 0 opt_host = None opt_port = None opt_htpasswd_file = None opt_config_file = None opt_script_alias = None opt_repositories = [] # Parse command-line options. try: opts, args = getopt.getopt(argv[1:], short_opts, long_opts) for opt, val in opts: if opt in ['--help']: usage() elif opt in ['-g', '--gui']: opt_gui = 1 elif opt in ['-r', '--repository']: # may be used more than once opt_repositories.append(val) elif opt in ['-d', '--daemon']: opt_daemon = 1 elif opt in ['-p', '--port']: opt_port = val elif opt in ['-h', '--host']: opt_host = val elif opt in ['-s', '--script-alias']: opt_script_alias = val elif opt in ['-c', '--config-file']: opt_config_file = val elif opt in ['--htpasswd-file']: opt_htpasswd_file = val except getopt.error, err: badusage(str(err)) # Validate options that need validating. class BadUsage(Exception): pass try: if opt_port is not None: try: options.port = int(opt_port) except ValueError: raise BadUsage("Port '%s' is not a valid port number" % (opt_port)) if not options.port: raise BadUsage("You must supply a valid port.") if opt_htpasswd_file is not None: if not os.path.isfile(opt_htpasswd_file): raise BadUsage("'%s' does not appear to be a valid htpasswd file." % (opt_htpasswd_file)) if not has_crypt: raise BadUsage("Unable to locate suitable `crypt' module for use " "with --htpasswd-file option. If your Python " "distribution does not include this module (as is " "the case on many non-Unix platforms), consider " "installing the `fcrypt' module instead (see " "http://carey.geek.nz/code/python-fcrypt/).") options.htpasswd_file = opt_htpasswd_file if opt_config_file is not None: if not os.path.isfile(opt_config_file): raise BadUsage("'%s' does not appear to be a valid configuration file." % (opt_config_file)) options.config_file = opt_config_file if opt_host is not None: options.host = opt_host if opt_script_alias is not None: options.script_alias = string.join(filter(None, string.split(opt_script_alias, '/')), '/') for repository in opt_repositories: if not options.repositories.has_key('Development'): rootname = 'Development' else: rootname = 'Repository%d' % (len(options.repositories.keys()) + 1) options.repositories[rootname] = repository except BadUsage, err: badusage(str(err)) # Fork if we're in daemon mode. if opt_daemon: pid = os.fork() if pid != 0: sys.exit() # Finaly, start the server. if opt_gui: gui(options.host, options.port) else: def ready(server): print 'server ready at %s%s' % (server.url, options.script_alias) serve(options.host, options.port, ready) if __name__ == '__main__': options = Options() main(sys.argv) viewvc-1.1.22/bin/make-database0000755000175000017500000002631512113165412016462 0ustar cmpilatocmpilato#!/usr/bin/env python # -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # administrative program for CVSdb; creates a clean database in # MySQL 3.22 or later # # ----------------------------------------------------------------------- import os, sys, string import popen2 import getopt ## ------------------------------------------------------------------------ ## Stuff common to all schemas ## DATABASE_SCRIPT_COMMON="""\ DROP DATABASE IF EXISTS ; CREATE DATABASE ; USE ; """ ## ------------------------------------------------------------------------ ## Version 0: The original, Bonsai-compatible schema. ## DATABASE_SCRIPT_VERSION_0="""\ DROP TABLE IF EXISTS branches; CREATE TABLE branches ( id mediumint(9) NOT NULL auto_increment, branch varchar(64) binary DEFAULT '' NOT NULL, PRIMARY KEY (id), UNIQUE branch (branch) ) ENGINE=MyISAM; DROP TABLE IF EXISTS checkins; CREATE TABLE checkins ( type enum('Change','Add','Remove'), ci_when datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, whoid mediumint(9) DEFAULT '0' NOT NULL, repositoryid mediumint(9) DEFAULT '0' NOT NULL, dirid mediumint(9) DEFAULT '0' NOT NULL, fileid mediumint(9) DEFAULT '0' NOT NULL, revision varchar(32) binary DEFAULT '' NOT NULL, stickytag varchar(255) binary DEFAULT '' NOT NULL, branchid mediumint(9) DEFAULT '0' NOT NULL, addedlines int(11) DEFAULT '0' NOT NULL, removedlines int(11) DEFAULT '0' NOT NULL, descid mediumint(9), UNIQUE repositoryid (repositoryid,dirid,fileid,revision), KEY ci_when (ci_when), KEY whoid (whoid), KEY repositoryid_2 (repositoryid), KEY dirid (dirid), KEY fileid (fileid), KEY branchid (branchid) ) ENGINE=MyISAM; DROP TABLE IF EXISTS descs; CREATE TABLE descs ( id mediumint(9) NOT NULL auto_increment, description text, hash bigint(20) DEFAULT '0' NOT NULL, PRIMARY KEY (id), KEY hash (hash) ) ENGINE=MyISAM; DROP TABLE IF EXISTS dirs; CREATE TABLE dirs ( id mediumint(9) NOT NULL auto_increment, dir varchar(255) binary DEFAULT '' NOT NULL, PRIMARY KEY (id), UNIQUE dir (dir) ) ENGINE=MyISAM; DROP TABLE IF EXISTS files; CREATE TABLE files ( id mediumint(9) NOT NULL auto_increment, file varchar(255) binary DEFAULT '' NOT NULL, PRIMARY KEY (id), UNIQUE file (file) ) ENGINE=MyISAM; DROP TABLE IF EXISTS people; CREATE TABLE people ( id mediumint(9) NOT NULL auto_increment, who varchar(128) binary DEFAULT '' NOT NULL, PRIMARY KEY (id), UNIQUE who (who) ) ENGINE=MyISAM; DROP TABLE IF EXISTS repositories; CREATE TABLE repositories ( id mediumint(9) NOT NULL auto_increment, repository varchar(64) binary DEFAULT '' NOT NULL, PRIMARY KEY (id), UNIQUE repository (repository) ) ENGINE=MyISAM; DROP TABLE IF EXISTS tags; CREATE TABLE tags ( repositoryid mediumint(9) DEFAULT '0' NOT NULL, branchid mediumint(9) DEFAULT '0' NOT NULL, dirid mediumint(9) DEFAULT '0' NOT NULL, fileid mediumint(9) DEFAULT '0' NOT NULL, revision varchar(32) binary DEFAULT '' NOT NULL, UNIQUE repositoryid (repositoryid,dirid,fileid,branchid,revision), KEY repositoryid_2 (repositoryid), KEY dirid (dirid), KEY fileid (fileid), KEY branchid (branchid) ) ENGINE=MyISAM; """ ## ------------------------------------------------------------------------ ## Version 1: Adds the 'metadata' table. Adds 'descid' index to ## 'checkins' table, and renames that table to 'commits'. ## DATABASE_SCRIPT_VERSION_1="""\ DROP TABLE IF EXISTS branches; CREATE TABLE branches ( id mediumint(9) NOT NULL auto_increment, branch varchar(64) binary DEFAULT '' NOT NULL, PRIMARY KEY (id), UNIQUE branch (branch) ) ENGINE=MyISAM; DROP TABLE IF EXISTS commits; CREATE TABLE commits ( type enum('Change','Add','Remove'), ci_when datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, whoid mediumint(9) DEFAULT '0' NOT NULL, repositoryid mediumint(9) DEFAULT '0' NOT NULL, dirid mediumint(9) DEFAULT '0' NOT NULL, fileid mediumint(9) DEFAULT '0' NOT NULL, revision varchar(32) binary DEFAULT '' NOT NULL, stickytag varchar(255) binary DEFAULT '' NOT NULL, branchid mediumint(9) DEFAULT '0' NOT NULL, addedlines int(11) DEFAULT '0' NOT NULL, removedlines int(11) DEFAULT '0' NOT NULL, descid mediumint(9), UNIQUE repositoryid (repositoryid,dirid,fileid,revision), KEY ci_when (ci_when), KEY whoid (whoid), KEY repositoryid_2 (repositoryid), KEY dirid (dirid), KEY fileid (fileid), KEY branchid (branchid), KEY descid (descid) ) ENGINE=MyISAM; DROP TABLE IF EXISTS descs; CREATE TABLE descs ( id mediumint(9) NOT NULL auto_increment, description text, hash bigint(20) DEFAULT '0' NOT NULL, PRIMARY KEY (id), KEY hash (hash) ) ENGINE=MyISAM; DROP TABLE IF EXISTS dirs; CREATE TABLE dirs ( id mediumint(9) NOT NULL auto_increment, dir varchar(255) binary DEFAULT '' NOT NULL, PRIMARY KEY (id), UNIQUE dir (dir) ) ENGINE=MyISAM; DROP TABLE IF EXISTS files; CREATE TABLE files ( id mediumint(9) NOT NULL auto_increment, file varchar(255) binary DEFAULT '' NOT NULL, PRIMARY KEY (id), UNIQUE file (file) ) ENGINE=MyISAM; DROP TABLE IF EXISTS people; CREATE TABLE people ( id mediumint(9) NOT NULL auto_increment, who varchar(128) binary DEFAULT '' NOT NULL, PRIMARY KEY (id), UNIQUE who (who) ) ENGINE=MyISAM; DROP TABLE IF EXISTS repositories; CREATE TABLE repositories ( id mediumint(9) NOT NULL auto_increment, repository varchar(64) binary DEFAULT '' NOT NULL, PRIMARY KEY (id), UNIQUE repository (repository) ) ENGINE=MyISAM; DROP TABLE IF EXISTS tags; CREATE TABLE tags ( repositoryid mediumint(9) DEFAULT '0' NOT NULL, branchid mediumint(9) DEFAULT '0' NOT NULL, dirid mediumint(9) DEFAULT '0' NOT NULL, fileid mediumint(9) DEFAULT '0' NOT NULL, revision varchar(32) binary DEFAULT '' NOT NULL, UNIQUE repositoryid (repositoryid,dirid,fileid,branchid,revision), KEY repositoryid_2 (repositoryid), KEY dirid (dirid), KEY fileid (fileid), KEY branchid (branchid) ) ENGINE=MyISAM; DROP TABLE IF EXISTS metadata; CREATE TABLE metadata ( name varchar(255) binary DEFAULT '' NOT NULL, value text, PRIMARY KEY (name), UNIQUE name (name) ) ENGINE=MyISAM; INSERT INTO metadata (name, value) VALUES ('version', '1'); """ BONSAI_COMPAT=""" WARNING: Creating Bonsai-compatible legacy database version. Some ViewVC features may not be available, or may not perform especially well. """ ## ------------------------------------------------------------------------ def usage_and_exit(errmsg=None): stream = errmsg is None and sys.stdout or sys.stderr stream.write("""\ Usage: %s [OPTIONS] This script creates the database and tables in MySQL used by the ViewVC checkin database. In order to operate correctly, it needs to know the following: your database server hostname, database user, database user password, and database name. (You will be prompted for any of this information that you do not provide via command-line options.) This script will use the 'mysql' program to create the database for you. You will then need to set the appropriate parameters in the [cvsdb] section of your viewvc.conf file. NOTE: If a hostname or port is supplied at the command line or during interactive prompting, this script will pass '--protocol=TCP' to 'mysql'. Options: --dbname=ARG Use ARG as the ViewVC database name to create. [Default: ViewVC] --help Show this usage message. --hostname=ARG Use ARG as the hostname for the MySQL connection. --port=ARG Use ARG as the port for the MySQL connection. --password=ARG Use ARG as the password for the MySQL connection. --username=ARG Use ARG as the username for the MySQL connection. --version=ARG Create the database using the schema employed by version ARG of ViewVC. Valid values are: [ "1.0" ] """ % (os.path.basename(sys.argv[0]))) if errmsg is not None: stream.write("[ERROR] %s.\n" % (errmsg)) sys.exit(1) sys.exit(0) if __name__ == "__main__": try: # Parse the command-line options, if any. dbname = version = hostname = port = username = password = None opts, args = getopt.getopt(sys.argv[1:], '', [ 'dbname=', 'help', 'hostname=', 'port=', 'password=', 'username=', 'version=', ]) if len(args) > 0: usage_and_exit("Unexpected command-line parameters") for name, value in opts: if name == '--help': usage_and_exit(0) elif name == '--dbname': dbname = value elif name == '--hostname': hostname = value elif name == '--port': port = value elif name == '--username': username = value elif name == '--password': password = value elif name == '--version': if value in ["1.0"]: version = value else: usage_and_exit("Invalid version specified") # Prompt for information not provided via command-line options. if hostname is None: hostname = raw_input("MySQL Hostname (leave blank for default): ") if port is None: port = raw_input("MySQL Port (leave blank for default): ") if username is None: username = raw_input("MySQL User: ") if password is None: password = raw_input("MySQL Password: ") if dbname is None: dbname = raw_input("ViewVC Database Name [default: ViewVC]: ") or "ViewVC" # Create the database. dscript = string.replace(DATABASE_SCRIPT_COMMON, "", dbname) if version == "1.0": print BONSAI_COMPAT dscript = dscript + DATABASE_SCRIPT_VERSION_0 else: dscript = dscript + DATABASE_SCRIPT_VERSION_1 # Calculate command arguments. cmd_args = "--user=%s --password=%s" % (username, password) if hostname or port: cmd_args = cmd_args + " --protocol=TCP" if hostname: cmd_args = cmd_args + " --host=%s" % (hostname) if port: cmd_args = cmd_args + " --port=%s" % (port) if sys.platform == "win32": cmd = "mysql %s" % (cmd_args) mysql = os.popen(cmd, "w") # popen2.Popen3 is not provided on windows mysql.write(dscript) status = mysql.close() else: cmd = "{ mysql %s ; } 2>&1" % (cmd_args) pipes = popen2.Popen3(cmd) pipes.tochild.write(dscript) pipes.tochild.close() print pipes.fromchild.read() status = pipes.wait() if status: print "[ERROR] The database did not create sucessfully." sys.exit(1) print "Database created successfully. Don't forget to configure the " print "[cvsdb] section of your viewvc.conf file." except KeyboardInterrupt: pass sys.exit(0) viewvc-1.1.22/bin/mod_python/0000755000175000017500000000000012265242270016234 5ustar cmpilatocmpilatoviewvc-1.1.22/bin/mod_python/handler.py0000644000175000017500000000163412071623704020227 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # Mod_Python handler based on mod_python.publisher # # ----------------------------------------------------------------------- from mod_python import apache import os.path def handler(req): path, module_name = os.path.split(req.filename) module_name, module_ext = os.path.splitext(module_name) try: module = apache.import_module(module_name, path=[path]) except ImportError: raise apache.SERVER_RETURN, apache.HTTP_NOT_FOUND req.add_common_vars() module.index(req) return apache.OK viewvc-1.1.22/bin/mod_python/query.py0000644000175000017500000000362212071623704017756 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # ViewVC: View CVS/SVN repositories via a web browser # # ----------------------------------------------------------------------- # # This is a teeny stub to launch the main ViewVC app. It checks the load # average, then loads the (precompiled) viewvc.py file and runs it. # # ----------------------------------------------------------------------- # ######################################################################### # # INSTALL-TIME CONFIGURATION # # These values will be set during the installation process. During # development, they will remain None. # LIBRARY_DIR = None CONF_PATHNAME = None ######################################################################### # # Adjust sys.path to include our library directory # import sys if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) import sapi import imp # Import real ViewVC module fp, pathname, description = imp.find_module('viewvc', [LIBRARY_DIR]) try: viewvc = imp.load_module('viewvc', fp, pathname, description) finally: if fp: fp.close() # Import real ViewVC Query modules fp, pathname, description = imp.find_module('query', [LIBRARY_DIR]) try: query = imp.load_module('query', fp, pathname, description) finally: if fp: fp.close() cfg = viewvc.load_config(CONF_PATHNAME) def index(req): server = sapi.ModPythonServer(req) try: viewvc_base_url = cfg.query.viewvc_base_url if viewvc_base_url is None: viewvc_base_url = "viewvc.py" query.main(server, cfg, viewvc_base_url) finally: server.close() viewvc-1.1.22/bin/mod_python/viewvc.py0000644000175000017500000000311212071623704020106 0ustar cmpilatocmpilato# -*-python-*- # # Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # viewvc: View CVS/SVN repositories via a web browser # # ----------------------------------------------------------------------- # # This is a teeny stub to launch the main ViewVC app. It checks the load # average, then loads the (precompiled) viewvc.py file and runs it. # # ----------------------------------------------------------------------- # ######################################################################### # # INSTALL-TIME CONFIGURATION # # These values will be set during the installation process. During # development, they will remain None. # LIBRARY_DIR = None CONF_PATHNAME = None ######################################################################### # # Adjust sys.path to include our library directory # import sys if LIBRARY_DIR: sys.path.insert(0, LIBRARY_DIR) import sapi import imp # Import real ViewVC module fp, pathname, description = imp.find_module('viewvc', [LIBRARY_DIR]) try: viewvc = imp.load_module('viewvc', fp, pathname, description) finally: if fp: fp.close() def index(req): server = sapi.ModPythonServer(req) cfg = viewvc.load_config(CONF_PATHNAME, server) try: viewvc.main(server, cfg) finally: server.close() viewvc-1.1.22/bin/mod_python/.htaccess0000644000175000017500000000010310345206676020033 0ustar cmpilatocmpilatoAddHandler python-program .py PythonHandler handler PythonDebug On viewvc-1.1.22/templates/0000755000175000017500000000000012265242270015302 5ustar cmpilatocmpilatoviewvc-1.1.22/templates/diff.ezt0000644000175000017500000002172611271135654016751 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Diff of /[where][end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "diff"] [include "include/file_header.ezt"]

[if-any raw_diff]
[raw_diff]
[end] [define left_view_href][if-any left.prefer_markup][left.view_href][else][if-any left.download_href][left.download_href][end][end][end] [define right_view_href][if-any right.prefer_markup][right.view_href][else][if-any right.download_href][right.download_href][end][end][end] [if-any changes] [for changes] [is changes.type "header"] [else] [is changes.type "add"] [else] [is changes.type "remove"] [else] [is changes.type "change"] [if-any changes.have_right] [else] [end] [if-any changes.have_left] [else] [end] [if-any changes.have_right] [else] [end] [else] [is changes.type "no-changes"] [else] [is changes.type "binary-diff"] [else] [is changes.type "error"] [else] [end] [end] [end] [end] [end] [end] [end] [end]
[is left.path right.path][else][left.path][end] revision [if-any left_view_href][end][left.rev][if-any left_view_href][end][if-any left.author] by [left.author][end], [left.date] [if-any left.tag]
Tag: [left.tag][end]
[is left.path right.path][else][right.path][end] revision [if-any right_view_href][end][right.rev][if-any right_view_href][end][if-any right.author] by [right.author][end], [right.date] [if-any right.tag]
Tag: [right.tag][end]
# Line [changes.line_info_left]  [changes.line_info_extra] Line [changes.line_info_right]  [changes.line_info_extra]
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end]    [changes.right]
 [changes.left]  
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end] [changes.left]  [changes.right] 
 

- No changes -
 
 

- Binary file revisions differ -
 
 

- ViewVC depends on rcsdiff and GNU diff to create this page. ViewVC cannot find GNU diff. Even if you have GNU diff installed, the rcsdiff program must be configured and compiled with the GNU diff location. -
 
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end]  [changes.left]  [changes.right]
[end] [if-any sidebyside] [for sidebyside] [if-any sidebyside.gap] [end] [for sidebyside.columns] [for sidebyside.columns.segments][if-any sidebyside.columns.segments.type][sidebyside.columns.segments.text][else][sidebyside.columns.segments.text][end][end] [end] [end]
[is left.path right.path][else][left.path][end] Revision [left.rev] [is left.path right.path][else][right.path][end] Revision [right.rev]
[sidebyside.columns.line_number]
[end] [if-any unified] [for unified] [if-any unified.gap] [end] [for unified.segments][if-any unified.segments.type][unified.segments.text][else][unified.segments.text][end][end] [end]
r[left.rev] r[right.rev]
[unified.left_number] [unified.right_number]
[end]
[for diff_format_hidden_values][end]
[if-any raw_diff]   [else]
Legend:
Removed from v.[left.rev]  
changed lines
  Added in v.[right.rev]
[end]
[include "include/footer.ezt"] viewvc-1.1.22/templates/directory.ezt0000644000175000017500000001112412231227564020033 0ustar cmpilatocmpilato[include "include/dir_header.ezt"] [is cfg.options.show_logs "1"] [end] [if-any up_href] [is cfg.options.show_logs "1"] [end] [end] [for entries] [is entries.pathtype "dir"] [else] [end] [entries.name][is entries.pathtype "dir"]/[end] [is entries.state "dead"](dead)[end] [if-any entries.graph_href] [end] [if-any entries.errors] [else] [is entries.pathtype "dir"] [else] [define rev_href][if-any entries.prefer_markup][entries.view_href][else][if-any entries.download_href][entries.download_href][end][end][end] [end] [is cfg.options.show_logs "1"] [end] [end] [end]
[if-any sortby_file_href]File[else]File[end] [is sortby "file"] [is sortdir [end] [if-any sortby_rev_href]Rev.[else]Rev.[end] [is sortby "rev"] [is sortdir [end] [if-any sortby_date_href]Age[else]Age[end] [is sortby "date"] [is sortdir [end] [if-any sortby_author_href]Author[else]Author[end] [is sortby "author"] [is sortdir [end] [if-any sortby_log_href]Last log entry[else]Last log entry[end] [is sortby "log"] [is sortdir [end]
 Parent Directory       
View Revision Graph [for entries.errors][entries.errors][end]  [if-any entries.rev][entries.rev][end] [if-any entries.rev][if-any rev_href][end][entries.rev][if-any rev_href][end][end] [if-any entries.lockinfo]locked[end]  [entries.ago]  [entries.author]  [if-any entries.short_log] [if-any entries.log_rev] (from [entries.log_file]/[entries.log_rev]) [entries.short_log] [else][if-any entries.rev] [entries.short_log] [end][end] [end]
[include "include/dir_footer.ezt"] viewvc-1.1.22/templates/query.ezt0000644000175000017500000002011711553572560017203 0ustar cmpilatocmpilato Checkin Database Query [if-any docroot][end] [# setup page definitions] [define help_href][if-any docroot][docroot]/help_query.html[end][end] [# end]

Select your parameters for querying the CVS commit database. You can search for multiple matches by typing a comma-seperated list into the text fields. Regular expressions, and wildcards are also supported. Blank text input fields are treated as wildcards.

Any of the text entry fields can take a comma-seperated list of search arguments. For example, to search for all commits from authors jpaint and gstein, just type: jpaint, gstein in the Author input box. If you are searching for items containing spaces or quotes, you will need to quote your request. For example, the same search above with quotes is: "jpaint", "gstein".

Wildcard and regular expression searches are entered in a similar way to the quoted requests. You must quote any wildcard or regular expression request, and a command charactor preceeds the first quote. The command charactor l is for wildcard searches, and the wildcard charactor is a percent (%). The command charactor for regular expressions is r, and is passed directly to MySQL, so you'll need to refer to the MySQL manual for the exact regex syntax. It is very similar to Perl. A wildard search for all files with a .py extention is: l"%.py" in the File input box. The same search done with a regular expression is: r".*\.py".

All search types can be mixed, as long as they are seperated by commas.

CVS Repository:
CVS Branch:
Directory:
File:
Author:
Sort By:
Date:
In the last hours
In the last day
In the last week
In the last month
Since the beginning of time
[is query "skipped"] [else]

[num_commits] matches found.

[if-any row_limit_reached]

WARNING: These query results have been artificially limited by an administrative threshold value and do not represent the entirety of the data set which matches the query. Consider modifying your query to be more specific, using your version control tool's query capabilities, or asking your administrator to raise the database response size threshold.

[end] [if-any commits] [# uncommment, if you want a separate Description column: (also see below) ] [for commits] [for commits.files] [# uncommment, if you want a separate Description column: {if-index commits.files first} {end} (substitute brackets for the braces) ] [# and also take the following out in the "Description column" case:] [if-index commits.files last] [end] [# ---] [end] [end] [# uncommment, if you want a separate Description column: ]
Revision File Branch +/- Date AuthorDescription
[if-any commits.files.rev][commits.files.rev][else] [end] [commits.files.link] [if-any commits.files.branch][commits.files.branch][else] [end] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [commits.files.plus]/[commits.files.minus] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [if-any commits.files.date][commits.files.date][else] [end] [if-any commits.files.author][commits.files.author][else] [end] {if-any commits.log}{commits.log}{else} {end}
  Log:
[if-any commits.log][commits.log][else] [end]
            
[end] [end] [include "include/footer.ezt"] viewvc-1.1.22/templates/dir_new.ezt0000644000175000017500000001247511147545467017502 0ustar cmpilatocmpilato[include "include/dir_header.ezt"] [if-any sortby_rev_href] [if-any up_href] [end] [for entries] [if-any entries.errors] [else] [define view_icon_link][end] [define graph_icon_link][end] [define download_icon_link][end] [define annotate_icon_link][end] [define log_icon_link][if-any entries.log_href]View Log[end][end] [is entries.pathtype "dir"] [is roottype "cvs"] [# no point in showing icon when there's only one to choose from] [else] [define view_icon_link]View Directory Listing[end] [end] [end] [is entries.pathtype "file"] [define view_icon_link][if-any entries.view_href]View File[end][end] [define graph_icon_link][if-any entries.graph_href]View Revision Graph[end][end] [define download_icon_link][if-any entries.download_href]Download File[end][end] [define annotate_icon_link][if-any entries.annotate_href]Annotate File[end][end] [end] [end] [end]
[if-any sortby_file_href]File[else]File[end] [is sortby "file"] [is sortdir [end] Last Change [is sortby "rev"] [is sortdir [end] [else] [if-any sortby_date_href]Last Change[else]Last Change[end] [is sortby "date"] [is sortdir [end] [end]
 Parent Directory  
[entries.name][is entries.pathtype "dir"]/[end] [if-any entries.lockinfo]locked[end] [is entries.state "dead"](dead)[end] [for entries.errors][entries.errors][end][# Icon column. We might want to add more icons like a tarball # icon for directories or a diff to previous icon for files. # Make sure this sucker has no whitespace in it, or the fixed # widthness of will suffer for large font sizes ][log_icon_link][view_icon_link][graph_icon_link][download_icon_link][annotate_icon_link] [if-any entries.rev] [if-any entries.revision_href][entries.rev][else][entries.rev][end] ([entries.ago] ago) by [entries.author]: [entries.log] [is entries.pathtype "dir"][is roottype "cvs"] (from [entries.log_file]/[entries.log_rev]) [end][end] [end]
[include "include/dir_footer.ezt"] viewvc-1.1.22/templates/revision.ezt0000644000175000017500000000600311405715122017657 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Revision [rev][end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "revision"]
Jump to revision: [for jump_rev_hidden_values][end] [if-any prev_href] Previous[end] [if-any next_href] Next[end]
Author: [if-any author][author][else](unknown author)[end]
Date: [if-any date][date][else](unknown date)[end] [if-any ago]([ago] ago)[end]
Changed paths: [num_changes] [if-any more_changes](showing only [limit_changes]; show all)[end][if-any first_changes](show only first [first_changes])[end]
Log Message:
[log]

Changed paths

[if-any changes] [for changes] [end] [else] [end] [if-any more_changes] [end]
Path Details
[if-any changes.view_href][end]Directory[changes.path][is changes.pathtype "dir"]/[end][if-any changes.view_href][end] [if-any changes.is_copy]
(Copied from [changes.copy_path], r[changes.copy_rev])[end]
[if-any changes.log_href][end][changes.action][if-any changes.log_href][end] [if-any changes.text_mods], [if-any changes.diff_href][end]text changed[if-any changes.diff_href][end][end] [if-any changes.prop_mods], props changed[end]
No changed paths.
[[]...]
[include "include/props.ezt"] [include "include/footer.ezt"] viewvc-1.1.22/templates/log_table.ezt0000644000175000017500000001370411015076476017770 0ustar cmpilatocmpilato[include "include/log_header.ezt"]
[is pathtype "file"] [end] [is roottype "cvs"] [end] [define first_revision][end] [define last_revision][end] [for entries] [if-index entries first][define first_revision][entries.rev][end][end] [if-index entries last][define last_revision][entries.rev][end][end] [# Revision column] [# Tasks column] [is pathtype "file"] [end] [is roottype "cvs"] [end] [# Time column] [# Author column] [end]
Revision TasksDiffsBranches/
Tags
Age Author
[is roottype "svn"][entries.rev][else][entries.rev][end] [if-any entries.view_href] [is pathtype "file"] View
[else] Directory Listing
[end] [end] [if-any entries.download_href]Download
[end] [if-any entries.download_text_href]As text
[end] [if-any entries.annotate_href]Annotate
[end]
[# Diffs column] [is entries.state "dead"] FILE REMOVED [else] [# if you don't want to allow select for diffs then remove this section] [is entries.rev rev_selected] [[]selected]
[else] [[]select for diffs]
[end] [if-any entries.diff_to_sel_href] Diff to selected [rev_selected] [if-any human_readable] [else] (colored) [end]
[end] [if-any entries.prev] Diff to previous [entries.prev] [if-any human_readable] [else] (colored) [end]
[end] [end]
[# Branches column] [if-any entries.vendor_branch] vendor branch
[end] [if-any entries.branches] [for entries.branches] [entries.branches.name]
[end] [end] [if-any entries.branch_points] Branch point for: [for entries.branch_points] [entries.branch_points.name]
[end] [end] [if-any entries.next_main] Diff to next MAIN [entries.next_main] [if-any human_readable] [else] (colored) [end]
[end] [if-any entries.branch_point] Diff to branch point [entries.branch_point] [if-any human_readable] [else] (colored) [end]
[end] [# Tags ] [if-any entries.tags]
[for pathrev_hidden_values][end]
[else]  [end]
[is roottype "svn"] [if-index entries last]Added[else]Modified[end] [end] [if-any entries.ago][entries.ago] ago
[end] [if-any entries.date][entries.date][end] [is roottype "cvs"] [if-any entries.prev] [if-any entries.changed]
Changes since [entries.prev]: [entries.changed] lines [end] [end] [end]
[entries.author]
[if-any entries.lockinfo] Lock status: Locked [entries.lockinfo]
[end] [is roottype "svn"] [if-any entries.orig_path] Original Path: [entries.orig_path]
[end] [if-any entries.size] File length: [entries.size] byte(s)
[end] [if-any entries.copy_path] Copied from: [entries.copy_path] revision [entries.copy_rev]
[end] [end] Log:
[entries.log]
[include "include/log_footer.ezt"] viewvc-1.1.22/templates/docroot/0000755000175000017500000000000012265242270016753 5ustar cmpilatocmpilatoviewvc-1.1.22/templates/docroot/help_rootview.html0000644000175000017500000001701411020035055022517 0ustar cmpilatocmpilato ViewVC Help: General
ViewVC logotype

Help

General
Directory View
Log View
Query Database

ViewVC Help: General

ViewVC is a WWW interface for CVS and Subversion repositories. It allows you to browse the files and directories in a repository while showing you metadata from the repository history: log messages, modification dates, author names, revision numbers, copy history, and so on. It provides several different views of repository data to help you find the information you are looking for:

Sticky Revision and Tag

By default, ViewVC will show the files and directories and revisions that currently exist in the repository. But it's also possible to browse the contents of a repository at a point in its past history by choosing a "sticky tag" (in CVS) or a "sticky revision" (in Subversion) from the forms at the top of directory and log pages. They're called sticky because once they're chosen, they stick around when you navigate to other pages, until you reset them. When they're set, directory and log pages only show revisions preceding the specified point in history. In CVS, when a tag refers to a branch or a revision on a branch, only revisions from the branch history are shown, including branch points and their preceding revisions.

Dead Files

In CVS directory listings, ViewVC can optionally display dead files. Dead files are files which used to be in a directory but are currently deleted, or files which just don't exist in the currently selected sticky tag. Dead files cannot be shown in Subversion repositories. The only way to see a deleted file in a Subversion directory is to navigate to a sticky revision where the file previously existed.

Artificial Tags

In CVS Repositories, ViewVC adds artificial tags HEAD and MAIN to tag listings and accepts them in place of revision numbers and real tag names in all URLs. MAIN acts like a branch tag pointing at the default branch, while HEAD acts like a revision tag pointing to the latest revision on the default branch. The default branch is usually just the trunk, but may be set to other branches inside individual repository files. CVS will always check out revisions from a file's default branch when no other branch is specified on the command line.

More Information

More information about ViewVC is available from viewvc.org. See the links below for guides to CVS and Subversion

Documentation about CVS

Open Source Development with CVS
CVS User's Guide
Another CVS tutorial
Yet another CVS tutorial (a little old, but nice)
An old but very useful FAQ about CVS

Documentation about Subversion

Version Control with Subversion


ViewVC Users Mailinglist
viewvc-1.1.22/templates/docroot/help_dirview.html0000644000175000017500000001251711020035055022315 0ustar cmpilatocmpilato ViewVC Help: Directory View
ViewVC logotype

Help

General
Directory View
Log View
Query Database

ViewVC Help: Directory View

The directory listing view should be a familiar sight to any computer user. It shows the path of the current directory being viewed at the top of the page. Below that is a table summarizing the directory contents, and then comes actual contents, a sortable list of all files and subdirectories inside the current directory.

The summary table is made up of some or all of the following rows:

  • Files Shown - Number of files shown in the directory listing. This might be less than the actual number of files in the directory if a regular expression search is in place, hiding files which don't meet the search criteria. In CVS directory listings, this row will also have a link to toggle display of dead files, if any are present.
  • Directory Revision - For Subversion directories only. Shown as "# of #" where the first number is the most recent repository revision where the directory (or a path underneath it) was modified. The second number is just the latest repository revision. Both numbers are links to revision views
  • Sticky Revision/Tag - shows the current sticky revision or tag and contains form fields to set or clear it.
  • Current Search - If a regular expression search is in place, shows the search string.
  • Query - Provides a link to a query form for the directory

The actual directory list is a table with filenames and directory names in one column and information about the most recent revisions where each file or directory was modified in the other columns. Column headers can be clicked to sort the directory entries in order by a column, and clicked again to reverse the sort order.

File names are links to log views showing a list of revisions where a file was modified. Revision numbers are links to either view or download a file (depending on its file type). The links are reversed for directories. Directory revision numbers are links to log views, while directory names are links showing the contents of those directories. Also, in CVS repositories with the graph view enabled, there will be small graph icons next to file names which are links to revision graphs.

Depending on how ViewVC is configured, there may be more options at the bottom of directory pages:

  • Regular expression search - If enabled, will show a form field accepting a search string (a python regular expression). Once submitted, only files that have at least one occurance of the expression will show up in directory listings.
  • Tarball download - If enabled, will show a link to download a gzipped tar archive of the directory contents.

ViewVC Users Mailinglist
viewvc-1.1.22/templates/docroot/help_query.html0000644000175000017500000000557611020035055022020 0ustar cmpilatocmpilato ViewVC Help: Query The Commit Database
ViewVC logotype

Help:

General
Directory View
Log View
Query Database

ViewVC Help: Query The Commit Database

Select your parameters for querying the CVS commit database in the form at the top of the page. You can search for multiple matches by typing a comma-seperated list into the text fields. Regular expressions, and wildcards are also supported. Blank text input fields are treated as wildcards.

Any of the text entry fields can take a comma-seperated list of search arguments. For example, to search for all commits from authors jpaint and gstein, just type: jpaint, gstein in the Author input box. If you are searching for items containing spaces or quotes, you will need to quote your request. For example, the same search above with quotes is: "jpaint", "gstein".

Wildcard and regular expression searches are entered in a similar way to the quoted requests. You must quote any wildcard or regular expression request, and a command character preceeds the first quote. The command character l(lowercase L) is for wildcard searches, and the wildcard character is a percent (%). The command character for regular expressions is r, and is passed directly to MySQL, so you'll need to refer to the MySQL manual for the exact regex syntax. It is very similar to Perl. A wildard search for all files with a .py extention is: l"%.py" in the File input box. The same search done with a regular expression is: r".*\.py".

All search types can be mixed, as long as they are seperated by commas.


ViewVC Users Mailinglist
viewvc-1.1.22/templates/docroot/images/0000755000175000017500000000000012265242270020220 5ustar cmpilatocmpilatoviewvc-1.1.22/templates/docroot/images/log.png0000644000175000017500000000065410345362401021510 0ustar cmpilatocmpilato‰PNG  IHDRóÿabKGDÿÿÿ ½§“aIDATxÚ¥“OŠ"1Æ/)tåÎuÓ«9‚.½Š.z6½-E‹an 9Cm]¹ª;xqSB*É*³˜®š®l¤|$¼¼ïKÞŸü~7 >ððót:Ekm´Öƺ®;pÎEç\ôÞGï} !ÄB<x Zk¹^¯ˆHJ©‡¾ápH‡5Îý~Oš¦(¥ØívˆHç`µZµûäòç[‹ÅÃ×´Hš¦­ˆˆçy'¸Y×ë5J©{ÆÙ–ËåÃ:뺦ªª»À<Ï[Á&ïÏ‚ƒÁீsŽªª8ŸÏEÁt:e<Ù•~¿ßM¡( f³Æ&“ Y–Ýu@DØl6ݼ÷Ün7.— Ææó9£ÑèËyèõz¨FUk1áp8 ”"Ë2¶Û-J©Zk´Öÿ ÄÖÚ§¦PDH’ü*ËòirY–ï¯ÀÛ“?1~p^ù®ý4þ©W£ÀùIEND®B`‚viewvc-1.1.22/templates/docroot/images/list.png0000644000175000017500000000072310345362401021677 0ustar cmpilatocmpilato‰PNG  IHDRóÿagAMA± üabKGDÿÿÿ ½§“ pHYs  ÒÝ~ütIMEÓ¸ „PIDATxÚ•’1OÂ@†HGg&'€3ô¸96G“þg¢£lÔÍÙ[Ú6u7l!÷ áŽå¨×!|É¥½ïí½ùîé p hk]qbi¥”NÓT‡a¸3)”RŠ8Ž Ã°rØÉ_‡Õjekàø®×ë÷¦÷ ¼7À9Ðtöäº.A4­ †Ëå’~¿Ïb±`>Ÿƒ ø»BEZ¡ƒ´¢`¸ÙlRæËÖàµÕjõ¬ïŸ“$)ŒÇc²,Ë÷eý(ÄCuây~—ÀÐ0p1€œæÿô›• F£Y–Uîfç`6›1Ni·Ûý¥”ħ}iš²^¯ñ}ÿx]×ÝkàyN‡²®•RZJ©…v¤ù ¼x·¦7 Á.â±ÛGQdç`˜$ Ýn€šIbѵS²pW‚Ø;åð6ùÖ&q÷IEND®B`‚viewvc-1.1.22/templates/docroot/images/lock.png0000644000175000017500000000045710754373137021674 0ustar cmpilatocmpilato‰PNG  IHDRóÿasRGB®ÎébKGD™™™^DV pHYs  ÒÝ~ütIMEØ  mjAƯIDAT8˵“ë Ã0„?Ún‘•²3ÝY‰5B4¶RÇN_êIHÖ‰—90úÈÖ=aƒ«$æy>DOÓp9)@JʈȈÈͱÇU\F™¶j˜»³,K×o˜à]Ü^ ­òîþ 21³lçWÿºŸ¡-Zƒ%uƒKâÆÆ´VThÕ(3@îŽ$€î4 =«P‚˰F²DÄ1Á¯Ø·Xg•»ì«îoÇ>¸Dã¸6O­jþ(ÏÑIEND®B`‚viewvc-1.1.22/templates/docroot/images/chalk.jpg0000644000175000017500000000136310124215062021776 0ustar cmpilatocmpilatoÿØÿàJFIFÿÛC    $.' ",#(7),01444'9=82<.342ÿÛC  2!!22222222222222222222222222222222222222222222222222ÿÀ``"ÿÄÿÄ&!"1AQ2RaqBbÿÄÿÄÿÚ ?ö•¢>á^å„]Q5ðŠ T¯«²,œT@O¦‰Ó°O©qžjÓzh‰‚î*¸‰U­~òbÞ¬›9顚Ý0ž£I¥"(®Uþ€”¤Š³™‘9»‹N±Ì€ºTQ)ü¨JilZKw–n› IMËÑ©s†œ ¾Ã½Ò…gœ^¾$¹ßø.eà(’â8[p4œ·– HtªSZI½š¦­È&î6§y’ßu»ÛPŽ7#;±Qò5EöÀ6÷HÑC¹q€ ó#¥»Ë%)«pB†ª°/6àÍÁòj›x„|™u+)Û²ª ê#£–ª;Wø›º®E()o %>Ù);ImÆTE%§8_QðdêÍ[QŽ"Fh+«ÜiœH}7pŸ #²ì°¾ØÐ—ÔrÀ $ñq¥c%«`pï*º“»b`š°Z@+û YfSMH¸RÚ¹S×±›éÒ&µŠ#ݼ°ª¢’|†7lÔyÌ Uøä)ng/4éˆjÕî«&Q«.Æ÷•ÓeM¨á@iËÚ£ªŒE[€,ï/—ù'úÆM2I¼°›QæöFN¼€×QÜSUÜ…AéÿÙviewvc-1.1.22/templates/docroot/images/download.png0000644000175000017500000000044710345362401022536 0ustar cmpilatocmpilato‰PNG  IHDRíÝâR+tEXtCreation Timemer 4 giu 2003 10:11:54 +0100–êÙ¨tIMEÓ /Q| pHYs  ÒÝ~ügAMA± üaPLTEóóó{{{ÆÆÆ„ÿ„ÿ™T*tRNS@æØfNIDATxÚc````Kd€¶B\ ¶´BA+½\EJIIIHIIÉ ÀÂLŠ Ê ¾I”ald(ˆVd(A€£ 00ô šŒÐ²IEND®B`‚viewvc-1.1.22/templates/docroot/images/dir.png0000644000175000017500000000034410124215062021474 0ustar cmpilatocmpilato‰PNG  IHDRíÝâRPLTEÀÀÀÿÜ®ÿÞ­¸ }WK;+%kÅ9ûtRNS@æØfbKGDˆH pHYs  šœtIMEÔ 6Aw¼}HIDATxÚmƱ€0À_e7H&пˆ ½/û`”^u€#=?…~Æfòx"±ÌZ¥¿•¡¿Ñ«ÒÖvbC {T»s4|ÖžwÝ=é?IEND®B`‚viewvc-1.1.22/templates/docroot/images/down.png0000644000175000017500000000024710131652017021672 0ustar cmpilatocmpilato‰PNG  IHDR H%v?PLTEâÇÃtRNS@æØfbKGDˆH pHYs  ÒÝ~ütIMEÔ #èe"IDATxÚc`Àöä0ð70°300AÅ+q—‡Ž™IEND®B`‚viewvc-1.1.22/templates/docroot/images/favicon.ico0000644000175000017500000000257611020031406022334 0ustar cmpilatocmpilatoh( ™Õ»`½•Jq^½”ÿÿÿÿÿøàÌ#˜° ð ààÀÀÀÿóÿçÿÿ?viewvc-1.1.22/templates/docroot/images/viewvc-logo.png0000644000175000017500000002027011020031406023151 0ustar cmpilatocmpilato‰PNG  IHDRðF}¤Ž­sRGB®ÎébKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEØ:;÷ƒAx IDATxÚí}iPWÞ÷éåö]Y.—MÙ‘MÙ— A‘hPg¢ITR)5±R™™ššÑç}3©w¦f>d’ÉÄ!õš§ÔñIâ$&NF[EâAd‘Md¹÷r¹{÷ûáè±íÛÝ4j2Ô[ýÿ`á½§OŸåwþç÷_ιÃ0@Yþ\Yd@Ë"Ë ò'x‡«Á0LžYž®`?‡fÆê4𭣓#͸”¤ÎW¤Vø°qŒþ–Á-ËL4ÒÍ ÃŒYîš:zÇšîÛµJ}ˆÏÜßø`ïX’P`ÑŒþE–™ h¶Ð4í¦]½cÍ7ªû7”¤:Ò”ìƒã8Æybd™¹€Ð4 dÜ:Ðдg¬௠O [aH#‚lyzd™Y€æ(i6 Ýn7MÓÃ{÷™lƒ?uhnÔË!¾qA`†ã¸Ì­e™q€ÒÓîâr;›î¿9\ cÈÉZ£VêpGÚZž'Yf ÙÖ!‚õC@»\.—«Ïxírÿ?]´ SúÄl ö‰&jkÓ²Ì,@ójk¨§].—Óét¹\#–Þs=»ín ÇÈÜð5sƒ$‰ˆµ<[²Ì8@sˆ5ÓN§st¢¿¶w·Ým†mK Y–¶T¡PȘ–eFš k¨ª! ǰùvMÏN7ã„e’‚Š2ÃWP%cZ)òŸÌå€äe…BAQEQþºðôàÕÜîõÁÊëwÎ8è‘se™¹€F˜ÆqaZ©TÎÑÏò}•ù¡ï@×½§Ó)cZ–™è‡íÀqÇI’„°žTêEÞg&€¾Ðýå¨yÀétB@˘–eڀ憈*?-£Öÿ€ªbZI©æ,CåîÉڮϻËå’•´,"Bò¢ÍM»lv Ûy<5sÀ 5¥•’=Ç0ŒÝ9ér9=ë‡Èv¹\N·ÃG9Ë Œžlƒ_LÞ¾q÷lòìEÐ4„ à'3^Åû.Ãhæša§ËQqøÍÁñ®iUj˜úÌÂùk!yàiˆ×Þᛕ ŸÞì»4­úUJUõŸa¾)z< ÷@YhóaG|dXÏPÊ9€’ ¦[Ñ€±ëLÓÃcý.— Zo¼´„¦éʆÏ[û¯L·~»ÃÎWóÝ“È:üitó{ï½G$Byë­·`geZ?C54œ#¢feöŒ´†è£6¿PIºçÓªw;jZþµ4ý …BA’ä¤sÜê0HBé­ À1‚a˜;£­}1ÿ¯Ÿïõ÷ž-¥æs-ž¯ B£Öt_Nœ\L’AÐ-ý£ªÆ)­"IRÎÏh ÈŠ~¡îæþ;c=Ã7âCs¤TT”Z¾c ¡±ûTxPâˆýÖ]s›Ýey¸`d€.bŽ>¥¹ó˜ÔÈç%¢ÙM»Î4} ðññp3®Ö¡³™ê•$IBgߪžáÆ"Ø6·Ûn·CŒéíåPS^óç,T5~!±¢ØŒ°€x§ÛVucG÷ØU6š4ã4·ŸïØ×ÒW‡láüµ«­ï¨Ÿ$IÂËK?é6Ö³Ý?Ò^س8 ÙüGf3ÐÈÒÂq<'¦ŒÀÉ®»×º›$Öaj2›išjF#`˜ø°ÜY~ÑÒPEŸ¹¶àíýð$¢ƒž¸c¼ùøï¦\-0E¢/ˆáO} H^Ë m@Âìçš{OŸnübCñûRêJ_ä>8Þm6›|||<§Û<1X”"U=7w×÷(RÏPzMa~Én·&—Âf?žsMè)ôùâÅ‹µZ­Ãá€Ê~‡(>>^Ü;)ôjüDÄc#Ò)éå§[³DyŒjÅ‘8Pↇ†Ü”$ɜ貖¾ê›½‡b$ô/œÿê×ß¿g2™¼½½9o5™L ÃD§DÍ“8d§¿Ä8ˆ7ã@Ÿß›ì„YÔIc†6}^çZ¨ì&¡Ââ:`þüùqqqV«Õn·»\.4²A(•Jö™1^g____CCCGGÇðð°Éd²Ûí*•ÊÛÛ;(((666%%%00s˜þ+Ò22’››ËËàýýýkkkQwЫ.\ØÓÓÃ.¹|ùò¿üå/œŽ;ŽÔÔTÞŽ+ŠK—.i4š5kÖ444µùw¿û]yy9J~„Õ2 SVVvýúu¡§víÚUPPÀióµk×vïÞ}äÈ‘îînñš3gÎÒ¥KËËË333á«yÝ\¤ÐÖ³…ž‰YÕ6pñZ×™{¤ø%pœ(œ÷òÁóF£Ñ[ç…<~&³™¦éÙ†ØøÐìi©ç¿\­ÊGéöf0bé6x͆^¥R™ŸŸÿÉ'ŸUµÿþôôt’$!RáèWVVÞ¼ySè‘ÒÒRƒÁàv»F£ÐXŽŽr”a˜þþþ-[¶|óÍ7=å------|ðÁ믿þ§?ýI¯×ÃÙòõõõóó»|ù²ç#]]]hƒb¦©©éìÙ³ž+ùøƒZ­†%a /_¾,Ôñüü|ÇNgIIÉÁƒ…Ú¼wïÞ—_~™$I8¤°Ú¾¾¾cÇŽ íuQQQYYYpS…#ÖÙÙ¹eË–C‡IÄCOOÏŽ;vìØQTTôÁ$&&2 ã‰i\hC¬#ÄJ?0ΤHöÜtj½Ëéš°XOŒÉdœ–zîºÞ>pLj¤Y‹ …Wr ˜íÃ(ùŽa˜Í›7³—>GŽ=j·ÛÙ¹M4M‹L`Æ ‡šžRüzð着ªôôô}ûöM7îãr¹víÚ•™™Y__ƒ5.—kÉ’%BåëêêPóà«ÿõ¯y?yò~( uÜ÷HŠ‹‹¡µPVVf0„ŠÕÔÔ qª=xð s[¿~=<—gí›o¾IOO—Žf¶TVVæååíÝ»—Ý€)ÜvlLçÆ¬\¹uÂ8yOÊû„2?éçЧßc6›Ün:À'l~Äs Ý…‘~™>…BA‘*N ç=v 222RdúÛÛÛ¯_¿ŽF¦‹;vL„ddd ( ¸—µ¡ªªjùòå÷îÝ+}}}K—.½zõ*œªÅ‹ •¼páʇràÀÞ’@ЇRSS#Tm~~>|5EQ¯½öšP1§ÓyâÄ  >,T^£Ñ¬Zµ ÿ—_~¹nÝ:Š8¥X­Ö7~ûí·ž˜4dÒáɳõsÝ´ólÓ>‰ïËKX¡¢´‡Ã:9 c2š…ó_Å0IÙªc7zÎc›R ³I=µ¯ÕeBátøÉo¼!RçÑ£G‘Ϙ¦éÚÚÚááau‚j–èW_}Uˆ‘Cªž•••‘‘$Tl|||ݺuccc.—+))I¨$=‚éÍ›7[ZZxKž8qÂb± Åét:ëêêxKÆÄÄ„††"«÷õ×_W(Bíüî»ïØã966&²NV®\©Óéà677¿õÖ[Ð_ô$âv»7oÞ|ûöm¦q!o ;™3'fàbë‹Í(åejJ——°*i³yÂåvûj3bŠ$³ç/ÀÌѧøéB(ŠR(lÇýþ0„9ØÔüüü¤¤$¡:Ož<ÉN2Q'¾¾¾¥¥¥èñ¦"µ÷ûßÿ~dd„· EQo¿ývUUÕ¡C‡vïÞýÙgŸ:ujß¾}¼å;;;?úè#§Ó í<~‡fs³ÍfCoß¿¿P Fã™3g ôÝnw}}=´Î=¥   &Žã!!!eeeBÕVWW[­V´¢ ¾ZÉk×®E6ÜÖ­[­V«ÈR5oÞ¼ÂÂÂÔÔT•J%RÒd2½ÿþûœÜžû€6Ûî‰(é¸L]„Ýi­mÙ/”ùI/)¥ÍfÌ[Cà )Ž˜ï4v›?»¥9i» íhQ¢vnÚ´I¨ÚÆÆÆtw‘#G„J¾ôÒK*• ž6G~nñÀÈÈÈW_}%47n P©Tê’––öé§Ÿ¦¥¥ñ>µsçÎññqš¦-ZÄ[Àf³577£õ)NF> ½œâ|cáÂ…Ä£òæ›o 6›Í555p0ÅÄsÏ= ý›×¯_¯®®ñR¯_¿¾²²òÿøGEEÅž={Μ9³fÍ‘®íß¿ßd2±ÃÆ÷í¦]“v#‡_³G寮œk9hwZ¥àÒK­ÏŠ[ ã)Z•OÎÜeWBõµ¯iÚâèÑL„Õ5Îx$†½jÕ*½^/¤J‘’nhh¸}û6¿ P¼òÊ+ä14Ù˜>~ü¸Ù(..~æ™g(Š‚hÖ<µZíååõöÛoó>e±XŽ9BÓt~~>Eñg>Ö××Cù„—ÏÕÕÕ ÿFyy9z/ASž‹¹d¡·W¨Àùóç…0$.Q$¹hÑ"!@CéÉ7à³?9vìXIII[[d€ž²hÑ"¤›Q †$ÉÒÒÒððp^Oüàà`}}}NNŽ{ùå—ÙÚAhcäææªT*¥R©V«•J%tWCTBB‡~ˆŒ`6Ï„§ªÙáÃû[*‰+‡'zÐ ?Ï I’)…:•)Q)R8ÿ•K>TQ:I¾»ùüÍo)³Šªpœhãᘆsq:\x³gÏ~ñÅyëw8gΜéèèhnnæ-™™™œœŒ&@â5 ØL&𴥿¿‚²¸¸X(Ð0<<ÜÝÝÍÙp÷ä•••‹EˆoPõì³Ï"bÀ>ŠOQTyy¹ë°Ùl'Nœ2¯ËÊÊvÀ0LÄ P(JQ©T*•JÃ'úhµÜ׿Ȳ1ÛG.'ʼn­ü(J•ù ºé+šv?õ)¬m9`wZõêÙsüSØ€0ñZ«0pBD(º¹yóf_Çwß}'ôíÚµkÙêYJŠ3(!šû$b³Ù ! ç-ÓÐÐpôèQŽJMMõ´Ùl®®®>wîo=999^^^ì[רˆòòrF#Ä:êêê`Ä”×¼Öétl -b“¸\.t21öål“Z­VC óú¡÷›òékáMi‡MQ(é‘Å*…׈ :"ž¦ ʼ"JAAöŒã¸Ý59`â‰Ój?ÎéH©ˆ˜2ÕÕÕB€^´hPN¦„¸x¦>q:°GB¾ŽúúzOú´xñâ9sæ$&&r>?räÈÅ‹…vl¾ÁNç"IÒ`0¹nݺµsçN)æ5ÄœŸŸŸPg[[[‘«Í,üÃétÆÅÅEFFÆÄÄÄÅÅÍ} qqqÑÑÑ0©ž‡±xíœ[Cu¡¾Ižv!bÒj•.-¼ä|û7§¿L^$ñt–¹ÔvÔb3z)ý£²šÄ0¬käŠ'ø(CÿüsÞx¾Žc^CZ?wîÜk×®ñ–‡&Ð7ÏÎ#efïÞ½B#ìv».ôEˆÏÜîÑ«ûûž öHA¬dż@‘jÌ{ZhvÓÎê¦}€äY‹)ÅÚôAë =DbJo*ØóiÄŽJKK#""¤7@£Ñ¬^½ùU¦{‡ï‚ D¢ÙÝÝÝì}í•*•ÊétÚív»Ýîp8ì,±Ùl6› ŽI’ÙÙÙ¾¾¾ž•·µµq@–––LQÔ²eË<÷tÞÆÇLJ„„xÞòúH ".N$/ż†µ=ûì³"΢_þò—ýýýÈ· ÿ­­­Ýºu«È‹²³³ñC£øiÂHBÕ|§ÊóÆ-vß¼4¾óÃN_ûòiú‡ö“F˰Zá”ÇVÏÝ#ׯ¬ý<êYF‚0¥)ã)/¾ø¢^¯çeÏâ°†%333…ÖÃ0[¶l¹}û¶§+úûï¿ÏÉÉÉÊÊÊÎÎÎÎÎÎy ÙÙÙ™™™¿øÅ/P{”Jeaa¡”ŽÁÅ#7}Ä»úüóž|ƒ­È་Y<%55™×ˆ¿á8^VV&DÇ!)((xçw:ƒ©åååEEE"6wDDD||<ÛûA²'fŽoÊÍ¡³ñÞ6—â(éì˜å ·OÜlî¸Ûœú„hfútã^@bðB%¥†±n˜äÙÐÇï_ PÅy>ëX¿~ý‡~8999ep_¿~=[ œL)í‡Sõë_ÿúW¿ú•P({ÅŠyyy™™™³fÍ ÔÖÖŠ8e7n„5CïaQQÑ”þfÇKJJH’„Ù/Ë–-ÉN~è‰*,D|ƒCŠà Àñ,((HJJ’R¡§yªÕëõ›6m @—ùöíÛyù¯lÚ´‰£|é@”!ÛM;ëû#µÏ ^ÀÅê« H }PÕðÅ“«çÆ®ê{¦>%©M~ŽM¹ZkF&{<˘"HÏ4Ç×1¥)㛊Šb«éGòzõj‘t?·Û]SS³mÛ¶­[·nݺuÛ¶mâh.--…† Z¢EEESqOKK Bù"™H“““‘†ö$Zl!žþÅ&µK–,a›×lðüö·¿õ4XO²²²V®\É6"rhøŸ`ïX-å×5z¹g´‰mÌaÒ0Žcx[ÿå¾{­O¤ž©ËÜÀçÔJ-ZÓ“ã½üù þÔ\ŠT‹ÜìÏ6e¤ æªsFZ€†$gûöíYYYO>O™™™ï¿ÿ>;|€ã¸¿¿¿P2’ââb¨k! ãââ’““§äÈ Á;˜l,þìg?Ê,7¯Ù~@N·gÏžèèè'¥èèè¿þõ¯šþÐm‡.!°p©çk³uŒsU›¤¼C₟œn|"& ,’¸2iV!RÏ Cßþ)ç"„‡~mg<5´tS&***??™_ÃÝœõ˜¯¯ïW_}õꫯ>ö01íïÿ»F£Á¨¤ÅŸEz%c,_¾\: …Îê¢I×jµSZ&ȼæàŒ­ ÃÃÿýöÛ’’’džMaaágŸ}ãäìàÑÛ bóT¤×¤ÓXÓù?v‡Íóú äTÊ] ÖÔ]34ÞóØ-» 60O«òA:²±ÿø#ÿ!?E”*xÊØ‡tSfýúõð¢u¶^ºEˆü¦ÐóööþóŸÿ¼oß>Îá¹)E©T–””|ýõ×ï¼óŽV«å4 BÁÓqÁ–ôôtè&C%IrÅŠâà[°`jðì)Ì,I’”••ùùùÁÆ£;¥ØHñwìØ±{÷îéîi)))üq`` ô±Í5 qWʤ Å?ôšè¸pûëÑë %EÓƒÕ,¿¨èÀŒŽ¡+gšö®Éÿ_æ®Á¦®Á&#çÍ*B½5\×ÐÏûÀ¡[À&ñ‚†mÊ<ÿüó}ôÌp÷tÝà8¾jÕ*4úžN@ ÃŠŠŠt:ºÆ€}ê;>>ž7‘———‘‘ÑÓÓsêÔ©K—.µ¶¶Bo§ƒ!444!!!333//ÏÛÛûþUÂtТ….á„„„mÛ¶±³õÙíLNNFk韸¸ŠŠ ³Ùìyúð   ­V;¥ë}n:44tçÎ}}}¼ã ]Úh5r‚Slzÿ[TTTPPÐÑÑQYYyåÊ•[·n p‘$IÇÆÆ¦¥¥DFFÂ%bãœ<ŒíÞw»Ý‡Ãf³žhýë˜í Î??7r5E)ÙÏÐ4ír¹l6[Çk{ëÞ%pÅÿ~éK_]Ðt½ûäÝè½ëŸW0÷5š¿3Ñ|îö†á?¢LK0”ÀÄBµZ myÞi@?G„|ºžW<²ãÐ1Ì^èàÁ‡Ãk°Ûí@COT°ð.%‡Ã:Eçv»GGG-–û7«Õj…B˜\~pª ØóÄššGZÂæÙp% h0†a°1"À×±» „itÍ1ô—{VÈn{<9ׂ×ÂQBÊž¤A¾)­VëëëËæ]pÊИxj"Ò3¨PPé³~ºãÿ2€n»Wã¤íÏF­UR*O‹uN@B˜_Rïèõêæ}+r9-4ßi¿Ù{Ãðä"Øóα‹?ôïB3…ë"½òaO8á{¡-’a¸?â8Ϋ¢Ð¤òVÈÑ($I²žå8¼ÐWpÎà$Ñ4­ÑhQhÐs ¡â9Jž×`œ ôáv`·Û­Vë÷]»†&oÁ¯T¤W^ĺˆ€Ôy¸£Y­Ö^xoÂ:ÚDÇÀÈí[ýW&íf’ª©Cƒ ÀêšÓ'¸¦žÉ‹4ã)ïOÍMò]cÆ0oPºsr›ƒHöâ—ßqj˜òYÎ¥X¼È9Öç”-Þ²åÙÞÇÅá]vߌñ|¼ššy6 hVMÜ­ëûÔâ|äŒ>Žá~©Q†ìYÞñ ؤ^¤• ÃíwzMW,Ízê, ½"*Ñg¹F¥C9ÝòOÊÊ2m@#âÉ´Ýn7ZF.öntðä¤R„&Hã¯Ò«g{QJ’{ÚÊé¶™ìC&Ûà¥}ÈÒnu%¶,Jšë½D¥RÃ$5Oº,²H4¢êÐMh³Ù¬6KãÝÃ}–«âÕ‘8EWàá¢íN·Íá¶N»M€ˆPç‡é2ÑÁv„sZ8Yd@?‚ièFEîôîñú£Ç´íÇk÷‹Ó-Õ«g#Ÿ?äÍ‘9$‹ h~=Ͷü&¬c-#'&¯=õ¦à˜"T•¦ÉRR*v‹Í›e4Ëòø€|±JëÑÉÞöñïïÙÛ`ž ”ƒ¨äÙê -¥‡Fù ì°ŒfYžÐlLCJ ÁÁt…që@¯ù‡!ûM§—…ÁÐR‰Aêyj…ŠÑ#(sÎ;ȳ%ËS4xà}C®È@PZ‰Óåš¼5jësôLº§¾„ìM†ú+c¼Áìßiegœˆ„Oe‘å‰í©ª!¬ù¡í.‹Å92á±¹Ç\´ÝÍ8ià$EàJ£T„Ÿ†ÐkIIP((MØYឩɲÈò”ÍVÕÐ} ð_vlæ‚qÂýœü2„f”h††çù\YdùQÍVÕHa#³oT`'Ä ¤0”\ÂÉœb'OÉP–å§4ÖHa#6›9ÅÉ®d#›“ì&CY–ÿ  Ù$„s«•Ð/Úr~vE¹S,IDATSz–™,²üD€æ ÿÀ-Ó@ÚÏÜÊ"Ë ÐBøÁ´,²g˜·“áÅÓG( `âOÞÅë7EÎD7M¨˜ ûB‰‘i4IEND®B`‚viewvc-1.1.22/templates/docroot/images/up.png0000644000175000017500000000025010131652017021341 0ustar cmpilatocmpilato‰PNG  IHDR H%v?PLTEâÇÃtRNS@æØfbKGDˆH pHYs  ÒÝ~ütIMEÔ2›Î‘IDATxÚc`ÀL ì ü òì@Ä—Gb2ÞIEND®B`‚viewvc-1.1.22/templates/docroot/images/feed-icon-16x16.jpg0000644000175000017500000000175410413061470023337 0ustar cmpilatocmpilatoÿØÿàJFIFddÿìDuckyPÿîAdobedÀÿÛ„      ÿÀÿÄ¢  s!1AQa"q2‘¡±B#ÁRÑá3bð$r‚ñ%C4S’¢²csÂ5D'“£³6TdtÃÒâ&ƒ „”EF¤´VÓU(òãóÄÔäôeu…•¥µÅÕåõfv†–¦¶ÆÖæö7GWgw‡—§·Ç×ç÷8HXhxˆ˜¨¸ÈØèø)9IYiy‰™©¹ÉÙéù*:JZjzŠšªºÊÚêúm!1AQa"q‘2¡±ðÁÑá#BRbrñ3$4C‚’S%¢c²ÂsÒ5âDƒT“ &6E'dtU7ò£³Ã()Óã󄔤´ÄÔäôeu…•¥µÅÕåõFVfv†–¦¶ÆÖæöGWgw‡—§·Ç×ç÷8HXhxˆ˜¨¸ÈØèø9IYiy‰™©¹ÉÙéù*:JZjzŠšªºÊÚêúÿÚ ?÷‘<“£ùÃ˺‡šüí£ù’úîòþv½ó¾Ÿ$71ÅÅQ‰žÔò“ŠV¬Áztéž+£ÒbÖéç¨ÕC)õË /ªUdË“í¥Út:ˆiô³Äˆ¬R$óúgôÙè çÍ'ó¯—.?+îtûÛÈî ½O­ù_Ìö`*Ê«J:^.µZíîhµÝ™©ì½D2C'«Hòýu¡Øöv»kbœ' ôÎæé¡N´8èß–:3¥gùƒa©D.­ùñ¶6‰ÃÖ·ž.`@aBµ©Ïìíd{.2¸äŽª97ßÐaµÆB÷½úu[Ùù;[ õBZi@×ó¸÷©DÖÝ:ô"˜Çž0t=èú·|¥¥jw—·~m‹Sò¨·Uô º†—vÆŽx/ªÎÊ£ný³o©Õéõxg¦Á¾^,b¹ Tyì8¬€6Ogvf}Xjõˆ †S|ÌO¢\·<4 æÿÿÙviewvc-1.1.22/templates/docroot/images/cvsgraph_16x16.png0000644000175000017500000000024210211147720023377 0ustar cmpilatocmpilato‰PNG  IHDRbò PLTEÿÿÿ¨Üÿ »KttRNS@æØfgIFgNnz4IDATxÚc`ûÏÿLëWÿa0-^ å23ðYì?0Xm°Ú"6@¸Ì@—Ø€&Ý]ÙO#ŒIEND®B`‚viewvc-1.1.22/templates/docroot/images/annotate.png0000644000175000017500000000137410345362401022540 0ustar cmpilatocmpilato‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs  ÒÝ~ütIMEÓ "(1,tEXtCommentCreated with The GIMPïd%n`IDATxÚ­“KHTaÇß½3Ž3Ž:Íèä# \Qm£¥-JÛE ÑÀ É6‰Ñ2hÔ¢|Ð*1‹D› %ŠjŒHKzR˜Iæ£1;×;{ï×"M‰ýá,Îá|þçüÏG1ÐÈÅh^¬ý3šoôuˉ±a{núƒÝ×sI×W6ˆ•IEE¹/­QUgµ¢(žXl¾iäåùyþå_°rÕ;ÇJÃH¶ÇãñæÚÚZgvv6]]]ôôÞâ@IÛ²(ÝyDee255Q¤¡!£,ÝB‘6W¯ÝÄmkdÊøº3®"z¤gŸr›=z'Ž¸× ˜ï'™H` Z[¶¯K0¯À—å&×íÂ寞ë<ù¤pªí8À Å¥n[³Ä­ã\MnæëÉ v—gp¸?ņ]B¾0z±Â´6gÖcEÎGT@,)ápEÝ|Ú5K%ù6oñmÎb&f*†‘AáŽJæ­z@Wÿt¡*¦i‡NxzGžñêÝc¤âdÔ»“M!7ÃwîSµyGžJ"-1S&«òƒ‘ƒ ­9øKñ„’\9}†´ ýƒoøøuŠÂ¢jutQÖRb›IKÚ†´ç2Ïï÷–†·ðe2Š)ä7"…ŠÛ;JŽ7‹TÚDJ7C'‡–FV…Ǥ”¿]°Q•…„¦é¤lÁ\LççœRR_·—L—¡¨rqéRÊ Ë LËffæ q Cqwà)Bqòþó8ß’N&°¬´µÞ!-ÙØ˜“Wtqí÷ ‘)µè÷6à,ÿ¿»öuvæsIEND®B`‚viewvc-1.1.22/templates/docroot/images/back_small.png0000644000175000017500000000031510210637247023015 0ustar cmpilatocmpilato‰PNG  IHDRíÝâRPLTEÿÿÿŒŒŒZZZ„„„111!!!8ÐtRNS@æØfgIFgNnzVIDATxÚc`@€4(Ífœa$›i& CÌ66dÀd¸08€FŒ.J ¬ÆÆL¡¡@õLÆÆ®¡¡ EÆÆAF²qRh T[(ØH "'ˆI‚‚@Í&] èP4Œ~IEND®B`‚viewvc-1.1.22/templates/docroot/images/cvsgraph_32x32.png0000644000175000017500000000033310211147720023374 0ustar cmpilatocmpilato‰PNG  IHDR TgÇPLTEÿÿÿÿÿ¨Üÿ jG>tRNS@æØfgIFgNnzjIDATxÚc`À...¨NJ*¤ `˜Áà€Å fqqq€(vqaAAaAAf€mq@²ªá$Fgc„",€aa[@ábaL[°…Ö`D!I3²Àº(=uì´,IEND®B`‚viewvc-1.1.22/templates/docroot/images/broken.png0000644000175000017500000000036710324021771022207 0ustar cmpilatocmpilato‰PNG  IHDRíÝâRPLTEkkkŒŒŒÎÎÎçççÿÿÿ999F‹…/tRNS@æØfbKGDˆH pHYs  šœtIMEÕ  ÷w [IDAT×c`€P P1LCCƒ`Œ(Ã8ÂMKƒ2Ba G #Ð54$Ä`Ht© `L`2TY@ŠXX@ŒPW„v#HÄ'K!©¹°„IEND®B`‚viewvc-1.1.22/templates/docroot/help_log.html0000644000175000017500000000554211020035055021425 0ustar cmpilatocmpilato ViewVC Help: Log View
ViewVC logotype

Help

General
Directory View
Log View
Query Database

ViewVC Help: Log View

The log view displays the revision history of the selected source file or directory. For each revision the following information is displayed:

  • The revision number. In Subversion repositories, this is a link to the revision view
  • For files, links to view, download, and annotate the revision. For directories, a link to list directory contents
  • A link to select the revision for diffs (see below)
  • The date and age of the change
  • The author of the modification
  • The CVS branch (usually MAIN, if not on a branch)
  • Possibly a list of CVS tags bound to the revision (if any)
  • The size of the change measured in added and removed lines of code. (CVS only)
  • The size of the file in bytes at the time of the revision (Subversion only)
  • Links to view diffs to the previous revision or possibly to an arbitrary selected revision (if any, see above)
  • If the revision is the result of a copy, the path and revision copied from
  • If the revision precedes a copy or rename, the path at the time of the revision
  • And last but not least, the commit log message which should tell about the reason for the change.

At the bottom of the page you will find a form which allows to request diffs between arbitrary revisions.


ViewVC Users Mailinglist
viewvc-1.1.22/templates/docroot/help.css0000644000175000017500000000041310351044240020402 0ustar cmpilatocmpilato/************************************/ /*** ViewVC Help CSS Stylesheet ***/ /************************************/ /*** Standard Tags ***/ body { margin: 0.5em; } img { border: none; } table { width: 100%; } td { vertical-align: top; } col.menu { width:12em; } viewvc-1.1.22/templates/docroot/styles.css0000644000175000017500000001677411553572047021036 0ustar cmpilatocmpilato/*******************************/ /*** ViewVC CSS Stylesheet ***/ /*******************************/ /*** Standard Tags ***/ html, body { color: #000000; background-color: #ffffff; font-family: sans-serif; } a:link { color: #0000ff; } a:visited { color: #880088; } a:active { color: #0000ff; } img { border: none; } table { width: 100%; margin: 0; border: none; } table.auto { width: auto; } table.fixed { width: 100%; table-layout: fixed; } table.fixed td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } tr, td, th { vertical-align: top; } th { white-space: nowrap; } form { margin: 0; } /*** Icons ***/ .vc_icon { width: 16px; height: 16px; border: none; padding: 0 1px; } /*** Navigation Headers ***/ .vc_navheader { background-color: #cccccc; padding: .25em; } .vc_navheader .pathdiv { padding: 0 3px; } /*** Table Headers ***/ .vc_header { text-align: left; vertical-align: top; background-color: #cccccc; } .vc_header_sort { text-align: left; background-color: #88ff88; } /*** Table Rows ***/ .vc_row_even { background-color: #ffffff; } .vc_row_odd { background-color: #f0f0f0; } .vc_row_special { background-color: #ffff7f; } /*** Log messages ***/ .vc_log { /* unfortunately, white-space: pre-wrap isn't widely supported ... */ white-space: -moz-pre-wrap; /* Mozilla based browsers */ white-space: -pre-wrap; /* Opera 4 - 6 */ white-space: -o-pre-wrap; /* Opera >= 7 */ white-space: pre-wrap; /* CSS3 */ word-wrap: break-word; /* IE 5.5+ */ } /*** Properties Listing ***/ .vc_properties { margin: 1em 0; } /*** File Content Markup Styles ***/ .vc_summary { background-color: #eeeeee; } #vc_file td { border-right-style: solid; border-right-color: #505050; text-decoration: none; font-weight: normal; font-style: normal; padding: 1px 5px; } .vc_file_line_number { border-right-width: 1px; background-color: #eeeeee; color: #505050; text-align: right; } .vc_file_line_author, .vc_file_line_rev { border-right-width: 1px; text-align: right; } .vc_file_line_text { border-right-width: 0px; background-color: white; font-family: monospace; text-align: left; white-space: pre; width: 100%; } .pygments-c { color: #408080; font-style: italic } /* Comment */ .pygments-err { border: 1px solid #FF0000 } /* Error */ .pygments-k { color: #008000; font-weight: bold } /* Keyword */ .pygments-o { color: #666666 } /* Operator */ .pygments-cm { color: #408080; font-style: italic } /* Comment.Multiline */ .pygments-cp { color: #BC7A00 } /* Comment.Preproc */ .pygments-c1 { color: #408080; font-style: italic } /* Comment.Single */ .pygments-cs { color: #408080; font-style: italic } /* Comment.Special */ .pygments-gd { color: #A00000 } /* Generic.Deleted */ .pygments-ge { font-style: italic } /* Generic.Emph */ .pygments-gr { color: #FF0000 } /* Generic.Error */ .pygments-gh { color: #000080; font-weight: bold } /* Generic.Heading */ .pygments-gi { color: #00A000 } /* Generic.Inserted */ .pygments-go { color: #808080 } /* Generic.Output */ .pygments-gp { color: #000080; font-weight: bold } /* Generic.Prompt */ .pygments-gs { font-weight: bold } /* Generic.Strong */ .pygments-gu { color: #800080; font-weight: bold } /* Generic.Subheading */ .pygments-gt { color: #0040D0 } /* Generic.Traceback */ .pygments-kc { color: #008000; font-weight: bold } /* Keyword.Constant */ .pygments-kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ .pygments-kp { color: #008000 } /* Keyword.Pseudo */ .pygments-kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ .pygments-kt { color: #B00040 } /* Keyword.Type */ .pygments-m { color: #666666 } /* Literal.Number */ .pygments-s { color: #BA2121 } /* Literal.String */ .pygments-na { color: #7D9029 } /* Name.Attribute */ .pygments-nb { color: #008000 } /* Name.Builtin */ .pygments-nc { color: #0000FF; font-weight: bold } /* Name.Class */ .pygments-no { color: #880000 } /* Name.Constant */ .pygments-nd { color: #AA22FF } /* Name.Decorator */ .pygments-ni { color: #999999; font-weight: bold } /* Name.Entity */ .pygments-ne { color: #D2413A; font-weight: bold } /* Name.Exception */ .pygments-nf { color: #0000FF } /* Name.Function */ .pygments-nl { color: #A0A000 } /* Name.Label */ .pygments-nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ .pygments-nt { color: #008000; font-weight: bold } /* Name.Tag */ .pygments-nv { color: #19177C } /* Name.Variable */ .pygments-ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ .pygments-w { color: #bbbbbb } /* Text.Whitespace */ .pygments-mf { color: #666666 } /* Literal.Number.Float */ .pygments-mh { color: #666666 } /* Literal.Number.Hex */ .pygments-mi { color: #666666 } /* Literal.Number.Integer */ .pygments-mo { color: #666666 } /* Literal.Number.Oct */ .pygments-sb { color: #BA2121 } /* Literal.String.Backtick */ .pygments-sc { color: #BA2121 } /* Literal.String.Char */ .pygments-sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ .pygments-s2 { color: #BA2121 } /* Literal.String.Double */ .pygments-se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ .pygments-sh { color: #BA2121 } /* Literal.String.Heredoc */ .pygments-si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ .pygments-sx { color: #008000 } /* Literal.String.Other */ .pygments-sr { color: #BB6688 } /* Literal.String.Regex */ .pygments-s1 { color: #BA2121 } /* Literal.String.Single */ .pygments-ss { color: #19177C } /* Literal.String.Symbol */ .pygments-bp { color: #008000 } /* Name.Builtin.Pseudo */ .pygments-vc { color: #19177C } /* Name.Variable.Class */ .pygments-vg { color: #19177C } /* Name.Variable.Global */ .pygments-vi { color: #19177C } /* Name.Variable.Instance */ .pygments-il { color: #666666 } /* Literal.Number.Integer.Long */ /*** Diff Styles ***/ .vc_diff_header { background-color: #ffffff; } .vc_diff_chunk_header { background-color: #99cccc; } .vc_diff_chunk_extra { font-size: smaller; } .vc_diff_empty { background-color: #cccccc; font-family: sans-serif; font-size: smaller; } .vc_diff_add { background-color: #aaffaa; font-family: sans-serif; font-size: smaller; } .vc_diff_remove { background-color: #ffaaaa; font-family: sans-serif; font-size: smaller; } .vc_diff_change { background-color: #ffff77; font-family: sans-serif; font-size: smaller; } .vc_diff_change_empty { background-color: #eeee77; font-family: sans-serif; font-size: smaller; } .vc_diff_nochange { font-family: sans-serif; font-size: smaller; } .vc_diff_line_number { } .vc_raw_diff { background-color: #cccccc; font-size: smaller; } /*** Intraline Diff Styles ***/ .vc_idiff_add { background-color: #aaffaa; } .vc_idiff_change { background-color:#ffff77; } .vc_idiff_remove { background-color:#ffaaaa; } .vc_idiff_empty { background-color:#e0e0e0; } table.vc_idiff col.content { width: 50%; } table.vc_idiff tbody { font-family: monospace; /* unfortunately, white-space: pre-wrap isn't widely supported ... */ white-space: -moz-pre-wrap; /* Mozilla based browsers */ white-space: -pre-wrap; /* Opera 4 - 6 */ white-space: -o-pre-wrap; /* Opera >= 7 */ white-space: pre-wrap; /* CSS3 */ word-wrap: break-word; /* IE 5.5+ */ } table.vc_idiff tbody th { background-color:#e0e0e0; text-align:right; } /*** Query Form ***/ .vc_query_form { background-color: #e6e6e6; } /*** Warning! ***/ .vc_warning { border-width: 1px 2px 2px 2px; border-color: black; border-style: solid; background-color: red; color: white; padding: 0.5em; } viewvc-1.1.22/templates/file.ezt0000644000175000017500000001003212242412223016731 0ustar cmpilatocmpilato[# ------------------------------------------------------------------------- ] [# CUSTOMIZE ME: To avoid displaying "binary garbage" -- the contents of ] [# files with non-human-readable file formats -- change the value of the ] [# hide_binary_garbage variable below to 1. ] [# ------------------------------------------------------------------------- ] [define hide_binary_garbage]0[end] [# ------------------------------------------------------------------------- ] [# setup page definitions] [is annotation "annotated"] [define page_title]Annotation of /[where][end] [else] [define page_title]Contents of /[where][end] [end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "markup"] [include "include/file_header.ezt"]
Revision [if-any revision_href][rev][else][rev][end] - ([is annotation "annotated"]hide annotations[else]show annotations[end]) [if-any download_href](download)[end] [if-any download_text_href](as text)[end] [if-any vendor_branch] (vendor branch)[end]
[if-any date][date][else](unknown date)[end] [if-any ago]([ago] ago)[end] by [if-any author][author][else](unknown author)[end] [if-any orig_path]
Original Path: [orig_path] [end] [if-any branches]
Branch: [branches] [end] [if-any tags]
CVS Tags: [tags] [end] [if-any branch_points]
Branch point for: [branch_points] [end] [is roottype "cvs"] [if-any changed]
Changes since [prev]: [changed] lines [end] [end] [if-any mime_type]
File MIME type: [mime_type] [end] [is roottype "svn"][if-any size]
File size: [size] byte(s) [end][end] [if-any lockinfo]
Lock status: Locked [lockinfo] [end] [is annotation "binary"]
Unable to calculate annotation data on binary file contents. [end] [is annotation "error"]
Error occurred while calculating annotation data. [end] [is state "dead"]
FILE REMOVED [end] [if-any log]
[log]
[end]
[if-any prefer_markup][define hide_binary_garbage]0[end][end] [if-any image_src_href][define hide_binary_garbage]0[end][end] [is hide_binary_garbage "1"]

This file's contents are not viewable. [if-any download_href]Please download this version of the file in order to view it.[end]

[else] [define last_rev]0[end] [define rowclass]vc_row_even[end] [if-any lines]
[for lines] [is lines.rev last_rev] [else] [is lines.rev rev] [define rowclass]vc_row_special[end] [else] [is rowclass "vc_row_even"] [define rowclass]vc_row_odd[end] [else] [define rowclass]vc_row_even[end] [end] [end] [end] [is annotation "annotated"] [end] [define last_rev][lines.rev][end] [end]
[lines.line_number][is lines.rev last_rev] [else][lines.author][end] [is lines.rev last_rev] [else][if-any lines.diff_href][end][lines.rev][if-any lines.diff_href][end][end][lines.text]
[else] [if-any image_src_href]
[end] [end] [end] [include "include/props.ezt"] [include "include/footer.ezt"] viewvc-1.1.22/templates/error.ezt0000644000175000017500000000357610575264622017201 0ustar cmpilatocmpilato ViewVC Exception

An Exception Has Occurred

[if-any msg]

[msg]

[end] [if-any status]

HTTP Response Status

[status]


[end] [if-any msg][else]

Python Traceback

[stacktrace]

[end] [# Here follows a bunch of space characters, present to ensure that our error message is larger than 512 bytes so that IE's "Friendly Error Message" won't show. For more information, see http://oreillynet.com/onjava/blog/2002/09/internet_explorer_subverts_err.html] viewvc-1.1.22/templates/include/0000755000175000017500000000000012265242270016725 5ustar cmpilatocmpilatoviewvc-1.1.22/templates/include/file_header.ezt0000644000175000017500000000136111162205616021677 0ustar cmpilatocmpilato

[is pathtype "file"] Parent Directory Parent Directory [if-any log_href] | Revision Log Revision Log [end] [if-any graph_href] | View Revision Graph Revision Graph [end] [is view "diff"] | View Patch Patch [end] [else] View Directory Listing Directory Listing [end]

viewvc-1.1.22/templates/include/log_header.ezt0000644000175000017500000000251010763255712021546 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Log of /[where][end] [define help_href][docroot]/help_log.html[end] [# end] [include "header.ezt" "log"] [include "file_header.ezt"]
[if-any default_branch] [end] [is pathtype "file"] [if-any head_view_href] [end] [if-any tag_view_href] [end] [end]
Default branch: [for default_branch][default_branch.name][if-index default_branch last][else], [end] [end]
Links to HEAD: (view) [if-any head_download_href](download)[end] [if-any head_download_text_href](as text)[end] [if-any head_annotate_href](annotate)[end]
Links to [pathrev]: (view) [if-any tag_download_href](download)[end] [if-any tag_download_text_href](as text)[end] [if-any tag_annotate_href](annotate)[end]
Sticky [is roottype "cvs"]Tag[else]Revision[end]: [include "pathrev_form.ezt"]
[include "paging.ezt"] viewvc-1.1.22/templates/include/pathrev_form.ezt0000644000175000017500000000316611015076476022160 0ustar cmpilatocmpilato
[for pathrev_hidden_values][end] [is roottype "cvs"] [define pathrev_selected][pathrev][end] [else] [end]
[if-any pathrev]
[for pathrev_clear_hidden_values][end] [if-any lastrev] [is pathrev lastrev][else][end] (Current path doesn't exist after revision [lastrev]) [else] [end]
[end] viewvc-1.1.22/templates/include/footer.ezt0000644000175000017500000000122211442176750020751 0ustar cmpilatocmpilato[# standard footer used by all ViewVC pages ]
[if-any cfg.general.address]
[cfg.general.address]
[else] [end]
[if-any help_href]ViewVC Help[else] [end]
Powered by ViewVC [vsn] [if-any rss_href]RSS 2.0 feed[else] [end]
viewvc-1.1.22/templates/include/dir_footer.ezt0000644000175000017500000000034111020066731021575 0ustar cmpilatocmpilato[# if you want to disable tarball generation remove the following: ] [if-any tarball_href]

Download GNU tarball

[end] [include "props.ezt"] [include "footer.ezt"] viewvc-1.1.22/templates/include/props.ezt0000644000175000017500000000112210777221651020616 0ustar cmpilatocmpilato[if-any properties]

Properties

[for properties] [if-any properties.undisplayable] [else] [end] [end]
Name Value
[properties.name]Property value is undisplayable.[properties.value]
[end] viewvc-1.1.22/templates/include/dir_header.ezt0000644000175000017500000000502211200070326021523 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Index of /[where][end] [define help_href][docroot]/help_[if-any where]dir[else]root[end]view.html[end] [# end] [include "header.ezt" "directory"] [if-any where][else] [end] [is roottype "svn"] [end] [if-any search_re_action] [end] [if-any queryform_href] [end]
Files shown:[files_shown] [is num_dead "0"] [else] [if-any attic_showing] (Hide [num_dead] dead files) [else] (Show [num_dead] dead files) [end] [end]
Directory revision: [tree_rev][if-any youngest_rev] (of [youngest_rev])[end]
Sticky [is roottype "cvs"]Tag[else]Revision[end]: [include "pathrev_form.ezt"]
Filter files by content:
[for search_re_hidden_values][end]
[if-any search_re]
[for search_re_hidden_values][end]
[end]
Query: Query revision history
[is picklist_len "0"] [else] [is picklist_len "1"] [else]
[for dir_paging_hidden_values][end]
[end] [end]


viewvc-1.1.22/templates/include/paging.ezt0000644000175000017500000000152011200060036020676 0ustar cmpilatocmpilato [is picklist_len "0"] [else] [is picklist_len "1"] [else]
[for log_paging_hidden_values][end]
[end] [end]viewvc-1.1.22/templates/include/log_footer.ezt0000644000175000017500000000017610332410426021605 0ustar cmpilatocmpilato[include "paging.ezt"] [is pathtype "file"] [include "diff_form.ezt"] [end] [include "sort.ezt"] [include "footer.ezt"] viewvc-1.1.22/templates/include/header.ezt0000644000175000017500000000262611363646436020721 0ustar cmpilatocmpilato [if-any rootname][[][rootname]][else]ViewVC[end] [page_title] [if-any rss_href][end]
[if-any roots_href]/[else]/[end][if-any nav_path][for nav_path][if-any nav_path.href][end][if-index nav_path first][[][nav_path.name]][else][nav_path.name][end][if-any nav_path.href][end][if-index nav_path last][else]/[end][end][end] [if-any username]Logged in as: [username][end]
ViewVC logotype

[page_title]

viewvc-1.1.22/templates/include/diff_form.ezt0000644000175000017500000000454211015076476021416 0ustar cmpilatocmpilato

This form allows you to request diffs between any two revisions of this file. For each of the two "sides" of the diff, [if-any tags] select a symbolic revision name using the selection box, or choose 'Use Text Field' and enter a numeric revision. [else] enter a numeric revision. [end]

  [for diff_select_hidden_values][end] Diffs between [if-any tags] [else] [end] and [if-any tags] [else] [end]
  Type of Diff should be a
viewvc-1.1.22/templates/include/sort.ezt0000644000175000017500000000125211015076476020445 0ustar cmpilatocmpilato[is roottype "svn"] [else]

[for logsort_hidden_values][end] Sort log by:
[end]viewvc-1.1.22/templates/rss.ezt0000644000175000017500000000143111375304632016636 0ustar cmpilatocmpilato [rss_link_href] [rootname] checkins[if-any where] (in [where])[end] [is roottype "svn"]Subversion[else]CVS[end] commits to the[if-any where] [where] directory of the[end] [rootname] repository [for commits] [if-any commits.rev][commits.rev]: [end][[commits.author]] [format "xml"][commits.short_log][end] [if-any commits.rss_url][commits.rss_url][end] [commits.author] [if-any commits.rss_date][commits.rss_date][else](unknown date)[end] <pre>[format "xml"][format "html"][commits.log][end][end]</pre> [end] viewvc-1.1.22/templates/query_results.ezt0000644000175000017500000000707711553572560020776 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Query results on /[where][end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt"]

[english_query]

[# ] [if-any row_limit_reached]

WARNING: These query results have been artificially limited by an administrative threshold value and do not represent the entirety of the data set which matches the query. Consider modifying your query to be more specific, using your version control tool's query capabilities, or asking your administrator to raise the database response size threshold.

[end]

Modify query

Show commands which could be used to back out these changes

+[plus_count]/-[minus_count] lines changed.

[if-any commits] [if-any show_branch] [end] [# uncommment, if you want a separate Description column: (also see below) ] [for commits] [for commits.files] [if-any show_branch] [end] [end] [if-any commits.limited_files] [end] [end]
Revision FileBranch+/- Date AuthorDescription
[define rev_href][if-any commits.files.prefer_markup][commits.files.view_href][else][if-any commits.files.download_href][commits.files.download_href][end][end][end] [if-any commits.files.rev][if-any rev_href][end][commits.files.rev][if-any rev_href][end][else] [end] [commits.files.dir]/ [commits.files.file] [if-any commits.files.branch][commits.files.branch][else] [end] [# only show a diff link for changes ] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [commits.files.plus]/[commits.files.minus] [is commits.files.type "Add"][end] [is commits.files.type "Change"][end] [is commits.files.type "Remove"][end] [if-any commits.files.date][commits.files.date][else] [end] [if-any commits.files.author][commits.files.author][else] [end]
  Only first [commits.num_files] files shown. Show all files or adjust limit.
  Log:
[commits.log]
[end] [include "include/footer.ezt"] viewvc-1.1.22/templates/roots.ezt0000644000175000017500000000207311652601334017175 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Repository Listing[end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "directory"] [is cfg.options.show_roots_lastmod "1"] [end] [for roots] [is cfg.options.show_roots_lastmod "1"] [end] [end]
NameRevision Age Author Log
[roots.name]  [if-any roots.log_href][roots.rev][else][roots.rev][end]  [roots.ago]  [roots.author]  [roots.short_log]
[include "include/footer.ezt"] viewvc-1.1.22/templates/graph.ezt0000644000175000017500000000057210316546610017133 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Graph of /[where][end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "graph"] [include "include/file_header.ezt"]
[imagemap] Revisions of [where]
[include "include/footer.ezt"] viewvc-1.1.22/templates/query_form.ezt0000644000175000017500000001724111015076476020230 0ustar cmpilatocmpilato[# setup page definitions] [define page_title]Query on /[where][end] [define help_href][docroot]/help_rootview.html[end] [# end] [include "include/header.ezt" "query"]

Directory Browse Directory

[for query_hidden_values][end] [is roottype "cvs"] [# For subversion, the branch field is not used ] [end]
Branch:
Subdirectory:
(you can list multiple directories separated by commas)
File:
Who:
Comment:
Sort By:
Date:
hours
and
(use the form yyyy-mm-dd hh:mm:ss)
Limit: Show at most changed files per commit.
(use 0 to show all files)
[include "include/footer.ezt"] viewvc-1.1.22/templates/log.ezt0000644000175000017500000001100610754373137016615 0ustar cmpilatocmpilato[include "include/log_header.ezt"] [define first_revision][end] [define last_revision][end] [for entries] [if-index entries first][define first_revision][entries.rev][end][end] [if-index entries last][define last_revision][entries.rev][end][end]

[is entries.state "dead"] Revision [entries.rev] [else] [for entries.tag_names] [end] [for entries.branch_names] [end] Revision [is roottype "svn"][entries.rev][else][entries.rev][end] - [if-any entries.view_href] [is pathtype "file"] (view) [else] Directory Listing [end] [end] [if-any entries.download_href](download)[end] [if-any entries.download_text_href](as text)[end] [if-any entries.annotate_href](annotate)[end] [is pathtype "file"] [# if you don't want to allow select for diffs then remove this section] [is entries.rev rev_selected] - [[]selected] [else] - [[]select for diffs] [end] [end] [end] [if-any entries.vendor_branch] (vendor branch) [end]
[is roottype "svn"] [if-index entries last]Added[else]Modified[end] [end] [if-any entries.date][entries.date][else](unknown date)[end] [if-any entries.ago]([entries.ago] ago)[end] by [if-any entries.author][entries.author][else](unknown author)[end] [if-any entries.orig_path]
Original Path: [entries.orig_path] [end] [if-any entries.branches]
Branch: [for entries.branches] [entries.branches.name][if-index entries.branches last][else],[end] [end] [end] [if-any entries.tags]
CVS Tags: [for entries.tags] [entries.tags.name][if-index entries.tags last][else],[end] [end] [end] [if-any entries.branch_points]
Branch point for: [for entries.branch_points] [entries.branch_points.name][if-index entries.branch_points last][else],[end] [end] [end] [if-any entries.prev] [if-any entries.changed] [is roottype "cvs"]
Changes since [entries.prev]: [entries.changed] lines [end] [end] [end] [is roottype "svn"] [if-any entries.size]
File length: [entries.size] byte(s) [end] [if-any entries.copy_path]
Copied from: [entries.copy_path] revision [entries.copy_rev] [end] [end] [if-any entries.lockinfo]
Lock status: Locked [entries.lockinfo] [end] [is entries.state "dead"]
FILE REMOVED [else] [is pathtype "file"] [if-any entries.prev]
Diff to previous [entries.prev] [if-any human_readable] [else] (colored) [end] [end] [is roottype "cvs"] [if-any entries.branch_point] , to branch point [entries.branch_point] [if-any human_readable] [else] (colored) [end] [end] [if-any entries.next_main] , to next main [entries.next_main] [if-any human_readable] [else] (colored) [end] [end] [end] [if-any entries.diff_to_sel_href] [if-any entries.prev], [else]
Diff[end] to selected [rev_selected] [if-any human_readable] [else] (colored) [end] [end] [end] [end]
[entries.log]
[end] [include "include/log_footer.ezt"] viewvc-1.1.22/CHANGES0000644000175000017500000005432512265242023014304 0ustar cmpilatocmpilatoVersion 1.1.22 (released 14-Jan-2014) * minor directory sorting logic fix (re: show_subdir_lastmod) * fix display of show_subdir_lastmod details (issue #532) * pay attention to chardet's detection confidence * linkify line numbers in markup/annotate view Version 1.1.21 (released 13-Sep-2013) * fix markup/annotate exception with Python < 2.7 (issue #527) Version 1.1.20 (released 24-Apr-2013) * fix tab-to-space handling regression in markup view * fix regression in root lookup handling (issue #526) Version 1.1.19 (released 22-Apr-2013) * improve root lookup performance (issue #523) * new 'max_filesize_kbytes' config option and handling (issue #524) * tarball generation improvements: - preserve Subversion symlinks in generated tarballs (issue #487) - reduce memory usage of tarball generation logic - fix double compression of generated tarballs (issue #525) * file content handling improvements: - expanded support for encoding detection and transcoding (issue #11) - fix tab-to-space conversion bugs in markup, annotate, and diff views - fix handling of trailing whitespace in diff view * add support for timestamp display in ISO8601 format (issue #46) Version 1.1.18 (released 28-Feb-2013) * fix exception raised by BDB-backed SVN repositories (issue #519) * hide revision-less files when rcsparse is in use * include branchpoints in branch views using rcsparse (issue #347) * miscellaneous cvsdb improvements: - add --port option to make-database (issue #521) - explicitly name columns in queries (issue #522) - update MySQL syntax to avoid discontinued "TYPE=" terms Version 1.1.17 (released 25-Oct-2012) * fix exception caused by uninitialized variable usage (issue #516) Version 1.1.16 (released 24-Oct-2012) * security fix: escape "extra" diff info to avoid XSS attack (issue #515) * add 'binary_mime_types' configuration option and handling (issue #510) * fix 'select for diffs' persistence across log pages (issue #512) * remove lock status and filesize check on directories in remote SVN views * fix bogus 'Annotation of' page title for non-annotated view (issue #514) Version 1.1.15 (released 22-Jun-2012) * security fix: complete authz support for remote SVN views (issue #353) * security fix: log msg leak in SVN revision view with unreadable copy source * fix several instances of incorrect information in remote SVN views * increase performance of some revision metadata lookups in remote SVN views * fix RSS feed regression introduced in 1.1.14 Version 1.1.14 (released 12-Jun-2012) * fix annotation of svn files with non-URI-safe paths (issue #504) * handle file:/// Subversion rootpaths as local roots (issue #446) * fix bug caused by trying to case-normalize anon usernames (issue #505) * speed up log handling by reusing tokenization results (issue #506) * add support for custom revision log markup rules (issue #246) Version 1.1.13 (released 23-Jan-2012) * fix svndbadmin failure on deleted paths under Subversion 1.7 (issue #499) * fix annotation of files in svn roots with non-URI-safe paths * fix stray annotation warning in markup display of images * more gracefully handle attempts to display binary content (issue #501) Version 1.1.12 (released 03-Nov-2011) * fix path display in patch and certain diff views (issue #485) * fix broken cvsdb glob searching (issue 486) * allow svn revision specifiers to have leading r's (issue #441, #448) * allow environmental override of configuration location (issue #494) * fix exception HTML-escaping non-string data under WSGI (issue #454) * add links to root logs from roots view (issue #470) * use Pygments lexer-guessing functionality (issue #495) Version 1.1.11 (released 17-May-2011) * security fix: remove user-reachable override of cvsdb row limit * fix broken standalone.py -c and -d options handling * add --help option to standalone.py * fix stack trace when asked to checkout a directory (issue #478) * improve memory usage and speed of revision log markup (issue #477) * fix broken annotation view in CVS keyword-bearing files (issue #479) * warn users when query results are incomplete (issue #433) * avoid parsing errors on RCS newphrases in the admin section (issue #483) * make rlog parsing code more robust in certain error cases (issue #444) Version 1.1.10 (released 15-Mar-2011) * fix stack trace in Subversion revision info logic (issue #475, issue #476) Version 1.1.9 (released 18-Feb-2011) * vcauth universal access determinations (issue #425) * rework svn revision info cache for performance * make revision log "extra pages" count configurable * fix Subversion 1.4.x revision log compatibility code regression * display sanitized error when authzfile is malformed * restore markup of URLs in file contents (issue #455) * optionally display last-committed metadata in roots view (issue #457) Version 1.1.8 (released 02-Dec-2010) * fix slowness triggered by allow_compress=1 configuration (issue #467) * allow use of 'fcrypt' for Windows standalone.py authn support (issue #471) * yield more useful error on directory markup/annotate request (issue #472) Version 1.1.7 (released 09-Sep-2010) * display Subversion revision properties in the revision view (issue #453) * fix exception in 'standalone.py -r REPOS' when run without a config file * fix standalone.py server root deployments (--script-alias='') * add Basic authentication support to standalone.py (Unix only) (issue #49) * fix obscure "unexpected NULL parent pool" Subversion bindings error * enable path info / link display in remote Subversion root revision view * fix vhost name case handling inconsistency (issue #466) * use svn:mime-type property charset param as encoding hint * markup Subversion revision references in log messages (issue #313) * add rudimentary support for FastCGI-based deployments (issue #464) * fix query script WSGI deployment * add configuration to fix query script cross-linking to ViewVC Version 1.1.6 (released 02-Jun-2010) * add rudimentary support for WSGI-based deployments (issue #397) * fix exception caused by trying to HTML-escape non-string data (issue #454) * fix incorrect RSS feed Content-Type header (issue #449) * fix RSS encoding problem (issue #451) * allow 'svndbadmin purge' to work on missing repositories (issue #452) Version 1.1.5 (released 29-Mar-2010) * security fix: escape user-provided search_re input to avoid XSS attack Version 1.1.4 (released 10-Mar-2010) * security fix: escape user-provided query form input to avoid XSS attack * fix standalone.py failure (when per-root options aren't used) (issue #445) * fix annotate failure caused by ignored svn_config_dir (issue #447) Version 1.1.3 (released 22-Dec-2009) * security fix: add root listing support of per-root authz config * security fix: query.py requires 'forbidden' authorizer (or none) in config * fix URL-ification of truncated log messages (issue #3) * fix regexp input validation (issue #426, #427, #440) * add support for configurable tab-to-spaces conversion * fix not-a-sequence error in diff view * allow viewvc-install to work when templates-contrib is absent * minor template improvements/corrections * expose revision metadata in diff view (issue #431) * markup file/directory item property URLs and email addresses (issue #434) * make ViewVC cross copies in Subversion history by default * fix bug that caused standalone.py failure under Python 1.5.2 (issue #442) * fix support for per-vhost overrides of authorizer parameters (issue #411) * fix root name identification in query.py interface Version 1.1.2 (released 11-Aug-2009) * security fix: validate the 'view' parameter to avoid XSS attack * security fix: avoid printing illegal parameter names and values * add optional support for character encoding detection (issue #400) * fix username case handling in svnauthz module (issue #419) * fix cvsdbadmin/svnadmin rebuild error on missing repos (issue #420) * don't drop leading blank lines from colorized file contents (issue #422) * add file.ezt template logic for optionally hiding binary file contents Version 1.1.1 (released 03-Jun-2009) * fix broken query form (missing required template variables) (issue #416) * fix bug in cvsdb which caused rebuild operations to lose data (issue #417) * fix cvsdb purge/rebuild repos lookup to error on missing repos * fix misleading file contents view page title Version 1.1.0 (released 13-May-2009) * add support for full content diffs (issue #153) * make many more data dictionary items available to all views * various rcsparse and tparse module fixes * add daemon mode to standalone.py (issue #235) * rework helper application configuration options (issues #229, #62) * teach standalone.py to recognize Subversion repositories via -r option * now interpret relative paths in "viewvc.conf" as relative to that file * add 'purge' subcommand to cvsdbadmin and svndbadmin (issue #271) * fix orphaned data bug in cvsdbadmin/svndbadmin rebuild (issue #271) * add support for query by log message (issues #22, #121) * fix bug parsing 'svn blame' output with too-long author names (issue #221) * fix default standalone.py port to be within private IANA range (issue #234) * add unified configury of allowed views; checkout view disabled by default * add support for ranges of revisions to svndbadmin (issue #224) * make the query handling more forgiving of malformatted subdirs (issue #244) * add support for per-root configuration overrides (issue #371) * add support for optional email address mangling (issue #290) * extensible path-based authorization subsystem (issue #268), supporting: - Subversion authz files (new) - regexp-based path hiding (for compat with 1.0.x) - file glob top-level directory hiding (for compat with 1.0.x) * allow default file view to be "markup" (issue #305) * add support for displaying file/directory properties (issue #39) * pagination improvements * add gzip output encoding support for template-driven pages * fix cache control bugs (issue #259) * add RSS feed URL generation for file history * add support for remote creation of ViewVC checkins database * add integration with Pygments for syntax highlighting * preserve executability of Subversion files in tarballs (issue #233) * add ability to set Subversion runtime config dir (issue #351, issue #339) * show RSS/query links only for roots found in commits database (issue #357) * recognize Subversion svn:mime-type property values (issue #364) * hide CVS files when viewing tags/branches on which they don't exist * allow hiding of errorful entries from the directory view (issue #105) * fix directory view sorting UI * tolerate malformed Accept-Language headers (issue #396) * allow MIME type mapping overrides in ViewVC configuration (issue #401) * fix exception in rev-sorted remote Subversion directory views (issue #409) * allow setting of page sizes for log and dir views individually (issue #402) Version 1.0.9 (released 11-Aug-2009) * security fix: validate the 'view' parameter to avoid XSS attack * security fix: avoid printing illegal parameter names and values Version 1.0.8 (released 05-May-2009) * fix directory view sorting UI * tolerate malformed Accept-Language headers (issue #396) * fix directory log views in revision-less Subversion repositories * fix exception in rev-sorted remote Subversion directory views (issue #409) Version 1.0.7 (released 14-Oct-2008) * fix regression in the 'as text' download view (issue #373) Version 1.0.6 (released 16-Sep-2008) * security fix: ignore arbitrary user-provided MIME types (issue #354) * fix bug in regexp search filter when used with sticky tag (issue #346) * fix bug in handling of certain 'co' output (issue #348) * fix regexp search filter template bug * fix annotate code syntax error * fix mod_python import cycle (issue #369) Version 1.0.5 (released 28-Feb-2008) * security fix: omit commits of all-forbidden files from query results * security fix: disallow direct URL navigation to hidden CVSROOT folder * security fix: strip forbidden paths from revision view * security fix: don't traverse log history thru forbidden locations * security fix: honor forbiddenness via diff view path parameters * new 'forbiddenre' regexp-based path authorization feature * fix root name conflict resolution inconsistencies (issue #287) * fix an oversight in the CVS 1.12.9 loginfo-handler support * fix RSS feed content type to be more specific (issue #306) * fix entity escaping problems in RSS feed data (issue #238) * fix bug in tarball generation for remote Subversion repositories * fix query interface file-count-limiting logic * fix query results plus/minus count to ignore forbidden files * fix blame error caused by 'svn' unable to create runtime config dir Version 1.0.4 (released 10-Apr-2007) * fix some markup bugs in query views (issue #266) * fix loginfo-handler's support for CVS 1.12.9 (issues #151, #257) * make viewvc-install able to run from an arbitrary location * update viewvc-install's output for readability * fix bug writing commits to non-MyISAM databases (issue #262) * allow long paths in generated tarballs (issue #12) * fix bug interpreting EZT substitute patterns * fix broken markup view disablement * fix broken directory view link generation in directory log view * fix Windows-specific viewvc-install bugs * fix broke query result links for Subversion deleted items (issue #296) * fix some output XHTML validation buglets * fix database query cache staleness problems (issue #180) Version 1.0.3 (released 13-Oct-2006) * fix bug in path shown for Subversion deleted-under-copy items (issue #265) * security fix: declare charset for views to avoid IE UTF7 XSS attack Version 1.0.2 (released 29-Sep-2006) * minor documentation fixes * fix Subversion annotate functionality on Windows (issue #18) * fix annotate assertions on uncanonicalized #include paths (issue #208) * make RSS URL method match the method used to generate it (issue #245) * fix Subversion annotation to run non-interactively, preventing hangs * fix bug in custom syntax highlighter fallback logic * fix bug in PHP CGI hack to avoid force-cgi-redirect errors Version 1.0.1 (released 20-Jul-2006) * fix exception on log page when use_pagesize is enabled * fix an XHTML validation bug in the footer template (issue #239) * fix handling of single-component CVS revision numbers (issue #237) * fix bug in download-as-text URL link generation (issue #241) * fix query.cgi bug, missing 'rss_href' template data item (issue #249) * no longer omit empty Subversion directories from tarballs (issue #250) * use actual modification time for Subversion directories in tarballs Version 1.0 (released 01-May-2006) * add support for viewing Subversion repositories * add support for running on MS Windows * generate strict XHTML output * add support for caching by sending "Last-Modified", "Expires", "ETag", and "Cache-Control" headers * add support for Mod_Python on Apache 2.x and ASP on IIS * Several changes to standalone.py: - -h commandline option to specify hostname for non local use. - -r commandline option may be repeated to use more than repository before actually installing ViewCVS. - New GUI field to test paging. * add new, better-integrated query interface * add integrated RSS feeds * add new "root_as_url_component" option to embed root names as path components in ViewCVS URLs for a more natural URL scheme in ViewCVS configurations with multiple repositories. * add new "use_localtime" option to display local times instead of UTC times * add new "root_parents" option to make it possible to add and remove repositories without modifying the ViewCVS configuration * add new "template_dir" option to facilitate switching between sets of templates * add new "sort_group_dirs" option to disable grouping of directories in directory listings * add new "port" option to connect to a MySQL database on a nonstandard port * make "default_root" option optional. When no root is specified, show a page listing all available repositories * add "default_file_view" option to make it possible for relative links and image paths in checked out HTML files to work without the need for special /*checkout*/ prefixes in URLs. Deprecate "checkout_magic" option and disable by default * add "limit_changes" option to limit number of changed files shown per commit by default in query results and in the Subversion revision view * hide CVS "Attic" directories and add simple toggle for showing dead files in directory listings * show Unified, Context and Side-by-side diffs in HTML instead of in bare text pages * make View/Download links work the same for all file types * add links to tip of selected branch on log page * allow use of "Highlight" program for colorizing * enable enscript colorizing for more file types * add sorting arrows for directory views * get rid of popup windows for checkout links * obfuscate email addresses in html output by encoding @ symbol with an HTML character reference * add paging capability * Improvements to templates - add new template authoring guide - increase coverage, use templates to produce HTML for diff pages, markup pages, annotate pages, and error pages - move more common page elements into includes - add new template variables providing ViewCVS URLs for more links between related pages and less URL generation inside templates * add new [define] EZT directive for assigning variables within templates * add command line argument parsing to install script to allow non-interactive installs * add stricter parameter validation to lower likelihood of cross-site scripting vulnerabilities * add support for cvsweb's "mime_type=text/x-cvsweb-markup" URLs * fix incompatibility with enscript 1.6.3 * fix bug in parsing FreeBSD rlog output * work around rlog assumption all two digit years in RCS files are relative to the year 1900. * change loginfo-handler to cope with spaces in filenames and support a simpler command line invocation from CVS * make cvsdbadmin work properly when invoked on CVS subdirectory paths instead of top-level CVS root paths * show diff error when comparing two binary files * make regular expression search skip binary files * make regular expression search skip nonversioned files in CVS directories instead of choking on them * fix tarball generator so it doesn't include forbidden modules * output "404 Not Found" errors instead of "403 Forbidden" errors to not reveal whether forbidden paths exist * fix sorting bug in directory view * reset log and directory page numbers when leaving those pages * reset sort direction in directory listing when clicking new columns * fix "Accept-Language" handling for Netscape 4.x browsers * fix file descriptor leak in standalone server * clean up zombie processes from running enscript * fix mysql "Too many connections" error in cvsdbadmin * get rid of mxDateTime dependency for query database * store query database times in UTC instead of local time * fix daylight saving time bugs in various parts of the code Version 0.9.4 (released 17-Aug-2005) * security fix: omit forbidden/hidden modules from query results. Version 0.9.3 (released 17-May-2005) * security fix: disallow bad "content-type" input [CAN-2004-1062] * security fix: disallow bad "sortby" and "cvsroot" input [CAN-2002-0771] * security fix: omit forbidden/hidden modules from tarballs [CAN-2004-0915] Version 0.9.2 (released 15-Jan-2002) * fix redirects to Attic for diffs * fix diffs that have no changes (causing an infinite loop) Version 0.9.1 (released 26-Dec-2001) * fix a problem with some syntax in ndiff.py which isn't compatible with Python 1.5.2 (causing problems at install time) * remove a debug statement left in the code which continues to append lines to /tmp/log Version 0.9 (released 23-Dec-2001) * create templates for the rest of the pages: markup pages, graphs, annotation, and diff. * add multiple language support and dynamic selection based on the Accept-Language request header * add support for key/value files to provide a way for user-defined variables within templates * add optional regex searching for file contents * add new templates for the navigation header and the footer * EZT changes: - add formatting into print directives - add parameters to [include] directives - relax what can go in double quotes - [include] directives are now relative to the current template - throw an exception for unclosed blocks * changes to standalone.py: add flag for regex search * add more help pages * change installer to optionally show diffs * fix to log.ezt and log_table.ezt to select "Side by Side" properly * create dir_alternate.ezt for the flipped rev/name links * various UI tweaks for the directory pages Version 0.8 (released 10-Dec-2001) * add EZT templating mechanism for generating output pages * big update of cvs commit database - updated MySQL support - new CGI - better database caching - switch from old templates to new EZT templates (and integration of look-and-feel) * optional usage of CVSGraph is now builtin * standalone server (for testing) is now provided * shifted some options from viewcvs.conf to the templates * the help at the top of the pages has been shifted to separate help pages, so experienced users don't have to keep seeing it * paths in viewcvs.conf don't require trailing slashes any more * tweak the colorizing for Pascal and Fortran files * fix file readability problem where the user had access via the group, but the process' group did not match that group * some Daylight Savings Time fixes in the CVS commit database * fix tarball generation (the file name) for the root dir * changed default human-readable-diff colors to "stoplight" metaphor * web site and doc revamps * fix the mime types on the download, view, etc links * improved error response when the cvs root is missing * don't try to process vhosts if the config section is not present * various bug fixes and UI tweaks �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������