Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ venv*
.tox
/pip-selfcheck.json
/man
.eggs
98 changes: 86 additions & 12 deletions svn/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
'type',
'revision',
])
LOG_ENTRY = collections.namedtuple(
'LogEntry',
('date', 'msg', 'revision', 'author', 'changelist'))


class CommonClient(svn.common_base.CommonBase):
Expand Down Expand Up @@ -157,6 +160,11 @@ def properties(self, rel_path=None):
# query the proper list of this path
root = xml.etree.ElementTree.fromstring(result)
target_elem = root.find('target')

# check for empty property list
if target_elem is None:
return {}

property_names = [p.attrib["name"]
for p in target_elem.findall('property')]

Expand Down Expand Up @@ -185,7 +193,7 @@ def cat(self, rel_filepath, revision=None):
def log_default(self, timestamp_from_dt=None, timestamp_to_dt=None,
limit=None, rel_filepath=None, stop_on_copy=False,
revision_from=None, revision_to=None, changelist=False,
use_merge_history=False):
use_merge_history=False, search=None):
"""Allow for the most-likely kind of log listing: the complete list,
a FROM and TO timestamp, a FROM timestamp only, or a quantity limit.
"""
Expand Down Expand Up @@ -233,6 +241,9 @@ def log_default(self, timestamp_from_dt=None, timestamp_to_dt=None,
if stop_on_copy is True:
args += ['--stop-on-copy']

if search is not None:
args += ['--search', search]

if use_merge_history is True:
args += ['--use-merge-history']

Expand All @@ -245,10 +256,6 @@ def log_default(self, timestamp_from_dt=None, timestamp_to_dt=None,
do_combine=True)

root = xml.etree.ElementTree.fromstring(result)
named_fields = ['date', 'msg', 'revision', 'author', 'changelist']
c = collections.namedtuple(
'LogEntry',
named_fields)

# Merge history can create nested log entries, so use iter instead of findall
for e in root.iter('logentry'):
Expand All @@ -275,7 +282,7 @@ def log_default(self, timestamp_from_dt=None, timestamp_to_dt=None,
else:
log_entry['changelist'] = None

yield c(**log_entry)
yield LOG_ENTRY(**log_entry)

def export(self, to_path, revision=None, force=False):
cmd = []
Expand All @@ -287,20 +294,64 @@ def export(self, to_path, revision=None, force=False):
cmd.append('--force') if force else None

self.run_command('export', cmd)

def blame(self, rel_filepath,
revision_from=None, revision_to=None):
"""
svn usage: blame [-r M:N] TARGET[@REV]
"""

full_url_or_path = self.__url_or_path + "/" + rel_filepath

args = []
if revision_from or revision_to:
if not revision_from:
revision_from = '1'

if not revision_to:
revision_to = 'HEAD'

def status(self, rel_path=None):
args += ['-r', str(revision_from) + ':' + str(revision_to)]

result = self.run_command(
"blame", args + ["--xml", full_url_or_path], do_combine=True)

root = xml.etree.ElementTree.fromstring(result)

for entry in root.findall("target/entry"):
commit = entry.find("commit")
author = entry.find("commit/author")
date_ = entry.find("commit/date")
if author is None or date_ is None:
continue
info = {
'line_number': int(entry.attrib['line-number']),
"commit_author": self.__element_text(author),
"commit_date": dateutil.parser.parse(date_.text),
"commit_revision": int(commit.attrib["revision"]),
}
yield info

def status(self, rel_path=None, no_ignore=False, include_changelists=False):
full_url_or_path = self.__url_or_path
if rel_path is not None:
full_url_or_path += '/' + rel_path

cmd = ['--xml', full_url_or_path]
if no_ignore:
cmd.append('--no-ignore')

raw = self.run_command(
'status',
['--xml', full_url_or_path],
cmd,
do_combine=True)

root = xml.etree.ElementTree.fromstring(raw)

list_ = root.findall('target/entry')
if include_changelists is True:
list_ += root.findall('changelist/entry')

for entry in list_:
entry_attr = entry.attrib
name = entry_attr['path']
Expand Down Expand Up @@ -454,13 +505,36 @@ def diff(self, old, new, rel_path=None):
'--new', '{0}@{1}'.format(full_url_or_path, new)],
do_combine=True)
file_to_diff = {}

# A diff has this form, potentially repeating for multiple files.
#
# Index: relative/filename.txt
# ===================================================================
# The diff content
#
# Here we split diffs into files by the index section, pick up the
# file name, then split again to pick up the content.
for non_empty_diff in filter(None, diff_result.decode('utf8').split('Index: ')):
split_diff = non_empty_diff.split('==')
file_to_diff[split_diff[0].strip().strip('/')] = split_diff[-1].strip('=').strip()
split_diff = non_empty_diff.split('='*67)
index_filename = split_diff[0].strip().strip('/')
file_to_diff[index_filename] = split_diff[-1].strip()
diff_summaries = self.diff_summary(old, new, rel_path)

