구글로 하면 잘되나

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



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


---

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 구차니
프로그램 사용/apache2016. 12. 29. 10:16

공인 인증하려면 비싸니까(년 3만원 이상)

개인이 쓰거나 개발용이라면 사설인증서 만들어서 써도 된다네?(openssl)


[링크 : http://stackoverflow.com/questions/2336678/login-without-https-how-to-secure]

[링크 : https://opentutorials.org/course/228/4894]

[링크 : http://tuwlab.com/ece/8616] <<



개인정보 가지면 무조건 HTTPS를 의무화한 법안으로 인해

일단은 무조건 해봐야 할 듯?


+

라즈베리 밀고 다시 해봐야하나.

그냥 기본적으로 거의 다 되어 있고

http://localhost 대신

https://localhost로 하면 된다.


http://로 접속한걸 https:// 로 자동 리다이렉션 하는 법 없나..

<VirtualHost *:80>

        # The ServerName directive sets the request scheme, hostname and port that

        # the server uses to identify itself. This is used when creating

        # redirection URLs. In the context of virtual hosts, the ServerName

        # specifies what hostname must appear in the request's Host: header to

        # match this virtual host. For the default virtual host (this file) this

        # value is not decisive as it is used as a last resort host regardless.

        # However, you must set it for any further virtual host explicitly.

        #ServerName www.example.com


        ServerAdmin webmaster@localhost

        DocumentRoot /var/www/html


        # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,

        # error, crit, alert, emerg.

        # It is also possible to configure the loglevel for particular

        # modules, e.g.

        #LogLevel info ssl:warn


        ErrorLog ${APACHE_LOG_DIR}/error.log

        CustomLog ${APACHE_LOG_DIR}/access.log combined


        # For most configuration files from conf-available/, which are

        # enabled or disabled at a global level, it is possible to

        # include a line for only one particular virtual host. For example the

        # following line enables the CGI configuration for this host only

        # after it has been globally disabled with "a2disconf".

        #Include conf-available/serve-cgi-bin.conf


        Redirect / https://raspberrypi/

</VirtualHost> 

[링크 : http://stackoverflow.com/.../how-to-redirect-all-http-requests-to-https/21798882#21798882]


mod_rewrite로 하는법이라는데 왜 안되지..

[링크 : https://wiki.apache.org/httpd/RewriteHTTPToHTTPS]



+

16.11.25일자 라즈베리 배포판으로는 아래의 과정만 하면 설정완료!

ssl 관련 설정은 해당 모듈만 활성화 시켜주는 되는 듯

$ sudo apt-get install apache2 mysql-server mysql-client-5.5 php5 php5-mysql php5-gd openssl


$ cd /etc/apache2/sites-available/

$ sudo vi 000-default.conf

redirect / https://raspberrypi/

$ sudo a2enmod ssl

$ sudo a2ensite default-ssl

$ sudo service apache2 restart 


+2017.01.05

경고 뜨는게 싫고, 다른데서 매번 사용확인해주는게 귀찮아서라도 하나 해야 할 듯 -_-

[링크 : https://www.xetown.com/slope/135905]

[링크 : https://blog.elpo.net/get-free-ssl-certificate/]


Posted by 구차니
Linux/Ubuntu2016. 12. 29. 09:45

매번 하나하나 깔았는데 흑...


To install the default LAMP stack in Ubuntu 10.04 and above

First refresh your package index...


$ sudo apt-get update

... and then install the LAMP stack:


$ sudo apt-get install lamp-server^

Mind the caret (^) at the end.  


[링크 : https://help.ubuntu.com/community/ApacheMySQLPHP]

   [링크 : http://www.wikihow.com/Create-a-Secure-Login-Script-in-PHP-and-MySQL]


+

어라.. 라즈베리에서는 안되는데?!?

$ sudo apt-cache search lamp

air-quality-sensor - user space driver for AppliedSensor's Indoor Air Monitor

evolvotron - Generator of textures through interactive evolution

ruby-clamp - minimal framework for Ruby command-line utilities

xscreensaver-data - Screen saver modules for screensaver frontends 


'Linux > Ubuntu' 카테고리의 다른 글

우분투 창 관리 - 윈7처럼 창분할 단축키  (0) 2017.01.09
crontab 과 cron 서비스 reload  (0) 2017.01.04
ubuntu 16.04 설정관련  (0) 2016.11.14
fcitx-hangul  (0) 2016.11.14
xwindow 가상 터미널 비활성화 하기  (2) 2016.10.11
Posted by 구차니
Programming/php2016. 12. 28. 18:59

'Programming > php' 카테고리의 다른 글

php 변수 스코프  (0) 2017.01.02
php print_r  (0) 2017.01.02
php db connection pool  (0) 2016.12.28
php 로그인 예제 2  (0) 2016.12.27
php pdo? - PHP Data Object  (0) 2016.11.23
Posted by 구차니
Programming/php2016. 12. 28. 18:30

'Programming > php' 카테고리의 다른 글

php print_r  (0) 2017.01.02
php 게시판  (0) 2016.12.28
php 로그인 예제 2  (0) 2016.12.27
php pdo? - PHP Data Object  (0) 2016.11.23
php template  (0) 2016.11.14
Posted by 구차니

솔찍히 이전에 국정원 어쩌구 할때도 크게 관심이 없었고

단순히 수많은 자료를 취합해서 국정원 쪽이란걸 밝혀낸것만 알고

세월호 관련해서도 국정원의 사주에 의한 침몰로만 생각을 해서

이유에 대해서는 크게 생각을 안했는데(어짜피 침몰이라는 결과를 위한 원인이 중요하지 과정은 중요치 않다고 봐서)


이번 세월.X 다큐를 보고 지도를 보면서 드는생각은

이번만큼은 잘못 판단한게 아닐까? 라는 것

[링크 : http://v.media.daum.net/v/20161228095806476]



자로 본인도 레이더에 대한 허상 가능성을 알고

서거차도에 레이더에서 직선상에서만 레이더 허상이 생긴다고 하는데

다른 배들에게서는 생기지 않았다고 하는데


그 망할(!) 허상은 하도여러가지 원인이 있어서 머라고 딱히 밝히기 힘든 실무적인 문제라고 해야하나

그런 부류라고 생각이 된다. 만약 반사가 잘되는 구름이 전파 전달로 상에 있었고

아주 우연히 변침 시점에서 딱 맞아 들어가서 허상이 잘 발생하고 구름이 피하면서(약 10분?)

서서히 사라진게 아닐까 라는 생각이 든다.


그리고 가끔 보이는 레이더 영상들을 보면(세월호 말고)

수많은 노이즈들로 인해 오탐되는 것들도 있어서 어느정도 반사되는 것만 필터링하는 식으로

처리하는걸 볼 수 있는데(수신감도 라던가)


수 많은 선박을 봐야 하는 관제소 입장에서는 필터링되지 않은 노이즈가 많은 레이더 신호를 보고

처리할 수 없기에 수 많은 데이터들을 조합 / 필터링 하여 표기할것으로 생각이 되는데

이런 부분에 있어서 논리적 근거가 너무 약하다고 해야하나...


우연히 검색해서 들어갔던 시지프스(그 교수) 블로그 글의 댓글을 보면 실무vs이론의 열불나는 설전이..

[링크 : http://blog.naver.com/actachiral/220436684118]




아무튼 내 결론

1. 서거차도 레이더 영상이라면 대충 보기에는 일직선 상에 세월호와 미스테리 물체가 놓여있다.



2. 당시 비가 오는 등, 국지적으로 전파가 반사되거나 흡수될 여지가 많은 환경이기에 허상일 가능성이 있다.

Multiple reflections[edit]

Three-body-scattering.PNG

It is assumed that the beam hits the weather targets and returns directly to the radar. In fact, there is energy reflected in all directions. Most of it is weak, and multiple reflections diminish it even further so what can eventually return to the radar from such an event is negligible. However, some situations allow a multiple-reflected radar beam to be received by the radar antenna.[11] For instance, when the beam hits hail, the energy spread toward the wet ground will be reflected back to the hail and then to the radar. The resulting echo is weak but noticeable. Due to the extra path length it has to go through, it arrives later at the antenna and is placed further than its source.[51] This gives a kind of triangle of false weaker reflections placed radially behind the hail.[41] 

[링크 : https://en.wikipedia.org/wiki/Weather_radar#Limitations_and_artifacts]

[링크 : https://www.ntia.doc.gov/legacy/osmhome/reports/ntia00-40/chapt3.htm]


음.. 당일날씨가 연무인가? 동영상 보면 구조할때 비오고 하던데 착각했나..

[링크 : http://blog.naver.com/kj_nalssi/20123700977]

[링크 : http://www.kma.go.kr/weather/climate/past_cal.jsp?stn=175&yy=2014&mm=4&obs=9&x=19&y=7]


3. 사고 지점까지 멀다고 하는데 레이더 목적을 고려하면 절대 먼 거리라 볼 수 없다. (약 12km)

4.3.1 다양한 선박위치 정보 획득체계 개선

레이더의 물표 탐지 기준은 IALA Recommendation V-128에서 그 성능을 정의 되어 있으며, 레이더 안테나 높이가 해수면에서 100m 일 때 작은 금속 선박이나, 어선 등과 같이 반사유효 단면적이 100㎡ 인 선박을 맑은 날에는 22NM(주 약 39.6km)에서 식별할 수 있어야 한다. 따라서 국내에 사용되고 있는 연안VTS용 레이더는 기본적으로 22NM~25NM(주 약 45km)에서 선박의 관제가 가능한 성능을 가졌으며 리플렉트 타입의 안테나 및 고출력 트랜시버를 사용함으로 인해 서비스 거리를 45NM(주 약 81km)까지로 하고 있다 

※ 1NM = 약 1.8KM

[링크 : http://www.prism.go.kr/...&research_id=1530000-201200003]

[링크 : http://www.easat.com/download/products/ea3462-antenna.pdf]


4. 다만, 10ft 컨테이너 크기로 봤을때 VTS 시스템에 의해서 반사면적이 적어 무시되었을 가능성이 높다.

10ft 컨테이터 2.99m * 2.59m * 2.43m 가장 커도 7.7441m^2

[링크 : https://www.mrbox.co.uk/pdf/container-dimensions.pdf]


5. 중요한 내용은 아니지만, 일단 운영상 최저 속도는 10rpm, 초당 10회 니까 6초에 한번 같은 지점을 스캔 가능하다.

업체 카타로그로는 22rpm norminal 약 3초에 한번 씩 갱신되는게 정상이다.

그런데 영상 자체는 30~40초 한번 업데이트 되는데 레이더 영상 전송 및 저장공간 부담으로 인해

저장본 자체는 3rpm 이하로 저장되는 것으로 보인다.

[링크 : https://www.youtube.com/watch?v=e8VxA1BH4sQ]


대충 다음지도에 얹혀 본건데

수직선상이 아니라고 한 자로의 발언이 맞는건가..

저정도 각도 차이라면 반사나 감쇄에 의한 영향을 받기에는 너무 거리가 멀다고 해야하려나?



Posted by 구차니
Linux2016. 12. 28. 11:12

리눅스에 포함되서 편하군


$ base64 --help

Usage: base64 [OPTION]... [FILE]

Base64 encode or decode FILE, or standard input, to standard output.


  -d, --decode          decode data

  -i, --ignore-garbage  when decoding, ignore non-alphabet characters

  -w, --wrap=COLS       wrap encoded lines after COLS character (default 76).

                          Use 0 to disable line wrapping


      --help     이 도움말을 표시하고 끝냅니다

      --version  버전 정보를 출력하고 끝냅니다


<파일>이 주어지지 않거나 - 이면 표준 입력을 읽습니다.


데이터는 RFC 3548에 서술된 방식대로 base64 알파벳으로 인코딩됩니다.

디코딩할 때 입력은 공식적인 base64 알파벳과 함께 newline 문자를 포함할 수 있습니다.

인코딩된 스트림에서 알파벳이 아닌 문자열을 복구하려면 --ignore-garbage 옵션을 이용하십시오.


Report base64 bugs to bug-coreutils@gnu.org

GNU coreutils 홈 페이지: <http://www.gnu.org/software/coreutils/>

GNU 소프트웨어 사용에 관련된 전반적인 도움을 얻기: <http://www.gnu.org/gethelp/>

Report base64 translation bugs to <http://translationproject.org/team/>

For complete documentation, run: info coreutils 'base64 invocation' 


$ echo '내용들..' | base64 --decode > savename.file

'Linux' 카테고리의 다른 글

리눅스 파일 시간관련  (0) 2017.01.01
ctime mtime.. 엌?!  (0) 2016.12.31
리눅스 런레벨  (0) 2016.12.12
yaffs2 / ext4 비교?  (0) 2016.11.04
dri drm ddx  (0) 2016.10.23
Posted by 구차니
Programming/php2016. 12. 27. 10:30

'Programming > php' 카테고리의 다른 글

php 게시판  (0) 2016.12.28
php db connection pool  (0) 2016.12.28
php pdo? - PHP Data Object  (0) 2016.11.23
php template  (0) 2016.11.14
php 버전 및 년도  (0) 2016.10.11
Posted by 구차니

X 밴드 레이더라고 하는데..

진도 VTS에서 보고 있는 레이더 영상이었다고 하는데

[링크 : https://www.youtube.com/watch?v=4zKsg2LocBQ]


진도 VTS는 이렇다는데 대충 7층 정도

그럼 높이가 끽해봤자 30m 보다 낮다는건데


사고 해역인 병풍도 까지는 하조도가 가로막고 있고

하조도의 레이더 빔의 직선상인 조도면 쪽은 200m 정도 까지 되는 산지

그리고 VTS에서 직선상 왼쪽에(VTS 기준) 수풀이 있는데..

각도에 따라서는 숲의 영향을 받을 것 같다.

장죽도도 직선상 아주 가까운 곳에 존재하고

저 200m 정도 되는 조도면의 산을 뚫고 진도 VTS에서 

자기 레이더만으로 세월호를 추적이 가능했을까?


진도 VTS에서 공개한 자료는

실제로 자기가 가진 레이더가 아닌 다른 사이트의 레이더와 같이 합성된(synthetic) 지역 영상이 아닐까?

그리고 세월호를 뒷따르는 이상한 궤적은 저런 지형적인 영향으로 인한 고스트가 아닐까?



+

진도VTS쪽 레이더가 아니라 서거차도 레이더일려나?


[링크 : http://blog.naver.com/actachiral/220439022976]




+

고스트 라고 표현했는데. 다중경로 전파로 인한 허상 이라고 해야 하려나?

[링크 : https://en.wikipedia.org/wiki/Multipath_propagation]

Posted by 구차니

라고 하면 너무 노력을 폄하하는게 되려나?


1. 초반에는 "외력"을 의심을 함

2. 국가에서 공개한 각종 항적 자료와 항해자료를 교차검증

3. 결론은 데이터 통신이나 디지털 자료 특성의 오류로 인한것이지 조작되었다 볼 수는 없음

4. 세월호는 의외로 쓰러트리려고 해도 쓰러지지 않는 녀석이다.

5. 편견을 버리고 진실을 보자(조금은 작위적이고... 권위에 호소하는 오류 같지만)

6. 진짜 결론. 아직 세월호 사건에 대해서 알려진 내용은 하나도 없다. 조사가 필요하다






다큐에 대한 개인적인 평가

1. 다운로드받아서 5배속으로 보았음에도 하세월 ㅜㅜ

소리없이 빠르게 하다 보니 빠트린 부분이 있을 수도 있지만

밀도가 낮은 다큐멘타리 구성과, 느린 자막(?)으로 도배되고

나레이션으로(무음으로 봐서..) 도배되서 더 늘어난 재생시간이 아쉽다.


2. 초반에 외력을 의심하지만, 언론이나 리뷰어(?)들은 잠수함을 연관했으나

어떠한 외력에 의한 이론상 가능한 선회율을 상회하는 궤적만 남았다는 것이 핵심인데

그걸 잘못한 해석한 사람들의 평이 아쉽..


3. 7시간 시점에서 나오는 박근혜 7시간 의혹은 노린 편집이나.. 내용이 없어서 아쉽


4. 무기력하게 끝날수 있는 "실제로 아직 밝혀진건 하나도 없다" 라는 메시지

물론 개인의 입장에서 확신이 없는 자료로 추측성 발언을 할 수는 없기에 이해는 되지만

화두조차도 읽히기 힘들게 던져진 상황이라 아쉽..



개인적인 의문

외력에 대한 걸 하도 강조해서 고민을 해보았지만

외부의 충격 그리고 그 이후에 문제가 발생했고

내용들 중 무게중심에서 벗어난 곳에서 칠수록 선회력이 커질수 있다는

보이지 않는 면에서 어떠한 물체가 쳤거나 끌었다는 이야기 인데, 

6천톤급 이라는데 짐 실린것만 2천톤 막 이런녀석이라..

세월호 진행방향 기준 왼쪽 전면에서 무언가가 쳐박았거나(무게중심에서 멀어 질수록 영향력이 커지니)

왼쪽 후면에서 무언가가 견인을 해서 끌어 버렸거나 일텐데..

쳐박기에는 둘다 무사하지 못할 테고(고래라는 설도 있지만.. 세월호 파손이 없기에)

끌고 가기에는 그 질량을 순식간에 엄청난 힘이 걸릴 텐데 그만한 힘을 버텨줄 끈도 없을테고

잠수함으로 보기에는 함교탑의 크기가 너무 과도하게 큰게 아닌가 싶고..

(얼핏봐서는 1/5 정도는 되는데 함교탑이 그만하려면 도대체 잠수함이 얼마나 커질까 싶고

그렇게 큰 함교탑이 존재할까 싶으면서도 그렇게 무거운 두녀석을 버텨줄 끈의 존재가 가능할까 싶은 수준?)

그리고.. 도대체 저 레이더 영상은(미스테리 물건?) 어느쪽에서 찍은걸까

레이더-세월호-미스테리 일직선이라면 전파 특성상 세월호에 가려진 영상을 찍기 힘들텐데 도대체 무얼까

혹시.. 다른곳에서 반사된 잘못된 전파가 흔적으로 남은건 아닐까?


아무튼 그래서 결론은 알려진게 밝혀진게 너무도 없다는 사실에 대공감...



+ 개인적인 생각

오히려 세월호를 뒤집을 만큼 큰 너울성 파도라던가(얘가 레이더 잡히는진 모르겠지만) 이런게 현실이 있어 보이지

이번 다큐를 보고 나니.. 잠수함 설은 더더욱 불가능해 보이는 기분


+

레이더 영상이나 GPS 좌표에 의한 오차 범위를 고려해본다면, 마지막 급격한 선회역시도 최대 오차 범위로 본다면

오히려 완화될 가능성도 있지 않을까?

[링크 : http://newstapa.org/12806] << 레이더 영상 (02:00)


+

진도 VTS 레이더 영상이 공개된거라고 하니

저 미스테리 영상 자체는 세월호의 위치와 관계없이 찍혀질순 있겠네(세월호 기준 북서쪽에 레이더가 있는 줄..)


뉴스타파 동영상 캡쳐인데

세월호가 제주도를 향할때는 정 측면에서 레이더를 조사 받기에, 가장 크게 나올테고

충돌 이후에는 레이더를 보고 있기에 반사면적이 좁아져서 작게 나오게 된다.

레이더 특성상 반사면적에 따라 크게 보이기도 작게 보이기도 하는데

세월호 측면의 1/10 정도 되어 보이는 크기의 반사면적을 가지려면 얼마정도 크기면 되려나..



+

2016.12.27 추가

생각을 해본.. 초당 몇도 이상 선회를 했다고 해서 문제라고 하는데..

다르게 보면 설계상 최대 선회 각도가 있을 꺼고

설계를 넘어선... 자동차로 치면 오버스티어 발생시 최대 선회 각도는 다를테니

배가 선회를 하는 개념과 미끌리면서 돌아 버리는 경우를 다르게 판단해야 하지 않을까?

Posted by 구차니