구글로 하면 잘되나

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



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


---

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 구차니
프로그램 사용/u-boot2016. 12. 14. 17:41

nand 자체를 초기화 하는 명령어

배드 섹터 넘쳐 날때 해주면 초기화 하는데

생겨나는건 여전...


# nand scrub


NAND scrub: device 0 whole chip

Warning: scrub option will erase all factory set bad blocks!

         There is no reliable way to recover them.

         Use this command only for testing purposes if you

         are sure of what you are doing!


Really scrub this NAND flash? <y/N>

Erasing at 0xffe0000 -- 100% complete.

OK 


[링크 : http://www.stlinux.com/howto/NAND/clean]

[링크 : http://damduc.tistory.com/317]

'프로그램 사용 > u-boot' 카테고리의 다른 글

uboot filesize 환경변수  (0) 2016.11.17
tftpd-hpa  (0) 2016.11.08
ubuntu 12.04 xinetd/tftpd 느린 이유  (0) 2016.11.08
uboot bootargs 기본값 설정하기  (0) 2015.02.09
uboot 메모리 관련 명령어  (0) 2015.01.08
Posted by 구차니

빠르고 좋네 +_+

ffmpeg 만세


If you only want to convert mkv to mp4 then you will save quality and a lot of time by just changing the containers. Both of these are just wrappers over the same content so the cpu only needs to do a little work. Don't re encode as you will definitely lose quality.


It's very straight forward using ffmpeg:


ffmpeg -i LostInTranslation.mkv -vcodec copy -acodec copy LostInTranslation.mp4

Here, you are copying the video codec and audio codec so nothing is being encoded.


Tip:


To convert all the mkv files in current directory, run a simple loop in terminal:


for i in *mkv; do ffmpeg -i $i -vcodec copy -acodec copy $i.mp4; done 


[링크 : http://askubuntu.com/questions/396883/how-to-simply-convert-video-files-i-e-mkv-to-mp4]

'프로그램 사용 > ffmpeg & ffserver' 카테고리의 다른 글

ffmpeg concat  (0) 2018.10.10
ffmpeg huffyuv  (0) 2017.02.28
ffmpeg 으로 파일 재생하기  (0) 2015.02.10
Mplayer/ffmpeg 크로스 컴파일 하기  (0) 2015.01.27
ffmpeg arm 아키텍쳐별 최적화 코덱  (0) 2015.01.26
Posted by 구차니
프로그램 사용/Blender2016. 11. 23. 23:41

먼가 엉겨서 이상한거 같긴하지만...


일단 버전 자체는.. git에서 받은거랑 윈도우에서 포함되어 있던 버전이랑 확실히 다르니..

양쪽다 git에서 받던가 패키지 된거에서 뺴내던가 둘중에 하나일지도?


odroid 에다가

blender 두개 실행해서 하나는 Master

하나는 Client로 설정하고

윈도우에서는 

Slave로 해서 붙이긴 했는데 두번 실행했더니 두개가 붙네..


아무튼 파일을 저장하고 나서

Send Network 하고 나서

Get Image 하니 먼가 받아는 오는데

그 이후로는 다시는 안되고 Slave를 종료하기 전에는 Master 로 실행했던 Blender도 안죽고..

미묘하다 미묘해..

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

blender 포켓몬 애니메이션  (0) 2020.08.08
blender physical simulation  (0) 2018.03.19
blender netrender / incorrect master version  (0) 2016.11.22
blender netrender 구조  (0) 2016.11.19
블렌더 강좌  (0) 2016.11.18
Posted by 구차니
프로그램 사용/Blender2016. 11. 22. 21:41

netrender 소스인거 같은데 이런 에러네.. 버전 체크?

def clientConnection(netsettings, report = None, scan = True, timeout = 5):

    address = netsettings.server_address

    port = netsettings.server_port

    use_ssl = netsettings.use_ssl

    

    if address== "[default]":

#            calling operator from python is fucked, scene isn't in context

#            if bpy:

#                bpy.ops.render.netclientscan()

#            else:

        if not scan:

            return None


        address, port = clientScan()

        if address == "":

            return None

    conn = None

    try:

        HTTPConnection = http.client.HTTPSConnection if use_ssl else http.client.HTTPConnection

        if platform.system() == "Darwin":

            with ConnectionContext(timeout):

                conn = HTTPConnection(address, port)

        else:

            conn = HTTPConnection(address, port, timeout = timeout)


        if conn:

            if clientVerifyVersion(conn, timeout):

                return conn

            else:

                conn.close()

                reporting(report, "Incorrect master version", ValueError)

    except BaseException as err:

        if report:

            report({'ERROR'}, str(err))

            return None

        else:

            print(err)

            return None


def clientVerifyVersion(conn, timeout):

    with ConnectionContext(timeout):

        conn.request("GET", "/version")

    response = conn.getresponse()


    if response.status != http.client.OK:

        conn.close()

        return False


    server_version = response.read()


    if server_version != VERSION:

        print("Incorrect server version!")

        print("expected", str(VERSION, encoding='utf8'), "received", str(server_version, encoding='utf8'))

        return False


    return True 

[링크 : https://github.com/WARP-LAB/Blender-Network-Render-Additions/blob/master/netrender/utils.py]


이제야(?) 생각이 나서 콘솔에서 실행에서 에러를 보니.. 크앙..

2.76b랑 2.78이랑은 버전이 다른듯 흑 ㅠㅠ

윈도우 쪽을 버전을 내려야지 머 ㅠㅠ

$ blender

connect failed: No such file or directory

Read new prefs: /home/odroid/.config/blender/2.76/config/userpref.blend

libGL error: unable to load driver: exynos_dri.so

libGL error: driver pointer missing

libGL error: failed to load driver: exynos

Incorrect server version!

expected 1.9 received 1.8

Incorrect master version

Incorrect server version!

expected 1.9 received 1.8

Incorrect master version

Info: Master server found


Incorrect server version!

expected 1.9 received 1.8

Error: Incorrect master version 


+

2016.11.23

윈도우쪽 버전을 맞추어도 여전히 incorrect 버전이라고 뜨네..

python쪽 차이일려나? 미지수네...

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

blender physical simulation  (0) 2018.03.19
blender netrender 잠시 된건가..  (0) 2016.11.23
blender netrender 구조  (0) 2016.11.19
블렌더 강좌  (0) 2016.11.18
ubuntu blender addons  (0) 2016.11.17
Posted by 구차니
프로그램 사용/VLC2016. 11. 22. 09:21

엌 win/pc 버전으로 해서봤더니 나오네? ㅋㅋㅋ


엌ㅋㅋㅋㅋ 멀리서 보면 그럴싸 한데?


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

vlc 간편 녹화  (0) 2022.08.01
http vlc protocol  (0) 2018.11.30
vlc 옵션 -vvv  (0) 2015.09.15
dash with VLC  (0) 2015.09.11
VLC dshow(캡쳐장비) 해상도 설정  (0) 2015.05.23
Posted by 구차니
프로그램 사용/Blender2016. 11. 19. 22:02

Master는 Slave를 관리하기만 하고 자기 스스로는 rendering을 하지 않는 관리용 노드

Slave는 Master의 명령을 받아 rendering하는 노드

Client는 Mater에게 렌더링을 요청하는 노드

The Master Node does no rendering, it just stores the .blend information and sources out the rendering to slaves and keeps track of what 'Chunks' are going where.


The Slave Node (or nodes) are the work horses, doing all the rendering under the control of the master.


The Client Node is the node from which render jobs are sent to the master for distributed rendering by the slaves. This is where you have your project open and are working in Blender. 

[링크 : http://blendingwithforbes.blogspot.kr/2010/03/blender-25-network-render.html]



두대 가지고 하려면은

메인 PC에는

Mater / Slave / Client를 돌리고

보조 PC에다가는

Slave를 돌려야 하는데...

그러면 Blender를 세개나 돌려야 하나.. ㄷㄷㄷ

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

blender netrender 잠시 된건가..  (0) 2016.11.23
blender netrender / incorrect master version  (0) 2016.11.22
블렌더 강좌  (0) 2016.11.18
ubuntu blender addons  (0) 2016.11.17
blender 2.78 network render  (0) 2016.11.16
Posted by 구차니
프로그램 사용/Blender2016. 11. 18. 13:20

오랫만에 만져보려니 다 까먹었네ㅠㅠ


[링크 : https://www.youtube.com/user/nyaank/videos?sort=dd&shelf_id=1&view=0]


[링크 : http://minex2.tistory.com/18]


[링크 : http://sinyeobi.blog.me/10137607440] 컵만들기

    [링크 : http://sinyeobi.blog.me/10134908514] 컵만들기 1

[링크 : http://sinyeobi.blog.me/10137157495] 기본단축키

[링크 : http://sinyeobi.blog.me/10175283243] 기본 UI 설명

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

blender netrender / incorrect master version  (0) 2016.11.22
blender netrender 구조  (0) 2016.11.19
ubuntu blender addons  (0) 2016.11.17
blender 2.78 network render  (0) 2016.11.16
Blender stereoscopic Add-on 설치  (2) 2011.09.19
Posted by 구차니
프로그램 사용/sketchup2016. 11. 18. 10:12

layout이 라는 프로그램이 그런 용도라니..

대충 실행해보니.. layout은 30일간 만 무료인 pro버전에 속한 패키지..

[링크 : http://allbim.kr/?bim-software-article=sketchup-layout]


주말에 sketchup<->blender 한번 시도 해봐야겠다

[링크 : http://www.katsbits.com/tutorials/blender/sketchup-import.php]



[링크 : http://s.elfism.com/entry/카타나-검-모델링하기2-손잡이-모델링]

[링크 : https://wiki.blender.org/index.php/Doc:KO/2.6/Manual/3D_interaction/Transform_Control/Snap] 블렌더 snap

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

sketchup stl exporter  (0) 2017.02.28
sketchup flip 하기  (0) 2016.08.31
sketchup 여러번 복사하기  (0) 2016.08.31
스케치업 자격증  (0) 2014.02.28
스케치업 / export to DWG  (0) 2014.02.04
Posted by 구차니