구글로 하면 잘되나
다만, 구글에서 차단하지 않도록 "보안 수준이 낮은 앱"을 허용해야 한다.
커밋시 이메일은 이렇게 오긴 온다.
---
svn 저장소 안에 보면 이런식으로 구성이 되어있는데
~/repos $ ll 합계 24 -rw-r--r-- 1 pi pi 246 12월 29 13:48 README.txt drwxr-xr-x 2 pi pi 4096 12월 29 13:48 conf drwxr-sr-x 6 pi pi 4096 12월 29 13:48 db -r--r--r-- 1 pi pi 2 12월 29 13:48 format drwxr-xr-x 2 pi pi 4096 12월 29 13:48 hooks drwxr-xr-x 2 pi pi 4096 12월 29 13:48 locks |
hooks에 보면 이런식으로 템플릿 파일들이 존재한다.
~/repos/hooks $ ll 합계 36 -rwxr-xr-x 1 pi pi 2107 12월 29 13:48 post-commit.tmpl -rwxr-xr-x 1 pi pi 1663 12월 29 13:48 post-lock.tmpl -rwxr-xr-x 1 pi pi 2344 12월 29 13:48 post-revprop-change.tmpl -rwxr-xr-x 1 pi pi 1592 12월 29 13:48 post-unlock.tmpl -rwxr-xr-x 1 pi pi 3510 12월 29 13:48 pre-commit.tmpl -rwxr-xr-x 1 pi pi 2434 12월 29 13:48 pre-lock.tmpl -rwxr-xr-x 1 pi pi 2818 12월 29 13:48 pre-revprop-change.tmpl -rwxr-xr-x 1 pi pi 2122 12월 29 13:48 pre-unlock.tmpl -rwxr-xr-x 1 pi pi 3235 12월 29 13:48 start-commit.tmpl |
아무튼 post-commit을 열어 보면 얘는 템플릿이니 수정해서 써라라고 한다.
일단 버전마다 변수가 갯수가 다르네?
$ cat post-commit.tmpl #!/bin/sh # POST-COMMIT HOOK # # The post-commit hook is invoked after a commit. Subversion runs # this hook by invoking a program (script, executable, binary, etc.) # named 'post-commit' (for which this file is a template) with the # following ordered arguments: # # [1] REPOS-PATH (the path to this repository) # [2] REV (the number of the revision just committed) # [3] TXN-NAME (the name of the transaction that has become REV) # # The default working directory for the invocation is undefined, so # the program should set one explicitly if it cares. # # Because the commit has already completed and cannot be undone, # the exit code of the hook program is ignored. The hook program # can use the 'svnlook' utility to help it examine the # newly-committed tree. # # On a Unix system, the normal procedure is to have 'post-commit' # invoke other programs to do the real work, though it may do the # work itself too. # # Note that 'post-commit' must be executable by the user(s) who will # invoke it (typically the user httpd runs as), and that user must # have filesystem-level permission to access the repository. # # On a Windows system, you should name the hook program # 'post-commit.bat' or 'post-commit.exe', # but the basic idea is the same. # # The hook program typically does not inherit the environment of # its parent process. For example, a common problem is for the # PATH environment variable to not be set to its usual value, so # that subprograms fail to launch unless invoked via absolute path. # If you're having unexpected problems with a hook program, the # culprit may be unusual (or missing) environment variables. # # Here is an example hook script, for a Unix /bin/sh interpreter. # For more examples and pre-written hooks, see those in # /usr/share/subversion/hook-scripts, and in the repository at # http://svn.apache.org/repos/asf/subversion/trunk/tools/hook-scripts/ and # http://svn.apache.org/repos/asf/subversion/trunk/contrib/hook-scripts/ REPOS="$1" REV="$2" TXN_NAME="$3" "$REPOS"/hooks/mailer.py commit "$REPOS" $REV "$REPOS"/mailer.conf |
근데 저 망할(!) mailer.py가 없어서 검색을 해보니
python-mailer나 subversion-tools가 맞을거 같고
$ sudo apt-file search mailer.py bzr-email: /usr/lib/python2.7/dist-packages/bzrlib/plugins/email/emailer.py bzr-email: /usr/share/pyshared/bzrlib/plugins/email/emailer.py gourmet: /usr/lib/python2.7/dist-packages/gourmet/exporters/recipe_emailer.py gourmet: /usr/lib/python2.7/dist-packages/gourmet/plugins/email_plugin/emailer.py gourmet: /usr/lib/python2.7/dist-packages/gourmet/plugins/email_plugin/recipe_emailer.py python-apptools: /usr/lib/python2.7/dist-packages/apptools/logger/agent/quality_agent_mailer.py python-apptools: /usr/share/pyshared/apptools/logger/agent/quality_agent_mailer.py python-enthoughtbase: /usr/lib/python2.6/dist-packages/enthought/logger/agent/quality_agent_mailer.py python-enthoughtbase: /usr/lib/python2.7/dist-packages/enthought/logger/agent/quality_agent_mailer.py python-enthoughtbase: /usr/share/pyshared/enthought/logger/agent/quality_agent_mailer.py python-mailer: /usr/share/pyshared/mailer.py python-mailutils: /usr/lib/python2.7/dist-packages/mailutils/mailer.py python-scrapy: /usr/lib/python2.7/dist-packages/scrapy/contrib/statsmailer.py python-zope.sendmail: /usr/lib/python2.6/dist-packages/zope/sendmail/mailer.py python-zope.sendmail: /usr/lib/python2.6/dist-packages/zope/sendmail/tests/test_mailer.py python-zope.sendmail: /usr/lib/python2.7/dist-packages/zope/sendmail/mailer.py python-zope.sendmail: /usr/lib/python2.7/dist-packages/zope/sendmail/tests/test_mailer.py python-zope.sendmail: /usr/share/pyshared/zope/sendmail/mailer.py python-zope.sendmail: /usr/share/pyshared/zope/sendmail/tests/test_mailer.py roundup: /usr/lib/python2.7/dist-packages/roundup/mailer.py sabnzbdplus: /usr/share/sabnzbdplus/sabnzbd/emailer.py subversion-tools: /usr/share/subversion/hook-scripts/mailer/mailer.py zope2.13: /usr/lib/zope2.13/lib/python/Products.MailHost-2.13.1.egg/Products/MailHost/mailer.py zope2.13: /usr/lib/zope2.13/lib/python/zope.sendmail-3.7.5.egg/zope/sendmail/mailer.py zope2.13: /usr/lib/zope2.13/lib/python/zope.sendmail-3.7.5.egg/zope/sendmail/tests/test_mailer.py |
설치를 하니 온갖 패키지가 다 끌려오네..
synology에서 이거 쓸 수 있긴 할려나?
$ sudo apt-get install subversion-tools 패키지 목록을 읽는 중입니다... 완료 의존성 트리를 만드는 중입니다 상태 정보를 읽는 중입니다... 완료 다음 패키지를 더 설치할 것입니다: bsd-mailx exim4 exim4-base exim4-config exim4-daemon-light libconfig-inifiles-perl libsvn-perl liburi-perl python-subversion svn2cl xsltproc 제안하는 패키지: eximon4 exim4-doc-html exim4-doc-info spf-tools-perl swaks libwww-perl ruby-svn 추천하는 패키지: mailx 다음 새 패키지를 설치할 것입니다: bsd-mailx exim4 exim4-base exim4-config exim4-daemon-light libconfig-inifiles-perl libsvn-perl liburi-perl python-subversion subversion-tools svn2cl xsltproc 0개 업그레이드, 12개 새로 설치, 0개 제거 및 33개 업그레이드 안 함. 4,205 k바이트 아카이브를 받아야 합니다. 이 작업 후 13.6 M바이트의 디스크 공간을 더 사용하게 됩니다. 계속 하시겠습니까? [Y/n] |
$ sudo apt-file search mailer.conf mixmaster: /etc/mixmaster/remailer.conf subversion-tools: /usr/share/subversion/hook-scripts/mailer/mailer.conf.example svnmailer: /usr/share/doc/svnmailer/docs/apidoc/svnmailer.config-module.html svnmailer: /usr/share/doc/svnmailer/docs/apidoc/svnmailer.config-pysrc.html svnmailer: /usr/share/doc/svnmailer/docs/apidoc/svnmailer.config.ConfigFileSettings-class.html svnmailer: /usr/share/doc/svnmailer/docs/apidoc/svnmailer.config.ConfigInvalidError-class.html svnmailer: /usr/share/doc/svnmailer/docs/apidoc/svnmailer.config.ConfigMappingSectionNotFoundError-class.html svnmailer: /usr/share/doc/svnmailer/docs/apidoc/svnmailer.config.ConfigMappingSpecInvalidError-class.html svnmailer: /usr/share/doc/svnmailer/docs/apidoc/svnmailer.config.ConfigMissingError-class.html svnmailer: /usr/share/doc/svnmailer/docs/apidoc/svnmailer.config.ConfigNotFoundError-class.html svnmailer: /usr/share/doc/svnmailer/docs/apidoc/svnmailer.config.ConfigOptionUnknownError-class.html svnmailer: /usr/share/doc/svnmailer/docs/apidoc/svnmailer.config.ConfigSectionNotFoundError-class.html svnmailer: /usr/share/doc/svnmailer/docs/apidoc/svnmailer.config.Error-class.html svnmailer: /usr/share/doc/svnmailer/examples/svnmailer.conf |
그나저나 설정파일 드럽게 기네
$ cat /usr/share/subversion/hook-scripts/mailer/mailer.conf.example # # mailer.conf: example configuration file for mailer.py # # $Id: mailer.conf.example 1439592 2013-01-28 19:20:53Z danielsh $ [general] # The [general].diff option is now DEPRECATED. # Instead use [defaults].diff . # # One delivery method must be chosen. mailer.py will prefer using the # "mail_command" option. If that option is empty or commented out, # then it checks whether the "smtp_hostname" option has been # specified. If neither option is set, then the commit message is # delivered to stdout. # # This command will be invoked with destination addresses on the command # line, and the message piped into it. #mail_command = /usr/sbin/sendmail # This option specifies the hostname for delivery via SMTP. #smtp_hostname = localhost # Username and password for SMTP servers requiring authorisation. #smtp_username = example #smtp_password = example # -------------------------------------------------------------------------- # # CONFIGURATION GROUPS # # Any sections other than [general], [defaults], [maps] and sections # referred to within [maps] are considered to be user-defined groups # which override values in the [defaults] section. # These groups are selected using the following three options: # # for_repos # for_paths # search_logmsg # # Each option specifies a regular expression. for_repos is matched # against the absolute path to the repository the mailer is operating # against. for_paths is matched against *every* path (files and # dirs) that was modified during the commit. # # The options specified in the [defaults] section are always selected. The # presence of a non-matching for_repos has no relevance. Note that you may # still use a for_repos value to extract useful information (more on this # later). Any user-defined groups without a for_repos, or which contains # a matching for_repos, will be selected for potential use. # # The subset of user-defined groups identified by the repository are further # refined based on the for_paths option. A group is selected if at least # one path(*) in the commit matches the for_paths regular expression. Note # that the paths are relative to the root of the repository and do not # have a leading slash. # # (*) Actually, each path will select just one group. Thus, it is possible # that one group will match against all paths, while another group matches # none of the paths, even though its for_paths would have selected some of # the paths in the commit. # # search_logmsg specifies a regular expression to match against the # log message. If the regular expression does not match the log # message, the group is not matched; if the regular expression matches # once, the group is used. If there are multiple matches, each # successful match generates another group-match (this is useful if # "named groups" are used). If search_logmsg is not used, no log # message filtering is performed. # # Groups are matched in no particular order. Do not depend upon their # order within this configuration file. The values from [defaults] will # be used if no group is matched or an option in a group does not override # the corresponding value from [defaults]. # # Generally, a commit email is generated for each group that has been # selected. The script will try to minimize mails, so it may be possible # that a single message will be generated to multiple recipients. In # addition, it is possible for multiple messages per group to be generated, # based on the various substitutions that are performed (see the following # section). # # # SUBSTITUTIONS # # The regular expressions can use the "named group" syntax to extract # interesting pieces of the repository or commit path. These named values # can then be substituted in the option values during mail generation. # # For example, let's say that you have a repository with a top-level # directory named "clients", with several client projects underneath: # # REPOS/ # clients/ # gsvn/ # rapidsvn/ # winsvn/ # # The client name can be extracted with a regular expression like: # # for_paths = clients/(?P<client>[^/]*)($|/) # # The substitution is performed using Python's dict-based string # interpolation syntax: # # to_addr = commits@%(client)s.tigris.org # # The %(NAME)s syntax will substitute whatever value for NAME was captured # in the for_repos and for_paths regular expressions. The set of names # available is obtained from the following set of regular expressions: # # [defaults].for_repos (if present) # [GROUP].for_repos (if present in the user-defined group "GROUP") # [GROUP].for_paths (if present in the user-defined group "GROUP") # # The names from the regexes later in the list override the earlier names. # If none of the groups match, but a for_paths is present in [defaults], # then its extracted names will be available. # # Further suppose you want to match bug-ids in log messages: # # search_logmsg = (?P<bugid>(ProjA|ProjB)#\d) # # The bugids would be of the form ProjA#123 and ProjB#456. In this # case, each time the regular expression matches, another match group # will be generated. Thus, if you use: # # commit_subject_prefix = %(bugid)s: # # Then, a log message such as "Fixes ProjA#123 and ProjB#234" would # match both bug-ids, and two emails would be generated - one with # subject "ProjA#123: <...>" and "ProjB#234: <...>". # # Note that each unique set of names for substitution will generate an # email. In the above example, if a commit modified files in all three # client subdirectories, then an email will be sent to all three commits@ # mailing lists on tigris.org. # # The substitution variable "author" is provided by default, and is set # to the author name passed to mailer.py for revprop changes or the # author defined for a revision; if neither is available, then it is # set to "no_author". Thus, you might define a line like: # # from_addr = %(author)s@example.com # # The substitution variable "repos_basename" is provided, and is set to # the directory name of the repository. This can be useful to set # a custom subject that can be re-used in multiple repositories: # # commit_subject_prefix = [svn-%(repos_basename)s] # # For example if the repository is at /path/to/repo/project-x then # the subject of commit emails will be prefixed with [svn-project-x] # # # SUMMARY # # While mailer.py will work to minimize the number of mail messages # generated, a single commit can potentially generate a large number # of variants of a commit message. The criteria for generating messages # is based on: # # groups selected by for_repos # groups selected by for_paths # unique sets of parameters extracted by the above regular expressions # [defaults] # This is not passed to the shell, so do not use shell metacharacters. # The command is split around whitespace, so if you want to include # whitespace in the command, then ### something ###. diff = /usr/bin/diff -u -L %(label_from)s -L %(label_to)s %(from)s %(to)s # The default prefix for the Subject: header for commits. commit_subject_prefix = # The default prefix for the Subject: header for propchanges. propchange_subject_prefix = # The default prefix for the Subject: header for locks. lock_subject_prefix = # The default prefix for the Subject: header for unlocks. unlock_subject_prefix = # The default From: address for messages. If the from_addr is not # specified or it is specified but there is no text after the `=', # then the revision's author is used as the from address. If the # revision author is not specified, such as when a commit is done # without requiring authentication and authorization, then the string # 'no_author' is used. You can specify a default from_addr here and # if you want to have a particular for_repos group use the author as # the from address, you can use "from_addr =". from_addr = invalid@example.com # The default To: addresses for message. One or more addresses, # separated by whitespace (no commas). # NOTE: If you want to use a different character for separating the # addresses put it in front of the addresses included in square # brackets '[ ]'. to_addr = invalid@example.com # If this is set, then a Reply-To: will be inserted into the message. reply_to = # Specify which types of repository changes mailer.py will create # diffs for. Valid options are any combination of # 'add copy modify delete', or 'none' to never create diffs. # If the generate_diffs option is empty, the selection is controlled # by the deprecated options suppress_deletes and suppress_adds. # Note that this only affects the display of diffs - all changes are # mentioned in the summary of changed paths at the top of the message, # regardless of this option's value. # Meaning of the possible values: # add: generates diffs for all added paths # copy: generates diffs for all copied paths # which were not changed after copying # modify: generates diffs for all modified paths, including paths that were # copied and modified afterwards (within the same commit) # delete: generates diffs for all removed paths generate_diffs = add copy modify # Commit URL construction. This adds a URL to the top of the message # that can lead the reader to a Trac, ViewVC or other view of the # commit as a whole. # # The available substitution variable is: rev #commit_url = http://diffs.server.com/trac/software/changeset/%(rev)s # Diff URL construction. For the configured diff URL types, the diff # section (which follows the message header) will include the URL # relevant to the change type, even if actual diff generation for that # change type is disabled (per the generate_diffs option). # # Available substitution variables are: path, base_path, rev, base_rev #diff_add_url = #diff_copy_url = #diff_modify_url = http://diffs.server.com/?p1=%(base_path)s&p2=%(path)s #diff_delete_url = # When set to "yes", the mailer will suppress the creation of a diff which # deletes all the lines in the file. If this is set to anything else, or # is simply commented out, then the diff will be inserted. Note that the # deletion is always mentioned in the message header, regardless of this # option's value. ### DEPRECATED (if generate_diffs is not empty, this option is ignored) #suppress_deletes = yes # When set to "yes", the mailer will suppress the creation of a diff which # adds all the lines in the file. If this is set to anything else, or # is simply commented out, then the diff will be inserted. Note that the # addition is always mentioned in the message header, regardless of this # option's value. ### DEPRECATED (if generate_diffs is not empty, this option is ignored) #suppress_adds = yes # A revision is reported on if any of its changed paths match the # for_paths option. If only some of the changed paths of a revision # match, this variable controls the behaviour for the non-matching # paths. Possible values are: # # yes: (Default) Show in both summary and diffs. # summary: Show the changed paths in the summary, but omit the diffs. # no: Show nothing more than a note saying "and changes in other areas" # show_nonmatching_paths = yes # Subject line length limit. The generated subject line will be truncated # and terminated with "...", to remain within the specified maximum length. # Set to 0 to turn off. #truncate_subject = 200 # -------------------------------------------------------------------------- [maps] # # This section can be used define rewrite mappings for option values. It # is typically used for computing from/to addresses, but can actually be # used to remap values for any option in this file. # # The mappings are global for the entire configuration file. There is # no group-specific mapping capability. For each mapping that you want # to perform, you will provide the name of the option (e.g. from_addr) # and a specification of how to perform those mappings. These declarations # are made here in the [maps] section. # # When an option is accessed, the value is loaded from the configuration # file and all %(NAME)s substitutions are performed. The resulting value # is then passed through the map. If a map entry is not available for # the value, then it will be used unchanged. # # NOTES: - Avoid using map substitution names which differ only in case. # Unexpected results may occur. # - A colon ':' is also considered as separator between option and # value (keep this in mind when trying to map a file path under # windows). # # The format to declare a map is: # # option_name_to_remap = mapping_specification # # At the moment, there is only one type of mapping specification: # # mapping_specification = '[' sectionname ']' # # This will use the given section to map values. The option names in # the section are the input values, and the option values are the result. # # # EXAMPLE: # # We have two projects using two repositories. The name of the repos # does not easily map to their commit mailing lists, so we will use # a mapping to go from a project name (extracted from the repository # path) to their commit list. The committers also need a special # mapping to derive their email address from their repository username. # # [projects] # for_repos = .*/(?P<project>.*) # from_addr = %(author)s # to_addr = %(project)s # # [maps] # from_addr = [authors] # to_addr = [mailing-lists] # # [authors] # john = jconnor@example.com # sarah = sconnor@example.com # # [mailing-lists] # t600 = spottable-commits@example.com # tx = hotness-commits@example.com # # -------------------------------------------------------------------------- # # [example-group] # # send notifications if any web pages are changed # for_paths = .*\.html # # set a custom prefix # commit_subject_prefix = [commit] # propchange_subject_prefix = [propchange] # # override the default, sending these elsewhere # to_addr = www-commits@example.com # # use the revision author as the from address # from_addr = # # use a custom diff program for this group # diff = /usr/bin/my-diff -u -L %(label_from)s -L %(label_to)s %(from)s %(to)s # # [another-example] # # commits to personal repositories should go to that person # for_repos = /home/(?P<who>[^/]*)/repos # to_addr = %(who)s@example.com # # [issuetracker] # search_logmsg = (?P<bugid>(?P<project>projecta|projectb|projectc)#\d+) # # (or, use a mapping if the bug-id to email address is not this trivial) # to_addr = %(project)s-tracker@example.com # commit_subject_prefix = %(bugid)s: # propchange_subject_prefix = %(bugid)s:
|
아따.. 드럽게 길다 -_-
$ cat /usr/share/subversion/hook-scripts/mailer/mailer.py #!/usr/bin/python # -*- coding: utf-8 -*- # # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # # mailer.py: send email describing a commit # # $HeadURL: http://svn.apache.org/repos/asf/subversion/branches/1.8.x/tools/hook-scripts/mailer/mailer.py $ # $LastChangedDate: 2013-04-12 07:44:37 +0000 (Fri, 12 Apr 2013) $ # $LastChangedBy: rhuijben $ # $LastChangedRevision: 1467191 $ # # USAGE: mailer.py commit REPOS REVISION [CONFIG-FILE] # mailer.py propchange REPOS REVISION AUTHOR REVPROPNAME [CONFIG-FILE] # mailer.py propchange2 REPOS REVISION AUTHOR REVPROPNAME ACTION \ # [CONFIG-FILE] # mailer.py lock REPOS AUTHOR [CONFIG-FILE] # mailer.py unlock REPOS AUTHOR [CONFIG-FILE] # # Using CONFIG-FILE, deliver an email describing the changes between # REV and REV-1 for the repository REPOS. # # ACTION was added as a fifth argument to the post-revprop-change hook # in Subversion 1.2.0. Its value is one of 'A', 'M' or 'D' to indicate # if the property was added, modified or deleted, respectively. # # See _MIN_SVN_VERSION below for which version of Subversion's Python # bindings are required by this version of mailer.py. import os import sys try: # Python >=3.0 import configparser from urllib.parse import quote as urllib_parse_quote except ImportError: # Python <3.0 import ConfigParser as configparser from urllib import quote as urllib_parse_quote import time import subprocess if sys.version_info[0] >= 3: # Python >=3.0 from io import StringIO else: # Python <3.0 from cStringIO import StringIO import smtplib import re import tempfile # Minimal version of Subversion's bindings required _MIN_SVN_VERSION = [1, 5, 0] # Import the Subversion Python bindings, making sure they meet our # minimum version requirements. try: import svn.fs import svn.delta import svn.repos import svn.core except ImportError: sys.stderr.write( "You need version %s or better of the Subversion Python bindings.\n" \ % ".".join([str(x) for x in _MIN_SVN_VERSION])) sys.exit(1) if _MIN_SVN_VERSION > [svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR, svn.core.SVN_VER_PATCH]: sys.stderr.write( "You need version %s or better of the Subversion Python bindings.\n" \ % ".".join([str(x) for x in _MIN_SVN_VERSION])) sys.exit(1) SEPARATOR = '=' * 78 def main(pool, cmd, config_fname, repos_dir, cmd_args): ### TODO: Sanity check the incoming args if cmd == 'commit': revision = int(cmd_args[0]) repos = Repository(repos_dir, revision, pool) cfg = Config(config_fname, repos, {'author': repos.author, 'repos_basename': os.path.basename(repos.repos_dir) }) messenger = Commit(pool, cfg, repos) elif cmd == 'propchange' or cmd == 'propchange2': revision = int(cmd_args[0]) author = cmd_args[1] propname = cmd_args[2] action = (cmd == 'propchange2' and cmd_args[3] or 'A') repos = Repository(repos_dir, revision, pool) # Override the repos revision author with the author of the propchange repos.author = author cfg = Config(config_fname, repos, {'author': author, 'repos_basename': os.path.basename(repos.repos_dir) }) messenger = PropChange(pool, cfg, repos, author, propname, action) elif cmd == 'lock' or cmd == 'unlock': author = cmd_args[0] repos = Repository(repos_dir, 0, pool) ### any old revision will do # Override the repos revision author with the author of the lock/unlock repos.author = author cfg = Config(config_fname, repos, {'author': author, 'repos_basename': os.path.basename(repos.repos_dir) }) messenger = Lock(pool, cfg, repos, author, cmd == 'lock') else: raise UnknownSubcommand(cmd) messenger.generate() def remove_leading_slashes(path): while path and path[0] == '/': path = path[1:] return path class OutputBase: "Abstract base class to formalize the interface of output methods" def __init__(self, cfg, repos, prefix_param): self.cfg = cfg self.repos = repos self.prefix_param = prefix_param self._CHUNKSIZE = 128 * 1024 # This is a public member variable. This must be assigned a suitable # piece of descriptive text before make_subject() is called. self.subject = "" def make_subject(self, group, params): prefix = self.cfg.get(self.prefix_param, group, params) if prefix: subject = prefix + ' ' + self.subject else: subject = self.subject try: truncate_subject = int( self.cfg.get('truncate_subject', group, params)) except ValueError: truncate_subject = 0 if truncate_subject and len(subject) > truncate_subject: subject = subject[:(truncate_subject - 3)] + "..." return subject def start(self, group, params): """Override this method. Begin writing an output representation. GROUP is the name of the configuration file group which is causing this output to be produced. PARAMS is a dictionary of any named subexpressions of regular expressions defined in the configuration file, plus the key 'author' contains the author of the action being reported.""" raise NotImplementedError def finish(self): """Override this method. Flush any cached information and finish writing the output representation.""" raise NotImplementedError def write(self, output): """Override this method. Append the literal text string OUTPUT to the output representation.""" raise NotImplementedError def run(self, cmd): """Override this method, if the default implementation is not sufficient. Execute CMD, writing the stdout produced to the output representation.""" # By default we choose to incorporate child stderr into the output pipe_ob = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=sys.platform != "win32") buf = pipe_ob.stdout.read(self._CHUNKSIZE) while buf: self.write(buf) buf = pipe_ob.stdout.read(self._CHUNKSIZE) # wait on the child so we don't end up with a billion zombies pipe_ob.wait() class MailedOutput(OutputBase): def __init__(self, cfg, repos, prefix_param): OutputBase.__init__(self, cfg, repos, prefix_param) def start(self, group, params): # whitespace (or another character) separated list of addresses # which must be split into a clean list to_addr_in = self.cfg.get('to_addr', group, params) # if list of addresses starts with '[.]' # use the character between the square brackets as split char # else use whitespaces if len(to_addr_in) >= 3 and to_addr_in[0] == '[' \ and to_addr_in[2] == ']': self.to_addrs = \ [_f for _f in to_addr_in[3:].split(to_addr_in[1]) if _f] else: self.to_addrs = [_f for _f in to_addr_in.split() if _f] self.from_addr = self.cfg.get('from_addr', group, params) \ or self.repos.author or 'no_author' # if the from_addr (also) starts with '[.]' (may happen if one # map is used for both to_addr and from_addr) remove '[.]' if len(self.from_addr) >= 3 and self.from_addr[0] == '[' \ and self.from_addr[2] == ']': self.from_addr = self.from_addr[3:] self.reply_to = self.cfg.get('reply_to', group, params) # if the reply_to (also) starts with '[.]' (may happen if one # map is used for both to_addr and reply_to) remove '[.]' if len(self.reply_to) >= 3 and self.reply_to[0] == '[' \ and self.reply_to[2] == ']': self.reply_to = self.reply_to[3:] def mail_headers(self, group, params): from email import Utils subject = self.make_subject(group, params) try: subject.encode('ascii') except UnicodeError: from email.Header import Header subject = Header(subject, 'utf-8').encode() hdrs = 'From: %s\n' \ 'To: %s\n' \ 'Subject: %s\n' \ 'Date: %s\n' \ 'Message-ID: %s\n' \ 'MIME-Version: 1.0\n' \ 'Content-Type: text/plain; charset=UTF-8\n' \ 'Content-Transfer-Encoding: 8bit\n' \ 'X-Svn-Commit-Project: %s\n' \ 'X-Svn-Commit-Author: %s\n' \ 'X-Svn-Commit-Revision: %d\n' \ 'X-Svn-Commit-Repository: %s\n' \ % (self.from_addr, ', '.join(self.to_addrs), subject, Utils.formatdate(), Utils.make_msgid(), group, self.repos.author or 'no_author', self.repos.rev, os.path.basename(self.repos.repos_dir)) if self.reply_to: hdrs = '%sReply-To: %s\n' % (hdrs, self.reply_to) return hdrs + '\n' class SMTPOutput(MailedOutput): "Deliver a mail message to an MTA using SMTP." def start(self, group, params): MailedOutput.start(self, group, params) self.buffer = StringIO() self.write = self.buffer.write self.write(self.mail_headers(group, params)) def finish(self): server = smtplib.SMTP(self.cfg.general.smtp_hostname) if self.cfg.is_set('general.smtp_username'): server.login(self.cfg.general.smtp_username, self.cfg.general.smtp_password) server.sendmail(self.from_addr, self.to_addrs, self.buffer.getvalue()) server.quit() class StandardOutput(OutputBase): "Print the commit message to stdout." def __init__(self, cfg, repos, prefix_param): OutputBase.__init__(self, cfg, repos, prefix_param) self.write = sys.stdout.write def start(self, group, params): self.write("Group: " + (group or "defaults") + "\n") self.write("Subject: " + self.make_subject(group, params) + "\n\n") def finish(self): pass class PipeOutput(MailedOutput): "Deliver a mail message to an MTA via a pipe." def __init__(self, cfg, repos, prefix_param): MailedOutput.__init__(self, cfg, repos, prefix_param) # figure out the command for delivery self.cmd = cfg.general.mail_command.split() def start(self, group, params): MailedOutput.start(self, group, params) ### gotta fix this. this is pretty specific to sendmail and qmail's ### mailwrapper program. should be able to use option param substitution cmd = self.cmd + [ '-f', self.from_addr ] + self.to_addrs # construct the pipe for talking to the mailer self.pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, close_fds=sys.platform != "win32") self.write = self.pipe.stdin.write # start writing out the mail message self.write(self.mail_headers(group, params)) def finish(self): # signal that we're done sending content self.pipe.stdin.close() # wait to avoid zombies self.pipe.wait() class Messenger: def __init__(self, pool, cfg, repos, prefix_param): self.pool = pool self.cfg = cfg self.repos = repos if cfg.is_set('general.mail_command'): cls = PipeOutput elif cfg.is_set('general.smtp_hostname'): cls = SMTPOutput else: cls = StandardOutput self.output = cls(cfg, repos, prefix_param) class Commit(Messenger): def __init__(self, pool, cfg, repos): Messenger.__init__(self, pool, cfg, repos, 'commit_subject_prefix') # get all the changes and sort by path editor = svn.repos.ChangeCollector(repos.fs_ptr, repos.root_this, \ self.pool) e_ptr, e_baton = svn.delta.make_editor(editor, self.pool) svn.repos.replay2(repos.root_this, "", svn.core.SVN_INVALID_REVNUM, 1, e_ptr, e_baton, None, self.pool) self.changelist = sorted(editor.get_changes().items()) log = repos.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG) or '' # collect the set of groups and the unique sets of params for the options self.groups = { } for path, change in self.changelist: for (group, params) in self.cfg.which_groups(path, log): # turn the params into a hashable object and stash it away param_list = sorted(params.items()) # collect the set of paths belonging to this group if (group, tuple(param_list)) in self.groups: old_param, paths = self.groups[group, tuple(param_list)] else: paths = { } paths[path] = None self.groups[group, tuple(param_list)] = (params, paths) # figure out the changed directories dirs = { } for path, change in self.changelist: if change.item_kind == svn.core.svn_node_dir: dirs[path] = None else: idx = path.rfind('/') if idx == -1: dirs[''] = None else: dirs[path[:idx]] = None dirlist = list(dirs.keys()) commondir, dirlist = get_commondir(dirlist) # compose the basic subject line. later, we can prefix it. dirlist.sort() dirlist = ' '.join(dirlist) if commondir: self.output.subject = 'r%d - in %s: %s' % (repos.rev, commondir, dirlist) else: self.output.subject = 'r%d - %s' % (repos.rev, dirlist) def generate(self): "Generate email for the various groups and option-params." ### the groups need to be further compressed. if the headers and ### body are the same across groups, then we can have multiple To: ### addresses. SMTPOutput holds the entire message body in memory, ### so if the body doesn't change, then it can be sent N times ### rather than rebuilding it each time. subpool = svn.core.svn_pool_create(self.pool) # build a renderer, tied to our output stream renderer = TextCommitRenderer(self.output) for (group, param_tuple), (params, paths) in self.groups.items(): self.output.start(group, params) # generate the content for this group and set of params generate_content(renderer, self.cfg, self.repos, self.changelist, group, params, paths, subpool) self.output.finish() svn.core.svn_pool_clear(subpool) svn.core.svn_pool_destroy(subpool) class PropChange(Messenger): def __init__(self, pool, cfg, repos, author, propname, action): Messenger.__init__(self, pool, cfg, repos, 'propchange_subject_prefix') self.author = author self.propname = propname self.action = action # collect the set of groups and the unique sets of params for the options self.groups = { } for (group, params) in self.cfg.which_groups('', None): # turn the params into a hashable object and stash it away param_list = sorted(params.items()) self.groups[group, tuple(param_list)] = params self.output.subject = 'r%d - %s' % (repos.rev, propname) def generate(self): actions = { 'A': 'added', 'M': 'modified', 'D': 'deleted' } for (group, param_tuple), params in self.groups.items(): self.output.start(group, params) self.output.write('Author: %s\n' 'Revision: %s\n' 'Property Name: %s\n' 'Action: %s\n' '\n' % (self.author, self.repos.rev, self.propname, actions.get(self.action, 'Unknown (\'%s\')' \ % self.action))) if self.action == 'A' or self.action not in actions: self.output.write('Property value:\n') propvalue = self.repos.get_rev_prop(self.propname) self.output.write(propvalue) elif self.action == 'M': self.output.write('Property diff:\n') tempfile1 = tempfile.NamedTemporaryFile() tempfile1.write(sys.stdin.read()) tempfile1.flush() tempfile2 = tempfile.NamedTemporaryFile() tempfile2.write(self.repos.get_rev_prop(self.propname)) tempfile2.flush() self.output.run(self.cfg.get_diff_cmd(group, { 'label_from' : 'old property value', 'label_to' : 'new property value', 'from' : tempfile1.name, 'to' : tempfile2.name, })) self.output.finish() def get_commondir(dirlist): """Figure out the common portion/parent (commondir) of all the paths in DIRLIST and return a tuple consisting of commondir, dirlist. If a commondir is found, the dirlist returned is rooted in that commondir. If no commondir is found, dirlist is returned unchanged, and commondir is the empty string.""" if len(dirlist) < 2 or '/' in dirlist: commondir = '' newdirs = dirlist else: common = dirlist[0].split('/') for j in range(1, len(dirlist)): d = dirlist[j] parts = d.split('/') for i in range(len(common)): if i == len(parts) or common[i] != parts[i]: del common[i:] break commondir = '/'.join(common) if commondir: # strip the common portion from each directory l = len(commondir) + 1 newdirs = [ ] for d in dirlist: if d == commondir: newdirs.append('.') else: newdirs.append(d[l:]) else: # nothing in common, so reset the list of directories newdirs = dirlist return commondir, newdirs class Lock(Messenger): def __init__(self, pool, cfg, repos, author, do_lock): self.author = author self.do_lock = do_lock Messenger.__init__(self, pool, cfg, repos, (do_lock and 'lock_subject_prefix' or 'unlock_subject_prefix')) # read all the locked paths from STDIN and strip off the trailing newlines self.dirlist = [x.rstrip() for x in sys.stdin.readlines()] # collect the set of groups and the unique sets of params for the options self.groups = { } for path in self.dirlist: for (group, params) in self.cfg.which_groups(path, None): # turn the params into a hashable object and stash it away param_list = sorted(params.items()) # collect the set of paths belonging to this group if (group, tuple(param_list)) in self.groups: old_param, paths = self.groups[group, tuple(param_list)] else: paths = { } paths[path] = None self.groups[group, tuple(param_list)] = (params, paths) commondir, dirlist = get_commondir(self.dirlist) # compose the basic subject line. later, we can prefix it. dirlist.sort() dirlist = ' '.join(dirlist) if commondir: self.output.subject = '%s: %s' % (commondir, dirlist) else: self.output.subject = '%s' % (dirlist) # The lock comment is the same for all paths, so we can just pull # the comment for the first path in the dirlist and cache it. self.lock = svn.fs.svn_fs_get_lock(self.repos.fs_ptr, self.dirlist[0], self.pool) def generate(self): for (group, param_tuple), (params, paths) in self.groups.items(): self.output.start(group, params) self.output.write('Author: %s\n' '%s paths:\n' % (self.author, self.do_lock and 'Locked' or 'Unlocked')) self.dirlist.sort() for dir in self.dirlist: self.output.write(' %s\n\n' % dir) if self.do_lock: self.output.write('Comment:\n%s\n' % (self.lock.comment or '')) self.output.finish() class DiffSelections: def __init__(self, cfg, group, params): self.add = False self.copy = False self.delete = False self.modify = False gen_diffs = cfg.get('generate_diffs', group, params) ### Do a little dance for deprecated options. Note that even if you ### don't have an option anywhere in your configuration file, it ### still gets returned as non-None. if len(gen_diffs): list = gen_diffs.split(" ") for item in list: if item == 'add': self.add = True if item == 'copy': self.copy = True if item == 'delete': self.delete = True if item == 'modify': self.modify = True else: self.add = True self.copy = True self.delete = True self.modify = True ### These options are deprecated suppress = cfg.get('suppress_deletes', group, params) if suppress == 'yes': self.delete = False suppress = cfg.get('suppress_adds', group, params) if suppress == 'yes': self.add = False class DiffURLSelections: def __init__(self, cfg, group, params): self.cfg = cfg self.group = group self.params = params def _get_url(self, action, repos_rev, change): # The parameters for the URLs generation need to be placed in the # parameters for the configuration module, otherwise we may get # KeyError exceptions. params = self.params.copy() params['path'] = change.path and urllib_parse_quote(change.path) or None params['base_path'] = change.base_path and urllib_parse_quote(change.base_path) \ or None params['rev'] = repos_rev params['base_rev'] = change.base_rev return self.cfg.get("diff_%s_url" % action, self.group, params) def get_add_url(self, repos_rev, change): return self._get_url('add', repos_rev, change) def get_copy_url(self, repos_rev, change): return self._get_url('copy', repos_rev, change) def get_delete_url(self, repos_rev, change): return self._get_url('delete', repos_rev, change) def get_modify_url(self, repos_rev, change): return self._get_url('modify', repos_rev, change) def generate_content(renderer, cfg, repos, changelist, group, params, paths, pool): svndate = repos.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE) ### pick a different date format? date = time.ctime(svn.core.secs_from_timestr(svndate, pool)) diffsels = DiffSelections(cfg, group, params) diffurls = DiffURLSelections(cfg, group, params) show_nonmatching_paths = cfg.get('show_nonmatching_paths', group, params) \ or 'yes' params_with_rev = params.copy() params_with_rev['rev'] = repos.rev commit_url = cfg.get('commit_url', group, params_with_rev) # figure out the lists of changes outside the selected path-space other_added_data = other_replaced_data = other_deleted_data = \ other_modified_data = [ ] if len(paths) != len(changelist) and show_nonmatching_paths != 'no': other_added_data = generate_list('A', changelist, paths, False) other_replaced_data = generate_list('R', changelist, paths, False) other_deleted_data = generate_list('D', changelist, paths, False) other_modified_data = generate_list('M', changelist, paths, False) if len(paths) != len(changelist) and show_nonmatching_paths == 'yes': other_diffs = DiffGenerator(changelist, paths, False, cfg, repos, date, group, params, diffsels, diffurls, pool) else: other_diffs = None data = _data( author=repos.author, date=date, rev=repos.rev, log=repos.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG) or '', commit_url=commit_url, added_data=generate_list('A', changelist, paths, True), replaced_data=generate_list('R', changelist, paths, True), deleted_data=generate_list('D', changelist, paths, True), modified_data=generate_list('M', changelist, paths, True), show_nonmatching_paths=show_nonmatching_paths, other_added_data=other_added_data, other_replaced_data=other_replaced_data, other_deleted_data=other_deleted_data, other_modified_data=other_modified_data, diffs=DiffGenerator(changelist, paths, True, cfg, repos, date, group, params, diffsels, diffurls, pool), other_diffs=other_diffs, ) renderer.render(data) def generate_list(changekind, changelist, paths, in_paths): if changekind == 'A': selection = lambda change: change.action == svn.repos.CHANGE_ACTION_ADD elif changekind == 'R': selection = lambda change: change.action == svn.repos.CHANGE_ACTION_REPLACE elif changekind == 'D': selection = lambda change: change.action == svn.repos.CHANGE_ACTION_DELETE elif changekind == 'M': selection = lambda change: change.action == svn.repos.CHANGE_ACTION_MODIFY items = [ ] for path, change in changelist: if selection(change) and (path in paths) == in_paths: item = _data( path=path, is_dir=change.item_kind == svn.core.svn_node_dir, props_changed=change.prop_changes, text_changed=change.text_changed, copied=(change.action == svn.repos.CHANGE_ACTION_ADD \ or change.action == svn.repos.CHANGE_ACTION_REPLACE) \ and change.base_path, base_path=remove_leading_slashes(change.base_path), base_rev=change.base_rev, ) items.append(item) return items class DiffGenerator: "This is a generator-like object returning DiffContent objects." def __init__(self, changelist, paths, in_paths, cfg, repos, date, group, params, diffsels, diffurls, pool): self.changelist = changelist self.paths = paths self.in_paths = in_paths self.cfg = cfg self.repos = repos self.date = date self.group = group self.params = params self.diffsels = diffsels self.diffurls = diffurls self.pool = pool self.diff = self.diff_url = None self.idx = 0 def __nonzero__(self): # we always have some items return True def __getitem__(self, idx): while True: if self.idx == len(self.changelist): raise IndexError path, change = self.changelist[self.idx] self.idx = self.idx + 1 diff = diff_url = None kind = None label1 = None label2 = None src_fname = None dst_fname = None binary = None singular = None content = None # just skip directories. they have no diffs. if change.item_kind == svn.core.svn_node_dir: continue # is this change in (or out of) the set of matched paths? if (path in self.paths) != self.in_paths: continue if change.base_rev != -1: svndate = self.repos.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE, change.base_rev) ### pick a different date format? base_date = time.ctime(svn.core.secs_from_timestr(svndate, self.pool)) else: base_date = '' # figure out if/how to generate a diff base_path = remove_leading_slashes(change.base_path) if change.action == svn.repos.CHANGE_ACTION_DELETE: # it was delete. kind = 'D' # get the diff url, if any is specified diff_url = self.diffurls.get_delete_url(self.repos.rev, change) # show the diff? if self.diffsels.delete: diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), base_path, None, None, self.pool) label1 = '%s\t%s\t(r%s)' % (base_path, self.date, change.base_rev) label2 = '/dev/null\t00:00:00 1970\t(deleted)' singular = True elif change.action == svn.repos.CHANGE_ACTION_ADD \ or change.action == svn.repos.CHANGE_ACTION_REPLACE: if base_path and (change.base_rev != -1): # any diff of interest? if change.text_changed: # this file was copied and modified. kind = 'W' # get the diff url, if any is specified diff_url = self.diffurls.get_copy_url(self.repos.rev, change) # show the diff? if self.diffsels.modify: diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), base_path, self.repos.root_this, change.path, self.pool) label1 = '%s\t%s\t(r%s, copy source)' \ % (base_path, base_date, change.base_rev) label2 = '%s\t%s\t(r%s)' \ % (change.path, self.date, self.repos.rev) singular = False else: # this file was copied. kind = 'C' if self.diffsels.copy: diff = svn.fs.FileDiff(None, None, self.repos.root_this, change.path, self.pool) label1 = '/dev/null\t00:00:00 1970\t' \ '(empty, because file is newly added)' label2 = '%s\t%s\t(r%s, copy of r%s, %s)' \ % (change.path, self.date, self.repos.rev, \ change.base_rev, base_path) singular = False else: # the file was added. kind = 'A' # get the diff url, if any is specified diff_url = self.diffurls.get_add_url(self.repos.rev, change) # show the diff? if self.diffsels.add: diff = svn.fs.FileDiff(None, None, self.repos.root_this, change.path, self.pool) label1 = '/dev/null\t00:00:00 1970\t' \ '(empty, because file is newly added)' label2 = '%s\t%s\t(r%s)' \ % (change.path, self.date, self.repos.rev) singular = True elif not change.text_changed: # the text didn't change, so nothing to show. continue else: # a simple modification. kind = 'M' # get the diff url, if any is specified diff_url = self.diffurls.get_modify_url(self.repos.rev, change) # show the diff? if self.diffsels.modify: diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), base_path, self.repos.root_this, change.path, self.pool) label1 = '%s\t%s\t(r%s)' \ % (base_path, base_date, change.base_rev) label2 = '%s\t%s\t(r%s)' \ % (change.path, self.date, self.repos.rev) singular = False if diff: binary = diff.either_binary() if binary: content = src_fname = dst_fname = None else: src_fname, dst_fname = diff.get_files() try: content = DiffContent(self.cfg.get_diff_cmd(self.group, { 'label_from' : label1, 'label_to' : label2, 'from' : src_fname, 'to' : dst_fname, })) except OSError: # diff command does not exist, try difflib.unified_diff() content = DifflibDiffContent(label1, label2, src_fname, dst_fname) # return a data item for this diff return _data( path=change.path, base_path=base_path, base_rev=change.base_rev, diff=diff, diff_url=diff_url, kind=kind, label_from=label1, label_to=label2, from_fname=src_fname, to_fname=dst_fname, binary=binary, singular=singular, content=content, ) def _classify_diff_line(line, seen_change): # classify the type of line. first = line[:1] ltype = '' if first == '@': seen_change = True ltype = 'H' elif first == '-': if seen_change: ltype = 'D' else: ltype = 'F' elif first == '+': if seen_change: ltype = 'A' else: ltype = 'T' elif first == ' ': ltype = 'C' else: ltype = 'U' if line[-2] == '\r': line=line[0:-2] + '\n' # remove carriage return return line, ltype, seen_change class DiffContent: "This is a generator-like object returning annotated lines of a diff." def __init__(self, cmd): self.seen_change = False # By default we choose to incorporate child stderr into the output self.pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=sys.platform != "win32") def __nonzero__(self): # we always have some items return True def __getitem__(self, idx): if self.pipe is None: raise IndexError line = self.pipe.stdout.readline() if not line: # wait on the child so we don't end up with a billion zombies self.pipe.wait() self.pipe = None raise IndexError line, ltype, self.seen_change = _classify_diff_line(line, self.seen_change) return _data( raw=line, text=line[1:-1], # remove indicator and newline type=ltype, ) class DifflibDiffContent(): "This is a generator-like object returning annotated lines of a diff." def __init__(self, label_from, label_to, from_file, to_file): import difflib self.seen_change = False fromlines = open(from_file, 'U').readlines() tolines = open(to_file, 'U').readlines() self.diff = difflib.unified_diff(fromlines, tolines, label_from, label_to) def __nonzero__(self): # we always have some items return True def __getitem__(self, idx): try: line = self.diff.next() except StopIteration: raise IndexError line, ltype, self.seen_change = _classify_diff_line(line, self.seen_change) return _data( raw=line, text=line[1:-1], # remove indicator and newline type=ltype, ) class TextCommitRenderer: "This class will render the commit mail in plain text." def __init__(self, output): self.output = output def render(self, data): "Render the commit defined by 'data'." w = self.output.write w('Author: %s\nDate: %s\nNew Revision: %s\n' % (data.author, data.date, data.rev)) if data.commit_url: w('URL: %s\n\n' % data.commit_url) else: w('\n') w('Log:\n%s\n\n' % data.log.strip()) # print summary sections self._render_list('Added', data.added_data) self._render_list('Replaced', data.replaced_data) self._render_list('Deleted', data.deleted_data) self._render_list('Modified', data.modified_data) if data.other_added_data or data.other_replaced_data \ or data.other_deleted_data or data.other_modified_data: if data.show_nonmatching_paths: w('\nChanges in other areas also in this revision:\n') self._render_list('Added', data.other_added_data) self._render_list('Replaced', data.other_replaced_data) self._render_list('Deleted', data.other_deleted_data) self._render_list('Modified', data.other_modified_data) else: w('and changes in other areas\n') self._render_diffs(data.diffs, '') if data.other_diffs: self._render_diffs(data.other_diffs, '\nDiffs of changes in other areas also' ' in this revision:\n') def _render_list(self, header, data_list): if not data_list: return w = self.output.write w(header + ':\n') for d in data_list: if d.is_dir: is_dir = '/' else: is_dir = '' if d.props_changed: if d.text_changed: props = ' (contents, props changed)' else: props = ' (props changed)' else: props = '' w(' %s%s%s\n' % (d.path, is_dir, props)) if d.copied: if is_dir: text = '' elif d.text_changed: text = ', changed' else: text = ' unchanged' w(' - copied%s from r%d, %s%s\n' % (text, d.base_rev, d.base_path, is_dir)) def _render_diffs(self, diffs, section_header): """Render diffs. Write the SECTION_HEADER if there are actually any diffs to render.""" if not diffs: return w = self.output.write section_header_printed = False for diff in diffs: if not diff.diff and not diff.diff_url: continue if not section_header_printed: w(section_header) section_header_printed = True if diff.kind == 'D': w('\nDeleted: %s\n' % diff.base_path) elif diff.kind == 'A': w('\nAdded: %s\n' % diff.path) elif diff.kind == 'C': w('\nCopied: %s (from r%d, %s)\n' % (diff.path, diff.base_rev, diff.base_path)) elif diff.kind == 'W': w('\nCopied and modified: %s (from r%d, %s)\n' % (diff.path, diff.base_rev, diff.base_path)) else: # kind == 'M' w('\nModified: %s\n' % diff.path) if diff.diff_url: w('URL: %s\n' % diff.diff_url) if not diff.diff: continue w(SEPARATOR + '\n') if diff.binary: if diff.singular: w('Binary file. No diff available.\n') else: w('Binary file (source and/or target). No diff available.\n') continue for line in diff.content: w(line.raw) class Repository: "Hold roots and other information about the repository." def __init__(self, repos_dir, rev, pool): self.repos_dir = repos_dir self.rev = rev self.pool = pool self.repos_ptr = svn.repos.open(repos_dir, pool) self.fs_ptr = svn.repos.fs(self.repos_ptr) self.roots = { } self.root_this = self.get_root(rev) self.author = self.get_rev_prop(svn.core.SVN_PROP_REVISION_AUTHOR) def get_rev_prop(self, propname, rev = None): if not rev: rev = self.rev return svn.fs.revision_prop(self.fs_ptr, rev, propname, self.pool) def get_root(self, rev): try: return self.roots[rev] except KeyError: pass root = self.roots[rev] = svn.fs.revision_root(self.fs_ptr, rev, self.pool) return root class Config: # The predefined configuration sections. These are omitted from the # set of groups. _predefined = ('general', 'defaults', 'maps') def __init__(self, fname, repos, global_params): cp = configparser.ConfigParser() cp.read(fname) # record the (non-default) groups that we find self._groups = [ ] for section in cp.sections(): if not hasattr(self, section): section_ob = _sub_section() setattr(self, section, section_ob) if section not in self._predefined: self._groups.append(section) else: section_ob = getattr(self, section) for option in cp.options(section): # get the raw value -- we use the same format for *our* interpolation value = cp.get(section, option, raw=1) setattr(section_ob, option, value) # be compatible with old format config files if hasattr(self.general, 'diff') and not hasattr(self.defaults, 'diff'): self.defaults.diff = self.general.diff if not hasattr(self, 'maps'): self.maps = _sub_section() # these params are always available, although they may be overridden self._global_params = global_params.copy() # prepare maps. this may remove sections from consideration as a group. self._prep_maps() # process all the group sections. self._prep_groups(repos) def is_set(self, option): """Return None if the option is not set; otherwise, its value is returned. The option is specified as a dotted symbol, such as 'general.mail_command' """ ob = self for part in option.split('.'): if not hasattr(ob, part): return None ob = getattr(ob, part) return ob def get(self, option, group, params): "Get a config value with appropriate substitutions and value mapping." # find the right value value = None if group: sub = getattr(self, group) value = getattr(sub, option, None) if value is None: value = getattr(self.defaults, option, '') # parameterize it if params is not None: value = value % params # apply any mapper mapper = getattr(self.maps, option, None) if mapper is not None: value = mapper(value) # Apply any parameters that may now be available for # substitution that were not before the mapping. if value is not None and params is not None: value = value % params return value def get_diff_cmd(self, group, args): "Get a diff command as a list of argv elements." ### do some better splitting to enable quoting of spaces diff_cmd = self.get('diff', group, None).split() cmd = [ ] for part in diff_cmd: cmd.append(part % args) return cmd def _prep_maps(self): "Rewrite the [maps] options into callables that look up values." mapsections = [] for optname, mapvalue in vars(self.maps).items(): if mapvalue[:1] == '[': # a section is acting as a mapping sectname = mapvalue[1:-1] if not hasattr(self, sectname): raise UnknownMappingSection(sectname) # construct a lambda to look up the given value as an option name, # and return the option's value. if the option is not present, # then just return the value unchanged. setattr(self.maps, optname, lambda value, sect=getattr(self, sectname): getattr(sect, value.lower(), value)) # mark for removal when all optnames are done if sectname not in mapsections: mapsections.append(sectname) # elif test for other mapper types. possible examples: # dbm:filename.db # file:two-column-file.txt # ldap:some-query-spec # just craft a mapper function and insert it appropriately else: raise UnknownMappingSpec(mapvalue) # remove each mapping section from consideration as a group for sectname in mapsections: self._groups.remove(sectname) def _prep_groups(self, repos): self._group_re = [ ] repos_dir = os.path.abspath(repos.repos_dir) # compute the default repository-based parameters. start with some # basic parameters, then bring in the regex-based params. self._default_params = self._global_params try: match = re.match(self.defaults.for_repos, repos_dir) if match: self._default_params = self._default_params.copy() self._default_params.update(match.groupdict()) except AttributeError: # there is no self.defaults.for_repos pass # select the groups that apply to this repository for group in self._groups: sub = getattr(self, group) params = self._default_params if hasattr(sub, 'for_repos'): match = re.match(sub.for_repos, repos_dir) if not match: continue params = params.copy() params.update(match.groupdict()) # if a matching rule hasn't been given, then use the empty string # as it will match all paths for_paths = getattr(sub, 'for_paths', '') exclude_paths = getattr(sub, 'exclude_paths', None) if exclude_paths: exclude_paths_re = re.compile(exclude_paths) else: exclude_paths_re = None # check search_logmsg re search_logmsg = getattr(sub, 'search_logmsg', None) if search_logmsg is not None: search_logmsg_re = re.compile(search_logmsg) else: search_logmsg_re = None self._group_re.append((group, re.compile(for_paths), exclude_paths_re, params, search_logmsg_re)) # after all the groups are done, add in the default group try: self._group_re.append((None, re.compile(self.defaults.for_paths), None, self._default_params, None)) except AttributeError: # there is no self.defaults.for_paths pass def which_groups(self, path, logmsg): "Return the path's associated groups." groups = [] for group, pattern, exclude_pattern, repos_params, search_logmsg_re in self._group_re: match = pattern.match(path) if match: if exclude_pattern and exclude_pattern.match(path): continue params = repos_params.copy() params.update(match.groupdict()) if search_logmsg_re is None: groups.append((group, params)) else: if logmsg is None: logmsg = '' for match in search_logmsg_re.finditer(logmsg): # Add captured variables to (a copy of) params msg_params = params.copy() msg_params.update(match.groupdict()) groups.append((group, msg_params)) if not groups: groups.append((None, self._default_params)) return groups class _sub_section: pass class _data: "Helper class to define an attribute-based hunk o' data." def __init__(self, **kw): vars(self).update(kw) class MissingConfig(Exception): pass class UnknownMappingSection(Exception): pass class UnknownMappingSpec(Exception): pass class UnknownSubcommand(Exception): pass if __name__ == '__main__': def usage(): scriptname = os.path.basename(sys.argv[0]) sys.stderr.write( """USAGE: %s commit REPOS REVISION [CONFIG-FILE] %s propchange REPOS REVISION AUTHOR REVPROPNAME [CONFIG-FILE] %s propchange2 REPOS REVISION AUTHOR REVPROPNAME ACTION [CONFIG-FILE] %s lock REPOS AUTHOR [CONFIG-FILE] %s unlock REPOS AUTHOR [CONFIG-FILE] If no CONFIG-FILE is provided, the script will first search for a mailer.conf file in REPOS/conf/. Failing that, it will search the directory in which the script itself resides. ACTION was added as a fifth argument to the post-revprop-change hook in Subversion 1.2.0. Its value is one of 'A', 'M' or 'D' to indicate if the property was added, modified or deleted, respectively. """ % (scriptname, scriptname, scriptname, scriptname, scriptname)) sys.exit(1) # Command list: subcommand -> number of arguments expected (not including # the repository directory and config-file) cmd_list = {'commit' : 1, 'propchange' : 3, 'propchange2': 4, 'lock' : 1, 'unlock' : 1, } config_fname = None argc = len(sys.argv) if argc < 3: usage() cmd = sys.argv[1] repos_dir = svn.core.svn_path_canonicalize(sys.argv[2]) try: expected_args = cmd_list[cmd] except KeyError: usage() if argc < (expected_args + 3): usage() elif argc > expected_args + 4: usage() elif argc == (expected_args + 4): config_fname = sys.argv[expected_args + 3] # Settle on a config file location, and open it. if config_fname is None: # Default to REPOS-DIR/conf/mailer.conf. config_fname = os.path.join(repos_dir, 'conf', 'mailer.conf') if not os.path.exists(config_fname): # Okay. Look for 'mailer.conf' as a sibling of this script. config_fname = os.path.join(os.path.dirname(sys.argv[0]), 'mailer.conf') if not os.path.exists(config_fname): raise MissingConfig(config_fname) svn.core.run_app(main, cmd, config_fname, repos_dir, sys.argv[3:3+expected_args]) # ------------------------------------------------------------------------ # TODO # # * add configuration options # - each group defines delivery info: # o whether to set Reply-To and/or Mail-Followup-To # (btw: it is legal do set Reply-To since this is the originator of the # mail; i.e. different from MLMs that munge it) # - each group defines content construction: # o max size of diff before trimming # o max size of entire commit message before truncation # - per-repository configuration # o extra config living in repos # o optional, non-mail log file # o look up authors (username -> email; for the From: header) in a # file(s) or DBM # * get rid of global functions that should properly be class methods |
$ tree . ├── README.txt ├── conf │ ├── authz │ ├── hooks-env.tmpl │ ├── passwd │ └── svnserve.conf ├── db │ ├── current │ ├── format │ ├── fs-type │ ├── fsfs.conf │ ├── min-unpacked-rev │ ├── rep-cache.db │ ├── revprops │ │ └── 0 │ │ ├── 0 │ │ └── 1 │ ├── revs │ │ └── 0 │ │ ├── 0 │ │ └── 1 │ ├── transactions │ ├── txn-current │ ├── txn-current-lock │ ├── txn-protorevs │ ├── uuid │ └── write-lock ├── format ├── hooks │ ├── mailer.py │ ├── post-commit │ ├── post-commit.tmpl │ ├── post-lock.tmpl │ ├── post-revprop-change.tmpl │ ├── post-unlock.tmpl │ ├── pre-commit.tmpl │ ├── pre-lock.tmpl │ ├── pre-revprop-change.tmpl │ ├── pre-unlock.tmpl │ └── start-commit.tmpl ├── locks │ ├── db-logs.lock │ └── db.lock └── mailer.conf 10 directories, 34 files |
커밋하는데.. 먼가 걸렸나 드럽게 진행이 안되네
$ svn ci 추가 t2.c 파일 데이터 전송중 . |
일단 아마도?
$ ps -ef | grep py pi 22559 22558 0 14:37 pts/0 00:00:00 /usr/bin/python /home/pi/repos/hooks/mailer.py commit /home/pi/repos 2 /home/pi/repos/mailer.conf |
pstree 해보니 다음과 같이 구동이 되나본데..
├─sshd─┬─sshd───sshd───bash───svn───post-commit───mailer.py |
회사 메일이 SSL 쓰는 바람에 보안관련 문제인걸려나?
경고: 'post-commit' 훅이 실패했습니다 (분명하게 빠져나가지 않았습니다: apr_exit_why_e 는 2, 종료코드는 2) 출력: Traceback (most recent call last): File "/home/pi/repos/hooks/mailer.py", line 1448, in <module> sys.argv[3:3+expected_args]) File "/usr/lib/python2.7/dist-packages/svn/core.py", line 345, in run_app return func(application_pool, *args, **kw) File "/home/pi/repos/hooks/mailer.py", line 132, in main messenger.generate() File "/home/pi/repos/hooks/mailer.py", line 424, in generate self.output.finish() File "/home/pi/repos/hooks/mailer.py", line 280, in finish server = smtplib.SMTP(self.cfg.general.smtp_hostname) File "/usr/lib/python2.7/smtplib.py", line 256, in __init__ (code, msg) = self.connect(host, port) File "/usr/lib/python2.7/smtplib.py", line 316, in connect self.sock = self._get_socket(host, port, self.timeout) File "/usr/lib/python2.7/smtplib.py", line 291, in _get_socket return socket.create_connection((host, port), timeout) File "/usr/lib/python2.7/socket.py", line 562, in create_connection sock.connect(sa) File "/usr/lib/python2.7/socket.py", line 224, in meth return getattr(self._sock,name)(*args) KeyboardInterrupt svn: E200000: 커밋이 성공하였지만, 오류가 있습니다: svn: E200015: 커밋 후 리비전들을 갱신하는 도중 오류가 발생하였습니다: svn: E200015: 시그널 수신 svn: E200000: 커밋 메시지는 다음 파일에 저장되어 있으며, -F로 재사용 할 수 있습니다. : svn: E200000: '/home/pi/test/svn-commit.tmp' |
[링크 : http://sunnyan.tistory.com/m/4840]
음.. 일단 tls 어쩌구 넣고 하는데도 안되네 머가 문제일려나..
경고: post-commit 훅이 실패했습니다 ( 종료코드 1) 출력: Traceback (most recent call last): File "/home/pi/repos/hooks/mailer.py", line 1452, in <module> sys.argv[3:3+expected_args]) File "/usr/lib/python2.7/dist-packages/svn/core.py", line 345, in run_app return func(application_pool, *args, **kw) File "/home/pi/repos/hooks/mailer.py", line 132, in main messenger.generate() File "/home/pi/repos/hooks/mailer.py", line 428, in generate self.output.finish() File "/home/pi/repos/hooks/mailer.py", line 280, in finish server = smtplib.SMTP(self.cfg.general.smtp_hostname) File "/usr/lib/python2.7/smtplib.py", line 256, in __init__ (code, msg) = self.connect(host, port) File "/usr/lib/python2.7/smtplib.py", line 317, in connect (code, msg) = self.getreply() File "/usr/lib/python2.7/smtplib.py", line 368, in getreply raise SMTPServerDisconnected("Connection unexpectedly closed") smtplib.SMTPServerDisconnected: Connection unexpectedly closed |
[링크 : http://stackoverflow.com/questions/6980631/svn-notifications-via-gmail-smtp]
[링크 : http://sadomovalex.blogspot.com/2009/12/use-gmail-smtp-server-for-post-commit.html]
[링크 : http://pyrasis.com/blog/entry/SubversionMailerPyScriptForGmailSMTP] TLS 방식일 경우
+
[링크 : https://docs.python.org/3.5/library/smtplib.html]
[링크 : https://docs.python.org/3.5/library/smtplib.html#smtplib.SMTP.ehlo]
+
telnet 으로 helo domain.com 해보니 바로 끊어 버리네 머지???
망할 mailplug인가? (구글은 문제 없는데?!)
[링크 : http://jang8584.tistory.com/52]
그리고.. python으로 시도하면 접속도 안된다. 머지?
>>> server = smtplib.SMTP("smtp.mailplug.co.kr:465") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python2.7/smtplib.py", line 256, in __init__ (code, msg) = self.connect(host, port) File "/usr/lib/python2.7/smtplib.py", line 317, in connect (code, msg) = self.getreply() File "/usr/lib/python2.7/smtplib.py", line 368, in getreply raise SMTPServerDisconnected("Connection unexpectedly closed") smtplib.SMTPServerDisconnected: Connection unexpectedly closed |
+
wireshark로 보니까 그냥 패킷이네..
함수를 바꾸자 -_-
s = smtplib.SMTP_SSL('host:port')
[링크 : http://stackoverflow.com/questions/24672079/send-email-using-smtp-ssl-port-465]
mailer.py 수정
def finish(self): if self.cfg.is_set('general.smtp_use_ssl') and self.cfg.general.smtp_use_ssl.lower() == "true": server = smtplib.SMTP_SSL(self.cfg.general.smtp_hostname) else: server = smtplib.SMTP(self.cfg.general.smtp_hostname) if self.cfg.is_set('general.smtp_username'): server.login(self.cfg.general.smtp_username, self.cfg.general.smtp_password) server.sendmail(self.from_addr, self.to_addrs, self.buffer.getvalue()) server.quit() |
시놀로지에 하니까 안되네.. 췌 ㅠㅠ
+
mailer.py에 이런게 있는데.. 라이브러리가 존재하지 않네.. ㅠㅠ
# Import the Subversion Python bindings, making sure they meet our # minimum version requirements. try: import svn.fs import svn.delta import svn.repos import svn.core except ImportError: sys.stderr.write( "You need version %s or better of the Subversion Python bindings.\n" \ % ".".join([str(x) for x in _MIN_SVN_VERSION])) sys.exit(1) if _MIN_SVN_VERSION > [svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR, svn.core.SVN_VER_PATCH]: sys.stderr.write( "You need version %s or better of the Subversion Python bindings.\n" \ % ".".join([str(x) for x in _MIN_SVN_VERSION])) sys.exit(1) |
[링크 : http://stack.../you-need-version-1-5-0-or-better-of-the-subversion-python-bindings-while-using]
+
synology에서는 걍 포기하면 편해..
libsvn 부터 온갖 so들을 자꾸 요구해서 짜증..
라즈베리에서 복사하는것도 한두개지 어우 ㅠㅠ
'프로그램 사용 > Version Control' 카테고리의 다른 글
svn commit email - python / synology (0) | 2016.12.30 |
---|---|
svn diff 결과물 컬러로 보기 (0) | 2016.12.30 |
svn console에서 엔터 입력하기 (0) | 2016.11.08 |
svn add를 취소하기 (0) | 2016.11.04 |
synology svn+ssh 퍼미션 문제 (0) | 2016.10.09 |