구글로 하면 잘되나

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



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


---

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 구차니

ubuntu 12.04 LTS / svn 1.6.17

테스트 완료


$ svn ci -m $'This is the first line\nThis is the second line' 

[링크 : http://serverfault.com/.../use-linefeed-or-carriage-return-in-...-message-from-the-command-li]

'프로그램 사용 > Version Control' 카테고리의 다른 글

svn diff 결과물 컬러로 보기  (0) 2016.12.30
svn commit시 email 알림  (0) 2016.12.29
svn add를 취소하기  (0) 2016.11.04
synology svn+ssh 퍼미션 문제  (0) 2016.10.09
svn://과 svn+ssh:// 경로 차이  (0) 2016.09.13
Posted by 구차니

으아 복잡해 -_-!!!

결론 : svn에서 복구 하려면 복사본 만들고 삭제해라!


cp -a ori back 

svn delete –force ori

svn revert ori

cp -a back ori 


[링크 : http://k44.kr/?p=2908]

Posted by 구차니

퍼미션 오류라길래 먼가 해서 봤더니..

정말로 퍼미션이 저렴하네...


$ svn list svn+ssh://minimonk@svn.host/volume1/svn/proj

minimonk@svn.host's password:

svn: E000013: Unable to connect to a repository at URL 'svn+ssh://minimonk@svn.host/volume1/svn/proj'

svn: E000013: Can't open file '/volume1/svn/proj/db/fs-type': Permission denied 


/volume1/svn/proj$ ll

total 36

drwxrwxrwx+  6 root root 4096 Oct  8 12:56 .

drwxrwxrwx+ 10 root root 4096 Oct  8 12:56 ..

drwxrwxrwx+  2 root root 4096 Oct  8 12:56 conf

d-----S---   6 root root 4096 Oct  8 13:39 db

-r--r--r--   1 root root    2 Oct  8 12:56 format

drwxrwxrwx+  2 root root 4096 Oct  8 12:56 hooks

drwxrwxrwx+  2 root root 4096 Oct  8 12:56 locks

-rwxrwxrwx+  1 root root  246 Oct  8 12:56 README.txt 


흐음.. 그러고 보니 other가 전부 rw+ 네.. 퍼미션 를 귀찮으니.. 777로 전부 해줘야하나...

$ svn list svn+ssh://minimonk@svn.host/volume1/svn/proj

minimonk@svn.host's password:

svn: E000013: Unable to connect to a repository at URL 'svn+ssh://minimonk@svn.host/volume1/svn/proj'

svn: E000013: Can't open file '/volume1/svn/proj/conf/svnserve.conf': Permission denied 


'프로그램 사용 > Version Control' 카테고리의 다른 글

svn console에서 엔터 입력하기  (0) 2016.11.08
svn add를 취소하기  (0) 2016.11.04
svn://과 svn+ssh:// 경로 차이  (0) 2016.09.13
tortoiseSVN에서 svn+ssh 사용하기  (0) 2016.07.31
svn+ssh 실패 -_-  (0) 2016.07.29
Posted by 구차니

시놀로지 사용중 

시놀로지에 접속해서 svn과 svn+ssh의 경로 차이점 테스트


svn:// 의 경우에는 저장소 경로만 입력

svn+ssh:// 의 경우에는 저장소가 위치한 절대경로 부터 저장소 경로 까지 입력


$ svn list svn://localhost/project

$ svn list svn+ssh://localhost/volume1/svn/project 


외부에서 접속시 포트가 22번이 아니면 아래와 같이 에러가 발생한다.

$ svn list svn+ssh://minimonk@svn.minimonk.net:2222/volume1/svn/project

svn: E210002: Unable to connect to a repository at URL 'svn+ssh://minimonk@svn.minimonk.net:2222/volume1/svn/project'

svn: E210002: SSH 접속 문제를 더 잘 디버깅하기 위해서는, 서브버전 환경설정 파일의 [tunnels] 섹션에서 ssh의 -q 옵션을 제거 하세요.

svn: E210002: 네트워크 접속이 예기치 않게 종료되었습니다


개인별로 설정시에는 아래 경로의 파일에서 포트를 지정해 주면되는데.. 

매번 다른 서버 접속시에는 수동으로 해주어야 하나?

$ vi ~/.subversion/config

[tunnels]

ssh = ssh -p 2222 

아무튼 저렇게 설정하고 나면 다음과 같이 평범(?)하게 접속이 가능하다.

$ svn list svn+ssh://minimonk@svn.minimonk.net/volume1/svn/project 


[링크 : http://unix.stackexchange.com/.../how-to-configure-svn-ssh-with-ssh-on-non-standard-port]

'프로그램 사용 > Version Control' 카테고리의 다른 글

svn add를 취소하기  (0) 2016.11.04
synology svn+ssh 퍼미션 문제  (0) 2016.10.09
tortoiseSVN에서 svn+ssh 사용하기  (0) 2016.07.31
svn+ssh 실패 -_-  (0) 2016.07.29
svn list 에러 generic failure  (0) 2016.06.23
Posted by 구차니

Step 1. puttygen을 이용하여 public key , private key 생성 및 저장

Step 2. puttygen의 UI에 출력된 public key를 복사해서 ~/.ssh/authorized_keys 에 붙여넣고

Step 3. pagent에서 생성된 private key를 Add key 해서 등록

Step 4. tortoiseSVN에서 svn+ssh://userid@serverip/repopath 를 통해 접속


---


로컬 저장소 만들어서 테스트

$ svnadmin create repos/

$ svn co svn+ssh://localhost/home/minimonk/repos svn

$ cd svn

$ cp ../src/* ./

$ svn add *

$ svn ci

$ cd ..

$ svn list svn+ssh://localhost/home/minimonk/repos

minimonk@localhost's password:

a.out

tt.c 


라즈베리 파이에서 위에 녀석으로 접속 테스트

$ sudo apt-get install subversion

$ svn co svn+ssh://minimonk@192.168.219.201/home/minimonk/repos svn

minimonk@192.168.219.201's password:

A    svn/a.out

A    svn/tt.c

체크아웃된 리비전 1.

$ ll svn

합계 16

-rwxr-xr-x 1 pi pi 8528  7월 31 18:06 a.out

-rw-r--r-- 1 pi pi   70  7월 31 18:06 tt.c

$ svn list svn+ssh://minimonk@192.168.219.201/home/minimonk/repos

minimonk@192.168.219.201's password:

a.out

tt.c


흐음.. 여기까진 문제가 없는데...

tortoiseSVN이 문제였던 건가?


회사에서 쓰던건 1.8.11 인가 그런데, 일단 최신버전으로 시도



[링크 : https://sourceforge.net/projects/tortoisesvn/files/?source=navbar]



일단.. svn+ssh 실패 -_-



ssh 인증키로 자동 로그인 시키려고 하는데

하라는 대로 키 생성해서 자동 로그인 가능하도록 수정해 봅시다

~/.ssh/authorized_keys 에는

생성된 public key 파일 내용이 아닌 아래에 ssh-rsa 라고 써있는 부분을 복사해서 붙여 넣어야 한다.

만약 닫았어도, private key 파일을 Load해서 다시 불러올수 있다.


private key 등록하면


안되잖아!!!! ㅠㅠ (인증키에 public.key 파일 붙여 넣어서 실패 ㅋㅋ)


올 된다!


[링크 : https://tortoisesvn.net/ssh_howto.html]


근데 저걸 설정하고 나서 시도해도 안되는건 여전 -_-

아놔...



+

핵심(?)은  pagent ... private key를 얘로 등록해줘야지 tortoiseplink를 통해서

별도의 인증절차 없이 진행된다. -_-



[링크 : http://blog.naver.com/sungback/90012397207]


테스트를 해보니..

tortoiseSVN에서 ssh+svn을 하려면 무조건 암호 없이 로그인 할 수 있도록 해야 하는 듯?

그래서 키 생성하고..  pagent를 통해서 적용해 주어야 하고...

크아아앙!!!

'프로그램 사용 > Version Control' 카테고리의 다른 글

synology svn+ssh 퍼미션 문제  (0) 2016.10.09
svn://과 svn+ssh:// 경로 차이  (0) 2016.09.13
svn+ssh 실패 -_-  (0) 2016.07.29
svn list 에러 generic failure  (0) 2016.06.23
svn 콘솔 에디터(주석)  (0) 2016.06.21
Posted by 구차니

흐음.. sshd_config까지 유저별로 건드려줘야하나?

고민되네...


아무튼 현재 상태

tortoiseSVN 에서 svn:// 을 svn+ssh:// 로 바꾸고 로그인 시도 하였으나

로그인 까지만 되고 그 이후에 로그인 무한반복...


[링크 : https://www.digitalocean.com/community/questions/how-do-i-set-svn-repository-to-private]

'프로그램 사용 > Version Control' 카테고리의 다른 글

svn://과 svn+ssh:// 경로 차이  (0) 2016.09.13
tortoiseSVN에서 svn+ssh 사용하기  (0) 2016.07.31
svn list 에러 generic failure  (0) 2016.06.23
svn 콘솔 에디터(주석)  (0) 2016.06.21
svn relocate / ubuntu  (0) 2016.06.21
Posted by 구차니

이번에 호스트 네임을 변경했더니 별별 이상한 일이 발생..

아무튼.. /etc/hostname 만 바꿀게 아니라 /etc/hosts에서도 바꿔줘야 하는구나...


$ svn list svn://192.168.10.12/repos -v

svn: generic failure



$ sudo vi /etc/hosts

127.0.0.1 hostname


[링크 : http://stackoverflow.com/questions/8634466/svn-generic-failure]

'프로그램 사용 > Version Control' 카테고리의 다른 글

tortoiseSVN에서 svn+ssh 사용하기  (0) 2016.07.31
svn+ssh 실패 -_-  (0) 2016.07.29
svn 콘솔 에디터(주석)  (0) 2016.06.21
svn relocate / ubuntu  (0) 2016.06.21
svn merge... 두근두근  (6) 2016.02.17
Posted by 구차니

ubuntu 12.04 LTS의 경우

기본 값이 비어 있어서 편집기가 제대로 실행하질 못해서 바보 되는 듯..

기본 값은 editor로 되어 있는데.. ubuntu 특성인진 모르겠으나.

svn에서 무언가 엉겨서 nano나 editor로 연결되면서 부터 바보가 됨

vi로 설정해주어도 화면 출력이 이상하게 엉겨서 먹통이 되어버리니 주의..


$ cat ~/.bashrc

export SVN_EDITOR=/usr/bin/vim


$ cat $HOME/.subversion/config

### This file configures various client-side behaviors.

###

### The commented-out examples below are intended to demonstrate

### how to use this file.


### Section for authentication and authorization customizations.

[auth]

### Set password stores used by Subversion. They should be

### delimited by spaces or commas. The order of values determines

### the order in which password stores are used.

### Valid password stores:

###   gnome-keyring        (Unix-like systems)

###   kwallet              (Unix-like systems)

###   keychain             (Mac OS X)

###   windows-cryptoapi    (Windows)

# password-stores = gnome-keyring,kwallet

###

### Set KWallet wallet used by Subversion. If empty or unset,

### then the default network wallet will be used.

# kwallet-wallet =

###

### Include PID (Process ID) in Subversion application name when

### using KWallet. It defaults to 'no'.

# kwallet-svn-application-name-with-pid = yes

###

### The rest of this section in this file has been deprecated.

### Both 'store-passwords' and 'store-auth-creds' can now be

### specified in the 'servers' file in your config directory.

### Anything specified in this section is overridden by settings

### specified in the 'servers' file.

###

### Set store-passwords to 'no' to avoid storing passwords in the

### auth/ area of your config directory.  It defaults to 'yes',

### but Subversion will never save your password to disk in

### plaintext unless you tell it to (see the 'servers' file).

### Note that this option only prevents saving of *new* passwords;

### it doesn't invalidate existing passwords.  (To do that, remove

### the cache files by hand as described in the Subversion book.)

# store-passwords = no

### Set store-auth-creds to 'no' to avoid storing any subversion

### credentials in the auth/ area of your config directory.

### It defaults to 'yes'.  Note that this option only prevents

### saving of *new* credentials;  it doesn't invalidate existing

### caches.  (To do that, remove the cache files by hand.)

# store-auth-creds = no


### Section for configuring external helper applications.

[helpers]

### Set editor-cmd to the command used to invoke your text editor.

###   This will override the environment variables that Subversion

###   examines by default to find this information ($EDITOR,

###   et al).

# editor-cmd = editor (vi, emacs, notepad, etc.)

### Set diff-cmd to the absolute path of your 'diff' program.

###   This will override the compile-time default, which is to use

###   Subversion's internal diff implementation.

# diff-cmd = diff_program (diff, gdiff, etc.)

### Set diff3-cmd to the absolute path of your 'diff3' program.

###   This will override the compile-time default, which is to use

###   Subversion's internal diff3 implementation.

# diff3-cmd = diff3_program (diff3, gdiff3, etc.)

### Set diff3-has-program-arg to 'yes' if your 'diff3' program

###   accepts the '--diff-program' option.

# diff3-has-program-arg = [yes | no]

### Set merge-tool-cmd to the command used to invoke your external

### merging tool of choice. Subversion will pass 4 arguments to

### the specified command: base theirs mine merged

# merge-tool-cmd = merge_command


### Section for configuring tunnel agents.

[tunnels]

### Configure svn protocol tunnel schemes here.  By default, only

### the 'ssh' scheme is defined.  You can define other schemes to

### be used with 'svn+scheme://hostname/path' URLs.  A scheme

### definition is simply a command, optionally prefixed by an

### environment variable name which can override the command if it

### is defined.  The command (or environment variable) may contain

### arguments, using standard shell quoting for arguments with

### spaces.  The command will be invoked as:

###   <command> <hostname> svnserve -t

### (If the URL includes a username, then the hostname will be

### passed to the tunnel agent as <user>@<hostname>.)  If the

### built-in ssh scheme were not predefined, it could be defined

### as:

# ssh = $SVN_SSH ssh -q -o ControlMaster=no

### If you wanted to define a new 'rsh' scheme, to be used with

### 'svn+rsh:' URLs, you could do so as follows:

# rsh = rsh

### Or, if you wanted to specify a full path and arguments:

# rsh = /path/to/rsh -l myusername

### On Windows, if you are specifying a full path to a command,

### use a forward slash (/) or a paired backslash (\\) as the

### path separator.  A single backslash will be treated as an

### escape for the following character.


### Section for configuring miscelleneous Subversion options.

[miscellany]

### Set global-ignores to a set of whitespace-delimited globs

### which Subversion will ignore in its 'status' output, and

### while importing or adding files and directories.

### '*' matches leading dots, e.g. '*.rej' matches '.foo.rej'.

# global-ignores = *.o *.lo *.la *.al .libs *.so *.so.[0-9]* *.a *.pyc *.pyo

#   *.rej *~ #*# .#* .*.swp .DS_Store

### Set log-encoding to the default encoding for log messages

# log-encoding = latin1

### Set use-commit-times to make checkout/update/switch/revert

### put last-committed timestamps on every file touched.

# use-commit-times = yes

### Set no-unlock to prevent 'svn commit' from automatically

### releasing locks on files.

# no-unlock = yes

### Set mime-types-file to a MIME type registry file, used to

### provide hints to Subversion's MIME type auto-detection

### algorithm.

# mime-types-file = /path/to/mime.types

### Set preserved-conflict-file-exts to a whitespace-delimited

### list of patterns matching file extensions which should be

### preserved in generated conflict file names.  By default,

### conflict files use custom extensions.

# preserved-conflict-file-exts = doc ppt xls od?

### Set enable-auto-props to 'yes' to enable automatic properties

### for 'svn add' and 'svn import', it defaults to 'no'.

### Automatic properties are defined in the section 'auto-props'.

# enable-auto-props = yes

### Set interactive-conflicts to 'no' to disable interactive

### conflict resolution prompting.  It defaults to 'yes'.

# interactive-conflicts = no


### Section for configuring automatic properties.

[auto-props]

### The format of the entries is:

###   file-name-pattern = propname[=value][;propname[=value]...]

### The file-name-pattern can contain wildcards (such as '*' and

### '?').  All entries which match (case-insensitively) will be

### applied to the file.  Note that auto-props functionality

### must be enabled, which is typically done by setting the

### 'enable-auto-props' option.

# *.c = svn:eol-style=native

# *.cpp = svn:eol-style=native

# *.h = svn:keywords=Author Date Id Rev URL;svn:eol-style=native

# *.dsp = svn:eol-style=CRLF

# *.dsw = svn:eol-style=CRLF

# *.sh = svn:eol-style=native;svn:executable

# *.txt = svn:eol-style=native;svn:keywords=Author Date Id Rev URL;

# *.png = svn:mime-type=image/png

# *.jpg = svn:mime-type=image/jpeg

# Makefile = svn:eol-style=native 


[링크 : http://stackoverflow.com/questions/6506653/change-svn-message-editor]



+

SVN_EDITOR=gedit 하거나 하면 우회는 가능하지만..

아무튼 STDOUT tty 문제로 보이긴 한데.. 해결책은 없는건가..

svn과 검색을 해도 도통안나옴..


Vim: Warning: Output is not to a terminal

[링크 : http://stackoverflow.com/questions/19290604/how-to-set-svnignore-without-opening-an-editor]

[링크 : http://stackoverflow.com/questions/1690274/how-do-i-launch-an-editor-from-a-shell-script]


[링크 : http://stackoverflow.com/../getting-error-trying-to-commit-using-subversion-on-mac-os-x]

'프로그램 사용 > Version Control' 카테고리의 다른 글

svn+ssh 실패 -_-  (0) 2016.07.29
svn list 에러 generic failure  (0) 2016.06.23
svn relocate / ubuntu  (0) 2016.06.21
svn merge... 두근두근  (6) 2016.02.17
tortoiseSVN merge 페이지 번역  (0) 2015.12.02
Posted by 구차니

ubuntu 10.04 LTS 사용중이라 그런지 버전이 낮아서 svn relocate가 안보인다!

$ svn help

사용법: svn <subcommand> [options] [args]

Subversion 명령행 클라이언트 버전 1.6.17.

'svn help <subcommand>'를 사용하여 특정 명령에 대하여 도움말을 얻으십시오.

'svn --version'를 사용하여 버전과 원격접속 모듈에 대한 정보를 얻으십시오.

 또는 'svn --version --quiet'를 사용하여 버전 정보만 얻으십시오.


대부분의 부속 명령어들은 재귀적으로 수행하면서 파일이나 디렉토리를 인자로 취합니다.

명령들에 인자가 주어지지 않으면 현재 디렉토리를 포함하여 재귀적으로 수행하게

됩니다.


가능한 명령:

   add

   blame (praise, annotate, ann)

   cat

   changelist (cl)

   checkout (co)

   cleanup

   commit (ci)

   copy (cp)

   delete (del, remove, rm)

   diff (di)

   export

   help (?, h)

   import

   info

   list (ls)

   lock

   log

   merge

   mergeinfo

   mkdir

   move (mv, rename, ren)

   propdel (pdel, pd)

   propedit (pedit, pe)

   propget (pget, pg)

   proplist (plist, pl)

   propset (pset, ps)

   resolve

   resolved

   revert

   status (stat, st)

   switch (sw)

   unlock

   update (up)


Subversion은 형상관리를 위한 도구입니다.

더 상세한 정보를 위해서는 http://subversion.tigris.org/ 를 방문하세요. 


$ svn --version

svn, 버젼 1.6.17 (r1128011)

    Aug 20 2015, 15:17:45에 컴파일 됨



svn switch --relocate http://old-url https://new-url

[링크 : http://stackoverflow.com/questions/1396368/how-can-i-switch-a-subversion-repository-using-only-command-line-tools]

'프로그램 사용 > Version Control' 카테고리의 다른 글

svn list 에러 generic failure  (0) 2016.06.23
svn 콘솔 에디터(주석)  (0) 2016.06.21
svn merge... 두근두근  (6) 2016.02.17
tortoiseSVN merge 페이지 번역  (0) 2015.12.02
svnadmin dump로 덤프/합치기  (0) 2015.11.26
Posted by 구차니