diff --git a/svn/common.py b/svn/common.py index a53e2f2..3afa524 100644 --- a/svn/common.py +++ b/svn/common.py @@ -285,6 +285,43 @@ def export(self, to_path, revision=None, force=False): 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' + + 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 list(self, extended=False, rel_path=None): full_url_or_path = self.__url_or_path if rel_path is not None: diff --git a/svn/resources/README.md b/svn/resources/README.md index d8b6aad..b54e507 100644 --- a/svn/resources/README.md +++ b/svn/resources/README.md @@ -25,6 +25,7 @@ Functions currently implemented: - update - cleanup - remove (i.e. rm, del, delete) +- blame In addition, there is also an "admin" class (`svn.admin.Admin`) that provides a `create` method with which to create repositories. @@ -268,6 +269,26 @@ for rel_path, e in l.list_recursive(): # 'timestamp': datetime.datetime(2015, 4, 24, 3, 25, 13, 479212, tzinfo=tzutc())} ``` +### blame(rel_filepath, revision=None) + +Blame specified file. + +``` +import pprint +import svn.local + +lc = svn.local.LocalClient('/tmp/test_repo.co') + +for line_info in lc.blame("version.py"): + pprint.pprint(line_info) + +# { +# 'line_number': 1, +# 'commit_revision': 1222, +# 'commit_author': 'codeskyblue', +# 'commit_date': datetime.datetime(2018, 12, 20, 3, 25, 13, 479212, tzinfo=tzutc())} +``` + ### diff_summary(start_revision, end_revision) diff --git a/tests/test_common.py b/tests/test_common.py index ba74bec..a763adc 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -208,6 +208,22 @@ def test_export(self): except: pass + def test_blame(self): + with svn.test_support.temp_common() as (_, _, cc): + f = svn.test_support.populate_bigger_file_change1() + actual_answer = cc.blame(f, revision_to=2) + line1 = next(actual_answer) + for i in range(5): + # find the modified line + line6 = next(actual_answer) + line7 = next(actual_answer) + self.assertEqual(line1['line_number'], 1) + self.assertEqual(line1['commit_revision'], 1) + self.assertEqual(line1['commit_author'], line7['commit_author']) + + self.assertEqual(line6['line_number'], 6) + self.assertEqual(line6['commit_revision'], 2) + def test_force__export(self): with svn.test_support.temp_common() as (_, _, cc): svn.test_support.populate_bigger_file_changes1()