# Allocate summary info to the text for each change.
# Not all diffs necessarily affect file text (directory changes, for example).
for diff_summary in diff_summaries:
diff_summary['diff'] = \
file_to_diff[diff_summary['path'].split(full_url_or_path)[-1].strip('/')]
diff_summary['diff'] = ''
if 'path' in diff_summary:
# Summary file paths are absolute, while diff file indexes are relative.
# We try to match them up, first checking the root of the diff.
summary_index = diff_summary['path'][len(full_url_or_path):].strip('/')
# If the diff was conducted directly on the file, not a directory, the
# above will fail to find the relative file name, so we look for that directly.
if summary_index == '':
summary_index = os.path.basename(full_url_or_path)
# If we can match a diff to a summary, we attach it.
if summary_index in file_to_diff:
diff_summary['diff'] = file_to_diff[summary_index]
return diff_summaries

@property
Expand Down
25 changes: 18 additions & 7 deletions svn/common_base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import sys
import subprocess
import logging

Expand All @@ -12,18 +13,28 @@ class CommonBase(object):
def external_command(self, cmd, success_code=0, do_combine=False,
return_binary=False, environment={}, wd=None):
_LOGGER.debug("RUN: %s" % (cmd,))
# print("RUN:", cmd)

env = os.environ.copy()
env['LANG'] = svn.config.CONSOLE_ENCODING
env.update(environment)

p = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=wd,
env=env)

kwargs = {
'stdout': subprocess.PIPE,
'stderr': subprocess.STDOUT,
'cwd': wd,
'env': env,
}

try:
# Don't open Command Prompt on Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
kwargs['startupinfo'] = startupinfo
except AttributeError:
pass

p = subprocess.Popen(cmd, **kwargs)
stdout = p.stdout.read()
r = p.wait()
p.stdout.close()
Expand Down
10 changes: 10 additions & 0 deletions svn/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,13 @@ def cleanup(self):
'cleanup',
[],
wd=self.path)

def propset(self, property_name, property_value, rel_path=None):
'''Set a property on a relative path'''
if rel_path is None:
rel_path = '.'

self.run_command(
'propset',
[property_name, property_value, rel_path],
wd=self.path)
29 changes: 22 additions & 7 deletions svn/resources/README.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
|donate|

|Build\_Status|
|Coverage\_Status|

Expand All @@ -22,11 +20,13 @@ Functions currently implemented:
- cat
- diff
- diff_summary
- propset
- status
- add
- commit
- update
- cleanup
- blame

In addition, there is also an "admin" class (`svn.admin.Admin`) that provides a `create` method with which to create repositories.

Expand Down Expand Up @@ -135,8 +135,8 @@ Get file-data as string::
l = svn.local.LocalClient('/tmp/test_repo')
content = l.cat('test_file')

log_default(timestamp_from_dt=None, timestamp_to_dt=None, limit=None, rel_filepath='', stop_on_copy=False, revision_from=None, revision_to=None, changelist=False)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
log_default(timestamp_from_dt=None, timestamp_to_dt=None, limit=None, rel_filepath='', stop_on_copy=False, revision_from=None, revision_to=None, changelist=False, search=None)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Perform a log-listing that can be bounded by time or revision number and/or take a maximum-
count::
Expand Down Expand Up @@ -280,9 +280,24 @@ diff(start_revision, end_revision)

Finds all the diff between start and end revision id. Here another key of 'diff' is added which shows the diff of files.

.. |donate| image:: https://pledgie.com/campaigns/31718.png?skin_name=chrome
:alt: Click here to lend your support to: PySvn and make a donation at pledgie.com !
:target: https://pledgie.com/campaigns/31718
blame(rel_filepath, revision=None)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Blame specified file ::

import svn.local

l = svn.local.LocalClient('/tmp/test_repo.co')

for line_info in l.blame("version.py"):
print(line_info)

# {
# 'line_number': 1,
# 'commit_revision': 1222,
# 'commit_author': 'codeskyblue',
# 'commit_date': datetime.datetime(2018, 12, 20, 3, 25, 13, 479212, tzinfo=tzutc())}

.. |Build_Status| image:: https://travis-ci.org/dsoprea/PySvn.svg?branch=master
:target: https://travis-ci.org/dsoprea/PySvn
.. |Coverage_Status| image:: https://coveralls.io/repos/github/dsoprea/PySvn/badge.svg?branch=master
Expand Down
7 changes: 6 additions & 1 deletion tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ def test_create_repository(self):
a.create(temp_path)

# Do a read.
rc = svn.remote.RemoteClient('file://' + temp_path)
# Duck-type identification of path type (Windows, Linux, etc.) to ensure
# file uri canonicity is enforced. URI must be file:///.
if temp_path[0] == '/':
rc = svn.remote.RemoteClient('file://' + temp_path)
else:
rc = svn.remote.RemoteClient('file:///' + temp_path)
info = rc.info()
_LOGGER.debug("Info from new repository: [%s]", str(info))
finally:
Expand Down
Loading