구글로 하면 잘되나

다만, 구글에서 차단하지 않도록 "보안 수준이 낮은 앱"을 허용해야 한다.



커밋시 이메일은 이렇게 오긴 온다.


---

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들을 자꾸 요구해서 짜증..

라즈베리에서 복사하는것도 한두개지 어우 ㅠㅠ

Posted by 구차니