561
628
def parse_one_node(self):
631
# optional 'kind' and required 'action' must be next
563
632
node['kind'] = self.parse_kind()
564
633
action = self.parse_action()
565
node['copyfrom_rev'] = self.parse_copyfrom_rev()
566
node['copyfrom_path'] = self.parse_copyfrom_path()
567
node['copy_md5'] = self.parse_copy_md5()
568
node['copy_sha1'] = self.parse_copy_sha1()
569
node['prop_length'] = self.parse_prop_length(required=False)
570
node['text_length'] = self.parse_text_length()
571
node['text_md5'] = self.parse_text_md5()
572
node['text_sha1'] = self.parse_text_sha1()
573
node['content_length'] = self.parse_content_length(required=False)
635
# read any remaining headers
636
headers_list = self.parse_headers()
637
headers = dict(headers_list)
639
# Content-length must be last, if present
640
if 'Content-length' in headers and headers_list[-1][0] != 'Content-length':
641
raise SVNDumpParseError("'Content-length' header is not last, "
642
"in header block ending at line %d"
645
# parse the remaining optional headers and store in specific keys in NODE
646
for key, header, regex in [
647
('copyfrom_rev', 'Node-copyfrom-rev', '([0-9]+)$'),
648
('copyfrom_path', 'Node-copyfrom-path', '(.*)$'),
649
('copy_md5', 'Text-copy-source-md5', '([0-9a-z]+)$'),
650
('copy_sha1', 'Text-copy-source-sha1','([0-9a-z]+)$'),
651
('prop_length', 'Prop-content-length', '([0-9]+)$'),
652
('text_length', 'Text-content-length', '([0-9]+)$'),
653
('text_md5', 'Text-content-md5', '([0-9a-z]+)$'),
654
('text_sha1', 'Text-content-sha1', '([0-9a-z]+)$'),
655
('content_length', 'Content-length', '([0-9]+)$'),
657
if not header in headers:
660
m = re.match(regex, headers[header])
662
raise SVNDumpParseError("expected '%s' at line %d\n%s"
663
% (regex, self.current,
664
self.lines[self.current]))
665
node[key] = m.group(1)
574
667
self.parse_blank()
575
668
if node['prop_length']:
576
669
node['props'] = self.get_props()
639
736
parsed_expected = DumpParser(expected).parse()
640
737
parsed_actual = DumpParser(actual).parse()
740
parsed_expected['uuid'] = '<ignored>'
741
parsed_actual['uuid'] = '<ignored>'
743
for parsed in [parsed_expected, parsed_actual]:
744
for rev_name, rev_record in parsed.items():
745
#print "Found %s" % (rev_name,)
746
if 'nodes' in rev_record:
747
#print "Found %s.%s" % (rev_name, 'nodes')
748
for path_name, path_record in rev_record['nodes'].items():
749
#print "Found %s.%s.%s" % (rev_name, 'nodes', path_name)
750
for action_name, action_record in path_record.items():
751
#print "Found %s.%s.%s.%s" % (rev_name, 'nodes', path_name, action_name)
753
if expect_content_length_always:
754
if action_record.get('content_length') == None:
755
#print 'Adding: %s.%s.%s.%s.%s' % (rev_name, 'nodes', path_name, action_name, 'content_length=0')
756
action_record['content_length'] = '0'
757
if ignore_empty_prop_sections:
758
if action_record.get('prop_length') == '10':
759
#print 'Removing: %s.%s.%s.%s.%s' % (rev_name, 'nodes', path_name, action_name, 'prop_length')
760
action_record['prop_length'] = None
761
del action_record['props']
762
old_content_length = int(action_record['content_length'])
763
action_record['content_length'] = str(old_content_length - 10)
764
if ignore_number_of_blank_lines:
765
action_record['blanks'] = 0
642
767
if parsed_expected != parsed_actual:
643
raise svntest.Failure('\n' + '\n'.join(ndiff(
768
print 'DIFF of raw dumpfiles (including expected differences)'
769
print ''.join(ndiff(expected, actual))
770
raise svntest.Failure('DIFF of parsed dumpfiles (ignoring expected differences)\n'
644
772
pprint.pformat(parsed_expected).splitlines(),
645
773
pprint.pformat(parsed_actual).splitlines())))
775
##########################################################################################
776
## diff verifications
777
def is_absolute_url(target):
778
return (target.startswith('file://')
779
or target.startswith('http://')
780
or target.startswith('https://')
781
or target.startswith('svn://')
782
or target.startswith('svn+ssh://'))
784
def make_diff_header(path, old_tag, new_tag, src_label=None, dst_label=None):
785
"""Generate the expected diff header for file PATH, with its old and new
786
versions described in parentheses by OLD_TAG and NEW_TAG. SRC_LABEL and
787
DST_LABEL are paths or urls that are added to the diff labels if we're
788
diffing against the repository or diffing two arbitrary paths.
789
Return the header as an array of newline-terminated strings."""
791
src_label = src_label.replace('\\', '/')
792
if not is_absolute_url(src_label):
793
src_label = '.../' + src_label
794
src_label = '\t(' + src_label + ')'
798
dst_label = dst_label.replace('\\', '/')
799
if not is_absolute_url(dst_label):
800
dst_label = '.../' + dst_label
801
dst_label = '\t(' + dst_label + ')'
804
path_as_shown = path.replace('\\', '/')
806
"Index: " + path_as_shown + "\n",
807
"===================================================================\n",
808
"--- " + path_as_shown + src_label + "\t(" + old_tag + ")\n",
809
"+++ " + path_as_shown + dst_label + "\t(" + new_tag + ")\n",
812
def make_no_diff_deleted_header(path, old_tag, new_tag):
813
"""Generate the expected diff header for a deleted file PATH when in
814
'no-diff-deleted' mode. (In that mode, no further details appear after the
815
header.) Return the header as an array of newline-terminated strings."""
816
path_as_shown = path.replace('\\', '/')
818
"Index: " + path_as_shown + " (deleted)\n",
819
"===================================================================\n",
822
def make_git_diff_header(target_path, repos_relpath,
823
old_tag, new_tag, add=False, src_label=None,
824
dst_label=None, delete=False, text_changes=True,
825
cp=False, mv=False, copyfrom_path=None,
827
""" Generate the expected 'git diff' header for file TARGET_PATH.
828
REPOS_RELPATH is the location of the path relative to the repository root.
829
The old and new versions ("revision X", or "working copy") must be
830
specified in OLD_TAG and NEW_TAG.
831
SRC_LABEL and DST_LABEL are paths or urls that are added to the diff
832
labels if we're diffing against the repository. ADD, DELETE, CP and MV
833
denotes the operations performed on the file. COPYFROM_PATH is the source
834
of a copy or move. Return the header as an array of newline-terminated
837
path_as_shown = target_path.replace('\\', '/')
839
src_label = src_label.replace('\\', '/')
840
src_label = '\t(.../' + src_label + ')'
844
dst_label = dst_label.replace('\\', '/')
845
dst_label = '\t(.../' + dst_label + ')'
850
"Index: " + path_as_shown + "\n",
851
"===================================================================\n"
855
"diff --git a/" + repos_relpath + " b/" + repos_relpath + "\n",
856
"new file mode 10644\n",
860
"--- /dev/null\t(" + old_tag + ")\n",
861
"+++ b/" + repos_relpath + dst_label + "\t(" + new_tag + ")\n"
865
"diff --git a/" + repos_relpath + " b/" + repos_relpath + "\n",
866
"deleted file mode 10644\n",
870
"--- a/" + repos_relpath + src_label + "\t(" + old_tag + ")\n",
871
"+++ /dev/null\t(" + new_tag + ")\n"
875
copyfrom_rev = '@' + copyfrom_rev
879
"diff --git a/" + copyfrom_path + " b/" + repos_relpath + "\n",
880
"copy from " + copyfrom_path + copyfrom_rev + "\n",
881
"copy to " + repos_relpath + "\n",
885
"--- a/" + copyfrom_path + src_label + "\t(" + old_tag + ")\n",
886
"+++ b/" + repos_relpath + "\t(" + new_tag + ")\n"
890
"diff --git a/" + copyfrom_path + " b/" + path_as_shown + "\n",
891
"rename from " + copyfrom_path + "\n",
892
"rename to " + repos_relpath + "\n",
896
"--- a/" + copyfrom_path + src_label + "\t(" + old_tag + ")\n",
897
"+++ b/" + repos_relpath + "\t(" + new_tag + ")\n"
901
"diff --git a/" + repos_relpath + " b/" + repos_relpath + "\n",
902
"--- a/" + repos_relpath + src_label + "\t(" + old_tag + ")\n",
903
"+++ b/" + repos_relpath + dst_label + "\t(" + new_tag + ")\n",
907
def make_diff_prop_header(path):
908
"""Return a property diff sub-header, as a list of newline-terminated
912
"Property changes on: " + path.replace('\\', '/') + "\n",
913
"___________________________________________________________________\n"
916
def make_diff_prop_val(plus_minus, pval):
917
"Return diff for prop value PVAL, with leading PLUS_MINUS (+ or -)."
918
if len(pval) > 0 and pval[-1] != '\n':
919
return [plus_minus + pval + "\n","\\ No newline at end of property\n"]
920
return [plus_minus + pval]
922
def make_diff_prop_deleted(pname, pval):
923
"""Return a property diff for deletion of property PNAME, old value PVAL.
924
PVAL is a single string with no embedded newlines. Return the result
925
as a list of newline-terminated strings."""
927
"Deleted: " + pname + "\n",
929
] + make_diff_prop_val("-", pval)
931
def make_diff_prop_added(pname, pval):
932
"""Return a property diff for addition of property PNAME, new value PVAL.
933
PVAL is a single string with no embedded newlines. Return the result
934
as a list of newline-terminated strings."""
936
"Added: " + pname + "\n",
938
] + make_diff_prop_val("+", pval)
940
def make_diff_prop_modified(pname, pval1, pval2):
941
"""Return a property diff for modification of property PNAME, old value
942
PVAL1, new value PVAL2.
944
PVAL is a single string with no embedded newlines. A newline at the
945
end is significant: without it, we add an extra line saying '\ No
946
newline at end of property'.
948
Return the result as a list of newline-terminated strings.
951
"Modified: " + pname + "\n",
953
] + make_diff_prop_val("-", pval1) + make_diff_prop_val("+", pval2)