git-svn: teach git-svn to populate svn:mergeinfo

Allow git-svn to populate the svn:mergeinfo property automatically in a narrow range of circumstances. Specifically, when dcommitting a revision with multiple parents, all but (potentially) the first of which have been committed to SVN in the same repository as the target of the dcommit. In this case, the merge info is the union of that given by each of the parents, plus all changes introduced to the first parent by the other parents. In all other cases where a revision to be committed has multiple parents, cause "git svn dcommit" to raise an error rather than completing the commit and potentially losing history information in the upstream SVN repository. This behavior is disabled by default, and can be enabled by setting the svn.pushmergeinfo config option. [ew: minor style changes and manpage merge fix] Acked-by: Eric Wong <normalperson@yhbt.net> Signed-off-by: Bryan Jacobs <bjacobs@woti.com>

Bryan Jacobs committed Sep 7, 2011 at 13:36 UTC 1e5814f3def08452aef4caa703f074ba7d167867
4 files changed +742
Documentation/git-svn.txt
+8
@@ -225,6 +225,14 @@ discouraged.
225 version 1.5 can make use of it. To specify merge information from multiple
226 branches, use a single space character between the branches
227 (`--mergeinfo="/branches/foo:1-10 /branches/bar:3,5-6,8"`)
228 ++
229 +[verse]
230 +config key: svn.pushmergeinfo
231 ++
232 +This option will cause git-svn to attempt to automatically populate the
233 +svn:mergeinfo property in the SVN repository when possible. Currently, this can
234 +only be done when dcommitting non-fast-forward merges where all parents but the
235 +first have already been pushed into SVN.
236
237 'branch'::
238 Create a branch in the SVN repository.
git-svn.perl
+256
@@ -508,6 +508,195 @@ sub cmd_set_tree {
508 unlink $gs->{index};
509 }
510
511 +sub split_merge_info_range {
512 + my ($range) = @_;
513 + if ($range =~ /(\d+)-(\d+)/) {
514 + return (int($1), int($2));
515 + } else {
516 + return (int($range), int($range));
517 + }
518 +}
519 +
520 +sub combine_ranges {
521 + my ($in) = @_;
522 +
523 + my @fnums = ();
524 + my @arr = split(/,/, $in);
525 + for my $element (@arr) {
526 + my ($start, $end) = split_merge_info_range($element);
527 + push @fnums, $start;
528 + }
529 +
530 + my @sorted = @arr [ sort {
531 + $fnums[$a] <=> $fnums[$b]
532 + } 0..$#arr ];
533 +
534 + my @return = ();
535 + my $last = -1;
536 + my $first = -1;
537 + for my $element (@sorted) {
538 + my ($start, $end) = split_merge_info_range($element);
539 +
540 + if ($last == -1) {
541 + $first = $start;
542 + $last = $end;
543 + next;
544 + }
545 + if ($start <= $last+1) {
546 + if ($end > $last) {
547 + $last = $end;
548 + }
549 + next;
550 + }
551 + if ($first == $last) {
552 + push @return, "$first";
553 + } else {
554 + push @return, "$first-$last";
555 + }
556 + $first = $start;
557 + $last = $end;
558 + }
559 +
560 + if ($first != -1) {
561 + if ($first == $last) {
562 + push @return, "$first";
563 + } else {
564 + push @return, "$first-$last";
565 + }
566 + }
567 +
568 + return join(',', @return);
569 +}
570 +
571 +sub merge_revs_into_hash {
572 + my ($hash, $minfo) = @_;
573 + my @lines = split(' ', $minfo);
574 +
575 + for my $line (@lines) {
576 + my ($branchpath, $revs) = split(/:/, $line);
577 +
578 + if (exists($hash->{$branchpath})) {
579 + # Merge the two revision sets
580 + my $combined = "$hash->{$branchpath},$revs";
581 + $hash->{$branchpath} = combine_ranges($combined);
582 + } else {
583 + # Just do range combining for consolidation
584 + $hash->{$branchpath} = combine_ranges($revs);
585 + }
586 + }
587 +}
588 +
589 +sub merge_merge_info {
590 + my ($mergeinfo_one, $mergeinfo_two) = @_;
591 + my %result_hash = ();
592 +
593 + merge_revs_into_hash(\%result_hash, $mergeinfo_one);
594 + merge_revs_into_hash(\%result_hash, $mergeinfo_two);
595 +
596 + my $result = '';
597 + # Sort below is for consistency's sake
598 + for my $branchname (sort keys(%result_hash)) {
599 + my $revlist = $result_hash{$branchname};
600 + $result .= "$branchname:$revlist\n"
601 + }
602 + return $result;
603 +}
604 +
605 +sub populate_merge_info {
606 + my ($d, $gs, $uuid, $linear_refs, $rewritten_parent) = @_;
607 +
608 + my %parentshash;
609 + read_commit_parents(\%parentshash, $d);
610 + my @parents = @{$parentshash{$d}};
611 + if ($#parents > 0) {
612 + # Merge commit
613 + my $all_parents_ok = 1;
614 + my $aggregate_mergeinfo = '';
615 + my $rooturl = $gs->repos_root;
616 +
617 + if (defined($rewritten_parent)) {
618 + # Replace first parent with newly-rewritten version
619 + shift @parents;
620 + unshift @parents, $rewritten_parent;
621 + }
622 +
623 + foreach my $parent (@parents) {
624 + my ($branchurl, $svnrev, $paruuid) =
625 + cmt_metadata($parent);
626 +
627 + unless (defined($svnrev)) {
628 + # Should have been caught be preflight check
629 + fatal "merge commit $d has ancestor $parent, but that change "
630 + ."does not have git-svn metadata!";
631 + }
632 + unless ($branchurl =~ /^$rooturl(.*)/) {
633 + fatal "commit $parent git-svn metadata changed mid-run!";
634 + }
635 + my $branchpath = $1;
636 +
637 + my $ra = Git::SVN::Ra->new($branchurl);
638 + my (undef, undef, $props) =
639 + $ra->get_dir(canonicalize_path("."), $svnrev);
640 + my $par_mergeinfo = $props->{'svn:mergeinfo'};
641 + unless (defined $par_mergeinfo) {
642 + $par_mergeinfo = '';
643 + }
644 + # Merge previous mergeinfo values
645 + $aggregate_mergeinfo =
646 + merge_merge_info($aggregate_mergeinfo,
647 + $par_mergeinfo, 0);
648 +
649 + next if $parent eq $parents[0]; # Skip first parent
650 + # Add new changes being placed in tree by merge
651 + my @cmd = (qw/rev-list --reverse/,
652 + $parent, qw/--not/);
653 + foreach my $par (@parents) {
654 + unless ($par eq $parent) {
655 + push @cmd, $par;
656 + }
657 + }
658 + my @revsin = ();
659 + my ($revlist, $ctx) = command_output_pipe(@cmd);
660 + while (<$revlist>) {
661 + my $irev = $_;
662 + chomp $irev;
663 + my (undef, $csvnrev, undef) =
664 + cmt_metadata($irev);
665 + unless (defined $csvnrev) {
666 + # A child is missing SVN annotations...
667 + # this might be OK, or might not be.
668 + warn "W:child $irev is merged into revision "
669 + ."$d but does not have git-svn metadata. "
670 + ."This means git-svn cannot determine the "
671 + ."svn revision numbers to place into the "
672 + ."svn:mergeinfo property. You must ensure "
673 + ."a branch is entirely committed to "
674 + ."SVN before merging it in order for "
675 + ."svn:mergeinfo population to function "
676 + ."properly";
677 + }
678 + push @revsin, $csvnrev;
679 + }
680 + command_close_pipe($revlist, $ctx);
681 +
682 + last unless $all_parents_ok;
683 +
684 + # We now have a list of all SVN revnos which are
685 + # merged by this particular parent. Integrate them.
686 + next if $#revsin == -1;
687 + my $newmergeinfo = "$branchpath:" . join(',', @revsin);
688 + $aggregate_mergeinfo =
689 + merge_merge_info($aggregate_mergeinfo,
690 + $newmergeinfo, 1);
691 + }
692 + if ($all_parents_ok and $aggregate_mergeinfo) {
693 + return $aggregate_mergeinfo;
694 + }
695 + }
696 +
697 + return undef;
698 +}
699 +
700 sub cmd_dcommit {
701 my $head = shift;
702 command_noisy(qw/update-index --refresh/);
@@ -558,6 +747,62 @@ sub cmd_dcommit {
747 "without --no-rebase may be required."
748 }
749 my $expect_url = $url;
750 +
751 + my $push_merge_info = eval {
752 + command_oneline(qw/config --get svn.pushmergeinfo/)
753 + };
754 + if (not defined($push_merge_info)
755 + or $push_merge_info eq "false"
756 + or $push_merge_info eq "no"
757 + or $push_merge_info eq "never") {
758 + $push_merge_info = 0;
759 + }
760 +
761 + unless (defined($_merge_info) || ! $push_merge_info) {
762 + # Preflight check of changes to ensure no issues with mergeinfo
763 + # This includes check for uncommitted-to-SVN parents
764 + # (other than the first parent, which we will handle),
765 + # information from different SVN repos, and paths
766 + # which are not underneath this repository root.
767 + my $rooturl = $gs->repos_root;
768 + foreach my $d (@$linear_refs) {
769 + my %parentshash;
770 + read_commit_parents(\%parentshash, $d);
771 + my @realparents = @{$parentshash{$d}};
772 + if ($#realparents > 0) {
773 + # Merge commit
774 + shift @realparents; # Remove/ignore first parent
775 + foreach my $parent (@realparents) {
776 + my ($branchurl, $svnrev, $paruuid) = cmt_metadata($parent);
777 + unless (defined $paruuid) {
778 + # A parent is missing SVN annotations...
779 + # abort the whole operation.
780 + fatal "$parent is merged into revision $d, "
781 + ."but does not have git-svn metadata. "
782 + ."Either dcommit the branch or use a "
783 + ."local cherry-pick, FF merge, or rebase "
784 + ."instead of an explicit merge commit.";
785 + }
786 +
787 + unless ($paruuid eq $uuid) {
788 + # Parent has SVN metadata from different repository
789 + fatal "merge parent $parent for change $d has "
790 + ."git-svn uuid $paruuid, while current change "
791 + ."has uuid $uuid!";
792 + }
793 +
794 + unless ($branchurl =~ /^$rooturl(.*)/) {
795 + # This branch is very strange indeed.
796 + fatal "merge parent $parent for $d is on branch "
797 + ."$branchurl, which is not under the "
798 + ."git-svn root $rooturl!";
799 + }
800 + }
801 + }
802 + }
803 + }
804 +
805 + my $rewritten_parent;
806 Git::SVN::remove_username($expect_url);
807 if (defined($_merge_info)) {
808 $_merge_info =~ tr{ }{\n};
@@ -575,6 +820,14 @@ sub cmd_dcommit {
820 print "diff-tree $d~1 $d\n";
821 } else {
822 my $cmt_rev;
823 +
824 + unless (defined($_merge_info) || ! $push_merge_info) {
825 + $_merge_info = populate_merge_info($d, $gs,
826 + $uuid,
827 + $linear_refs,
828 + $rewritten_parent);
829 + }
830 +
831 my %ed_opts = ( r => $last_rev,
832 log => get_commit_entry($d)->{log},
833 ra => Git::SVN::Ra->new($url),
@@ -617,6 +870,9 @@ sub cmd_dcommit {
870 @finish = qw/reset --mixed/;
871 }
872 command_noisy(@finish, $gs->refname);
873 +
874 + $rewritten_parent = command_oneline(qw/rev-parse HEAD/);
875 +
876 if (@diff) {
877 @refs = ();
878 my ($url_, $rev_, $uuid_, $gs_) =
t/t9160-git-svn-mergeinfo-push.sh new
+104
@@ -0,0 +1,104 @@
1 +#!/bin/sh
2 +#
3 +# Portions copyright (c) 2007, 2009 Sam Vilain
4 +# Portions copyright (c) 2011 Bryan Jacobs
5 +#
6 +
7 +test_description='git-svn svn mergeinfo propagation'
8 +
9 +. ./lib-git-svn.sh
10 +
11 +test_expect_success 'load svn dump' "
12 + svnadmin load -q '$rawsvnrepo' \
13 + < '$TEST_DIRECTORY/t9160/branches.dump' &&
14 + git svn init --minimize-url -R svnmerge \
15 + -T trunk -b branches '$svnrepo' &&
16 + git svn fetch --all
17 + "
18 +
19 +test_expect_success 'propagate merge information' '
20 + git config svn.pushmergeinfo yes &&
21 + git checkout svnb1 &&
22 + git merge --no-ff svnb2 &&
23 + git svn dcommit
24 + '
25 +
26 +test_expect_success 'check svn:mergeinfo' '
27 + mergeinfo=$(svn_cmd propget svn:mergeinfo "$svnrepo"/branches/svnb1)
28 + test "$mergeinfo" = "/branches/svnb2:3,8"
29 + '
30 +
31 +test_expect_success 'merge another branch' '
32 + git merge --no-ff svnb3 &&
33 + git svn dcommit
34 + '
35 +
36 +test_expect_success 'check primary parent mergeinfo respected' '
37 + mergeinfo=$(svn_cmd propget svn:mergeinfo "$svnrepo"/branches/svnb1)
38 + test "$mergeinfo" = "/branches/svnb2:3,8
39 +/branches/svnb3:4,9"
40 + '
41 +
42 +test_expect_success 'merge existing merge' '
43 + git merge --no-ff svnb4 &&
44 + git svn dcommit
45 + '
46 +
47 +test_expect_success "check both parents' mergeinfo respected" '
48 + mergeinfo=$(svn_cmd propget svn:mergeinfo "$svnrepo"/branches/svnb1)
49 + test "$mergeinfo" = "/branches/svnb2:3,8
50 +/branches/svnb3:4,9
51 +/branches/svnb4:5-6,10-12
52 +/branches/svnb5:6,11"
53 + '
54 +
55 +test_expect_success 'make further commits to branch' '
56 + git checkout svnb2 &&
57 + touch newb2file &&
58 + git add newb2file &&
59 + git commit -m "later b2 commit" &&
60 + touch newb2file-2 &&
61 + git add newb2file-2 &&
62 + git commit -m "later b2 commit 2" &&
63 + git svn dcommit
64 + '
65 +
66 +test_expect_success 'second forward merge' '
67 + git checkout svnb1 &&
68 + git merge --no-ff svnb2 &&
69 + git svn dcommit
70 + '
71 +
72 +test_expect_success 'check new mergeinfo added' '
73 + mergeinfo=$(svn_cmd propget svn:mergeinfo "$svnrepo"/branches/svnb1)
74 + test "$mergeinfo" = "/branches/svnb2:3,8,16-17
75 +/branches/svnb3:4,9
76 +/branches/svnb4:5-6,10-12
77 +/branches/svnb5:6,11"
78 + '
79 +
80 +test_expect_success 'reintegration merge' '
81 + git checkout svnb4 &&
82 + git merge --no-ff svnb1 &&
83 + git svn dcommit
84 + '
85 +
86 +test_expect_success 'check reintegration mergeinfo' '
87 + mergeinfo=$(svn_cmd propget svn:mergeinfo "$svnrepo"/branches/svnb4)
88 + test "$mergeinfo" = "/branches/svnb1:2-4,7-9,13-18
89 +/branches/svnb2:3,8,16-17
90 +/branches/svnb3:4,9
91 +/branches/svnb4:5-6,10-12
92 +/branches/svnb5:6,11"
93 + '
94 +
95 +test_expect_success 'dcommit a merge at the top of a stack' '
96 + git checkout svnb1 &&
97 + touch anotherfile &&
98 + git add anotherfile &&
99 + git commit -m "a commit" &&
100 + git merge svnb4 &&
101 + git svn dcommit
102 + '
103 +
104 +test_done
t/t9160/branches.dump new
+374
@@ -0,0 +1,374 @@
1 +SVN-fs-dump-format-version: 2
2 +
3 +UUID: 1ef08553-f2d1-45df-b38c-19af6b7c926d
4 +
5 +Revision-number: 0
6 +Prop-content-length: 56
7 +Content-length: 56
8 +
9 +K 8
10 +svn:date
11 +V 27
12 +2011-09-02T16:08:02.941384Z
13 +PROPS-END
14 +
15 +Revision-number: 1
16 +Prop-content-length: 114
17 +Content-length: 114
18 +
19 +K 7
20 +svn:log
21 +V 12
22 +Base commit
23 +
24 +K 10
25 +svn:author
26 +V 7
27 +bjacobs
28 +K 8
29 +svn:date
30 +V 27
31 +2011-09-02T16:08:27.205062Z
32 +PROPS-END
33 +
34 +Node-path: branches
35 +Node-kind: dir
36 +Node-action: add
37 +Prop-content-length: 10
38 +Content-length: 10
39 +
40 +PROPS-END
41 +
42 +
43 +Node-path: trunk
44 +Node-kind: dir
45 +Node-action: add
46 +Prop-content-length: 10
47 +Content-length: 10
48 +
49 +PROPS-END
50 +
51 +
52 +Revision-number: 2
53 +Prop-content-length: 121
54 +Content-length: 121
55 +
56 +K 7
57 +svn:log
58 +V 19
59 +Create branch svnb1
60 +K 10
61 +svn:author
62 +V 7
63 +bjacobs
64 +K 8
65 +svn:date
66 +V 27
67 +2011-09-02T16:09:43.628137Z
68 +PROPS-END
69 +
70 +Node-path: branches/svnb1
71 +Node-kind: dir
72 +Node-action: add
73 +Node-copyfrom-rev: 1
74 +Node-copyfrom-path: trunk
75 +
76 +
77 +Revision-number: 3
78 +Prop-content-length: 121
79 +Content-length: 121
80 +
81 +K 7
82 +svn:log
83 +V 19
84 +Create branch svnb2
85 +K 10
86 +svn:author
87 +V 7
88 +bjacobs
89 +K 8
90 +svn:date
91 +V 27
92 +2011-09-02T16:09:46.339930Z
93 +PROPS-END
94 +
95 +Node-path: branches/svnb2
96 +Node-kind: dir
97 +Node-action: add
98 +Node-copyfrom-rev: 1
99 +Node-copyfrom-path: trunk
100 +
101 +
102 +Revision-number: 4
103 +Prop-content-length: 121
104 +Content-length: 121
105 +
106 +K 7
107 +svn:log
108 +V 19
109 +Create branch svnb3
110 +K 10
111 +svn:author
112 +V 7
113 +bjacobs
114 +K 8
115 +svn:date
116 +V 27
117 +2011-09-02T16:09:49.394515Z
118 +PROPS-END
119 +
120 +Node-path: branches/svnb3
121 +Node-kind: dir
122 +Node-action: add
123 +Node-copyfrom-rev: 1
124 +Node-copyfrom-path: trunk
125 +
126 +
127 +Revision-number: 5
128 +Prop-content-length: 121
129 +Content-length: 121
130 +
131 +K 7
132 +svn:log
133 +V 19
134 +Create branch svnb4
135 +K 10
136 +svn:author
137 +V 7
138 +bjacobs
139 +K 8
140 +svn:date
141 +V 27
142 +2011-09-02T16:09:54.114607Z
143 +PROPS-END
144 +
145 +Node-path: branches/svnb4
146 +Node-kind: dir
147 +Node-action: add
148 +Node-copyfrom-rev: 1
149 +Node-copyfrom-path: trunk
150 +
151 +
152 +Revision-number: 6
153 +Prop-content-length: 121
154 +Content-length: 121
155 +
156 +K 7
157 +svn:log
158 +V 19
159 +Create branch svnb5
160 +K 10
161 +svn:author
162 +V 7
163 +bjacobs
164 +K 8
165 +svn:date
166 +V 27
167 +2011-09-02T16:09:58.602623Z
168 +PROPS-END
169 +
170 +Node-path: branches/svnb5
171 +Node-kind: dir
172 +Node-action: add
173 +Node-copyfrom-rev: 1
174 +Node-copyfrom-path: trunk
175 +
176 +
177 +Revision-number: 7
178 +Prop-content-length: 110
179 +Content-length: 110
180 +
181 +K 7
182 +svn:log
183 +V 9
184 +b1 commit
185 +K 10
186 +svn:author
187 +V 7
188 +bjacobs
189 +K 8
190 +svn:date
191 +V 27
192 +2011-09-02T16:10:20.292369Z
193 +PROPS-END
194 +
195 +Node-path: branches/svnb1/b1file
196 +Node-kind: file
197 +Node-action: add
198 +Prop-content-length: 10
199 +Text-content-length: 0
200 +Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
201 +Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
202 +Content-length: 10
203 +
204 +PROPS-END
205 +
206 +
207 +Revision-number: 8
208 +Prop-content-length: 110
209 +Content-length: 110
210 +
211 +K 7
212 +svn:log
213 +V 9
214 +b2 commit
215 +K 10
216 +svn:author
217 +V 7
218 +bjacobs
219 +K 8
220 +svn:date
221 +V 27
222 +2011-09-02T16:10:38.429199Z
223 +PROPS-END
224 +
225 +Node-path: branches/svnb2/b2file
226 +Node-kind: file
227 +Node-action: add
228 +Prop-content-length: 10
229 +Text-content-length: 0
230 +Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
231 +Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
232 +Content-length: 10
233 +
234 +PROPS-END
235 +
236 +
237 +Revision-number: 9
238 +Prop-content-length: 110
239 +Content-length: 110
240 +
241 +K 7
242 +svn:log
243 +V 9
244 +b3 commit
245 +K 10
246 +svn:author
247 +V 7
248 +bjacobs
249 +K 8
250 +svn:date
251 +V 27
252 +2011-09-02T16:10:52.843023Z
253 +PROPS-END
254 +
255 +Node-path: branches/svnb3/b3file
256 +Node-kind: file
257 +Node-action: add
258 +Prop-content-length: 10
259 +Text-content-length: 0
260 +Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
261 +Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
262 +Content-length: 10
263 +
264 +PROPS-END
265 +
266 +
267 +Revision-number: 10
268 +Prop-content-length: 110
269 +Content-length: 110
270 +
271 +K 7
272 +svn:log
273 +V 9
274 +b4 commit
275 +K 10
276 +svn:author
277 +V 7
278 +bjacobs
279 +K 8
280 +svn:date
281 +V 27
282 +2011-09-02T16:11:17.489870Z
283 +PROPS-END
284 +
285 +Node-path: branches/svnb4/b4file
286 +Node-kind: file
287 +Node-action: add
288 +Prop-content-length: 10
289 +Text-content-length: 0
290 +Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
291 +Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
292 +Content-length: 10
293 +
294 +PROPS-END
295 +
296 +
297 +Revision-number: 11
298 +Prop-content-length: 110
299 +Content-length: 110
300 +
301 +K 7
302 +svn:log
303 +V 9
304 +b5 commit
305 +K 10
306 +svn:author
307 +V 7
308 +bjacobs
309 +K 8
310 +svn:date
311 +V 27
312 +2011-09-02T16:11:32.277404Z
313 +PROPS-END
314 +
315 +Node-path: branches/svnb5/b5file
316 +Node-kind: file
317 +Node-action: add
318 +Prop-content-length: 10
319 +Text-content-length: 0
320 +Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
321 +Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
322 +Content-length: 10
323 +
324 +PROPS-END
325 +
326 +
327 +Revision-number: 12
328 +Prop-content-length: 192
329 +Content-length: 192
330 +
331 +K 7
332 +svn:log
333 +V 90
334 +Merge remote-tracking branch 'svnb5' into HEAD
335 +
336 +* svnb5:
337 + b5 commit
338 + Create branch svnb5
339 +K 10
340 +svn:author
341 +V 7
342 +bjacobs
343 +K 8
344 +svn:date
345 +V 27
346 +2011-09-02T16:11:54.274722Z
347 +PROPS-END
348 +
349 +Node-path: branches/svnb4
350 +Node-kind: dir
351 +Node-action: change
352 +Prop-content-length: 56
353 +Content-length: 56
354 +
355 +K 13
356 +svn:mergeinfo
357 +V 21
358 +/branches/svnb5:6,11
359 +
360 +PROPS-END
361 +
362 +
363 +Node-path: branches/svnb4/b5file
364 +Node-kind: file
365 +Node-action: add
366 +Prop-content-length: 10
367 +Text-content-length: 0
368 +Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
369 +Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
370 +Content-length: 10
371 +
372 +PROPS-END
373 +
374 +