-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpcvs2git.pike
More file actions
5465 lines (4968 loc) · 158 KB
/
Copy pathpcvs2git.pike
File metadata and controls
5465 lines (4968 loc) · 158 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// Conversion utility for cvs to git migration.
//
// 2009-10-02 Henrik Grubbström
//
// Problem:
//
// * CVS maintains a separate revision history for each file.
// * CVS tags may span a subset of all files.
// * CVS does not tag files which were dead at tag time.
//
// * Git maintains a common commit history for all files in the repository.
// * Git tags the entire set of files belonging to a commit.
//
// * We want as much of the original history as possible to be converted.
//
// Approach:
//
// From a graph-theoretical point of view, what we want to do
// is to construct a minimum spanning DAG of a partial order relation:
//
// Commit(X) <= Commit(Y) if (X.timestamp <= Y.timestamp)
//
// Commit(X) < Commit(Y) if (Y.parent == X)
//
// Commit(X) < Commit(Y) if (Y == X.child)
//
// Commit(X) <= Commit(Y) if (Y.leaves <= X.leaves)
//
// while maintaining the reachability from the tags (leaves).
// Note also that the assignment of original leaves to nodes
// may not change (leaves that aren't original may be added
// though).
//
// Method:
//
// To convert from CVS to Git, we first generate single file commit graphs
// for each of the files from CVS. We then create join nodes for all of
// the branches and tags spanning the set of revisions associated with the
// tag or branch.
//
// We at this point then typically have a commit-graph where we have
// a few commits with lots of parents, and lots of commits with just
// a single parent, as well as lots of roots (one per CVS file).
//
// Note: The graph from this stage could be exported and contains all
// the history, but wouldn't be very useful. All the following passes
// attempt to make the graph more like what was in CVS at the time
// the files were committed.
//
// Next we generate a total order that attempts to preserve the
// parent-child order with a primary timestamp-based ordering.
//
// Then we attempt to identify mergeable nodes that have the same
// set of timestamp, author, message and leaves. This pass is
// separated from the next due to the unreliability of the timestamps.
//
// The next step is building the DAG from scratch, by starting with
// the oldest node, and then attaching nodes in total ordering order
// to the most recent nodes in the DAG that aren't yet reachable
// (ie in the ancestor set) and are compatible.
//
// At the final phase, we attempt to reduce the amount of extra nodes,
// by replacing leaf nodes having a single parent with their parent.
//
// Each commit node contains two sets of leaves: leaves and dead_leaves.
// leaves is the set of leaves that the node is reachable from via
// parent links.
// dead_leaves is the set of leaves that the node is incompatible with.
// Any other leaves may be (re-)attached during processing.
//
// TODO:
//
// o Analyze the committed Id strings to find renames and merges.
// Note that merge links must be kept separate from the ordinary
// parent-child links, since leafs shouldn't propagate over them.
//
// FEATURES
//
// o Uses git-fast-import to do the actual import into git
// for optimal speed.
//
// o The git import phase is started in parallel with the
// init git from RCS phase. This reduces the working set
// and potentially speed up the total running time
// (at least on multi-cpu machines).
//
// o The tags (and branches) are created at commit time.
// This allows for observing of the git repository
// during its creation if suitable sequence points are
// added.
//
// o Converts the expand RCS keyword settings to the corresponding
// .gitattributes files.
//
// o Converts .cvsignore files to the corresponding .gitignore files.
//
// o Keyword expansion and filtering (-k) is supported.
//
// o Supports differing author and committer.
//
// o Supports simulating import from a remote git repository (--remote).
//
#define USE_BITMASKS
#define USE_FAST_IMPORT
#ifdef LEAF_DEBUG
#define LEAF_SPLIT_DEBUG
#define LEAF_MERGE_DEBUG
#endif
//! Fuzz in seconds (15 minutes).
constant FUZZ = 15*60;
enum Flags {
FLAG_PRETEND = 1,
FLAG_DETECT_MERGES = 2,
FLAG_QUIET = 4,
FLAG_NO_KEYWORDS = 8,
FLAG_HEADER = 16,
FLAG_LINEAR = 32,
FLAG_DISABLE_REV = 64,
};
#if 0
constant termination_uuid = "src/modules/_Crypto/Makefile:1.2";
#else
constant termination_uuid = 0;
#endif
void progress(Flags flags, sprintf_format fmt, sprintf_args ... args)
{
if (flags & FLAG_QUIET) return;
werror(fmt, @args);
}
//! The filepatterns that are ignored by CVS by default.
//!
//! The list is taken from the CVS 1.12.12 manual.
constant default_cvsignore = ({
"RCS",
"SCCS",
"CVS",
"CVS.adm",
"RCSLOG",
"cvslog.*",
"tags",
"TAGS",
".make.state",
".nse_depinfo",
"*~",
"#*",
".#*",
",*",
"_$*",
"*$",
"*.old",
"*.bak",
"*.BAK",
"*.orig",
"*.rej",
".del-*",
"*.a",
"*.olb",
"*.o",
"*.obj",
"*.so",
"*.exe",
"*.Z",
"*.elc",
"*.ln",
"core",
});
//! Mapping from sha string to content for selected files.
//!
//! Currently used for .cvsignore.
mapping(string:string) file_contents = ([]);
//! Convert a cvsignore file to the corresponding gitignore file.
string convert_cvsignore(string data)
{
// FIXME: Support '!'.
return map(data/"\n",
lambda(string line) {
if (sizeof(line)) {
return "/" + line;
}
return line;
}) * "\n";
}
//! The set of filename extensions we've seen so far.
//!
//! This is used for creation of the .gitattributes files.
multiset(string) extensions = (<>);
//! Quote against fnmatch(3C) suitable for .gitattributes
//! (or .gitignore).
protected string fnquote(string fname)
{
// NB: Due to the rules of the .gitattributes parser in
// git(1) it is not possible to quote whitespace.
return replace(fname, ([ "\\": "\\\\",
"\"": "\\\"",
"\'": "\\\'",
"[" : "\\[",
"]" : "\\]",
" " : "?",
"\t": "?",
"*" : "\\*",
"?" : "\\?" ]));
}
//! Get the file extension glob of a filename.
//!
//! @returns
//! Returns @expr{basename(filename)@} if there's no extension.
string file_extension_glob(string filename)
{
filename = basename(filename);
if (!has_value(filename, ".")) return fnquote(filename);
return "*." + fnquote((filename/".")[-1]);
}
enum RevisionFlags {
EXPAND_BINARY = 0, // -kb
EXPAND_LF = 1, //
EXPAND_CRLF = 2, //
EXPAND_TEXT = 3, // -ko Text file, don't care about EOL convention.
EXPAND_KEYWORDS = 4, //
EXPAND_ALL = 7, // -kkv (default)
EXPAND_GUESS = 8, // Use the default heuristics to determine flags.
EXPAND_GOT_KEYWORD = 16, // File contains an active keyword.
REVISION_COPY = 32, // The revision is a copy, don't delete the original.
REVISION_MERGE = 64, // The revision is a merge. The ancestor is soft.
EXPAND_MAGIC = 128,
};
class RCSFile
{
inherit Parser.RCS;
//! Mapping from branch revision (a.b.c) to the most recent commit on
//! the branch.
mapping(string:string) branch_heads = ([]);
//! Find the heads of all branches, and reverse the linkage
//! so that it is consistent with the trunk.
void find_branch_heads()
{
foreach(branches; string branch_rev; string name) {
string branch_point = (branch_rev/".")[..<1] * ".";
Revision rev = revisions[branch_point];
if (!rev) {
werror("%s: Failed to find branch point for branch %s:%s (%s)\n",
rcs_file_name, name, branch_rev, branch_point);
continue;
}
foreach(rev->branches, string br) {
if (has_prefix(br, branch_rev + ".")) {
// Typically branch_rev + ".1".
// rev->branches -= ({ br });
do {
rev = revisions[br];
branch_point = br;
br = rev->next;
} while (br);
break;
}
}
branch_heads[branch_rev] = branch_point;
}
}
protected void set_default_path(string path, string|void display_path)
{
foreach(revisions;;Revision rev) {
rev->path = path;
rev->display_path = display_path||path;
}
}
void create(string rcs_file, string path, string|void data,
string|void display_path)
{
::create(rcs_file, data);
set_default_path(path, display_path);
find_branch_heads();
}
//! Append a revision
//!
//! @param base
//! The revision to base the new revision on.
//!
//! @param ancestor
//! The revision to have as immediate ancestor for the new revision.
//! @expr{0@} (zero) for a new root commit.
Revision append_revision(string base, string ancestor,
Calendar.TimeRange rcs_time,
string author, string message, string|void rev,
string|void state)
{
Revision parent = revisions[ancestor];
if (ancestor && !parent) return UNDEFINED;
Revision new_rev;
if (!rev) {
int i;
do {
// Use a revision number that isn't used by cvs.
rev = sprintf("%s%c", ancestor || base, i + 'a');
i++;
} while (revisions[rev]);
} else if (new_rev = revisions[rev]) return new_rev;
Revision base_rev = revisions[base];
new_rev = FakeRevision(rev, base_rev, rcs_time, author, message);
new_rev->state = state || base_rev->state;
new_rev->ancestor = ancestor;
// Reparent the other children to parent, so that we are inserted
// in their history, but only if we're not on a new branch.
if (!ancestor || !has_prefix(rev, ancestor + ".")) {
foreach(revisions;; Revision r) {
if ((r->ancestor == ancestor) && (!ancestor || (r->time > rcs_time))) {
r->ancestor = rev;
}
}
}
revisions[rev] = new_rev;
return new_rev;
}
//! Differs from the original in that it updates
//! the custom fields @expr{sha@} and @expr{expand@} as well.
string get_contents_for_revision(string|Revision rev)
{
if (stringp(rev)) rev = revisions[rev];
if (!rev) return 0;
if (!rev->rcs_text) rev->rcs_text = ""; // Paranoia.
string data = ::get_contents_for_revision(rev);
if (rev->sha) return data;
// Update sha
if (data && rev->state != "dead") {
rev->sha = Crypto.SHA1()->update(data)->digest();
} else {
rev->sha = "\0"*20; // Death marker.
}
// Update expand
if (rev->revision_flags & EXPAND_GUESS) {
rev->revision_flags &= ~(EXPAND_GUESS|EXPAND_ALL);
RevisionFlags flags = EXPAND_ALL;
if (expand == "b") {
flags = EXPAND_BINARY;
} else {
if (expand == "o") flags = EXPAND_TEXT;
// A paranoia check for invalid expand markup.
if (data && (has_prefix(data, "%!PS") || has_value(data, "\0"))) {
flags = EXPAND_BINARY;
} else if (data && has_value(data, "\r")) {
if (replace(replace(data, "\r", ""), "\n", "\r\n") == data) {
// CRLF-conversion is safe.
flags &= ~EXPAND_LF;
} else {
flags &= ~EXPAND_TEXT;
}
}
}
rev->revision_flags |= flags;
}
return data;
}
//! Differs from the original in that it supports the
//! custom field @expr{path@} of Id and RCSFile, and
//! uses a @[String.Buffer] to build the result.
//!
//! Also uses a somewhat different approach to find the
//! RCS keywords to expand.
//!
//! It also supports a negative value for @[override_binary]
//! to enable stripping of keyword data.
string expand_keywords_for_revision( string|Revision rev, string|void text,
int|void override_binary )
{
if( stringp( rev ) ) rev = revisions[rev];
if( !rev ) return 0;
if( !text ) text = get_contents_for_revision( rev );
if( !(rev->revision_flags & EXPAND_KEYWORDS) && (override_binary <= 0) )
return text;
array(string) segments = text/"$";
if (sizeof(segments) < 3) return text; // Common case.
Calendar.TimeRange time = rev->time;
string state = rev->state;
string revision = rev->revision;
string author = rev->author;
if (rev->is_fake_revision) {
// Hide the fake information.
revision = rev->base_rev;
state = revisions[revision]->state;
time = revisions[revision]->time;
author = revisions[revision]->author;
}
string date = replace( time->format_time(), "-", "/" );
string file;
if (rev->path) {
file = basename(rev->path) + ",v";
} else {
file = basename(rcs_file_name);
}
mapping kws = ([ "Author" : author,
"Date" : date,
"Header" : ({ rcs_file_name, revision, date,
author, state }) * " ",
"Id" : ({ file, revision, date,
author, state }) * " ",
"Name" : "", // only applies to a checked-out file
"Locker" : search( locks, revision ) || "",
/*"Log" : "A horrible mess, at best", */
"RCSfile" : file,
"Revision" : revision,
"Source" : rcs_file_name,
"State" : state ]);
int got_keyword;
String.Buffer result = String.Buffer();
int i;
result->add(segments[0]);
for (i = 1; i < sizeof(segments)-1; i++) {
string segment = segments[i];
if (!has_value(segment, "\n")) {
sscanf(segment, "%[a-zA-Z]%s", string keyword, string rest);
if (sizeof(keyword) && (!sizeof(rest) || has_prefix(rest, ":"))) {
string expansion;
if (expansion = kws[keyword]) {
result->add("$", keyword);
if (!override_binary) {
result->add(": ", expansion, " ");
}
segment = segments[++i];
got_keyword++;
}
}
}
result->add("$", segment);
}
if (i < sizeof(segments)) {
// Trailer.
result->add("$", segments[-1]);
}
if (got_keyword) {
rev->revision_flags |= EXPAND_GOT_KEYWORD;
}
return result->get();
}
//! Same as @[RCS.Revision], but with some additional fields.
class Revision
{
//! Inherits the generic Revision.
inherit RCS::Revision;
//! Actual author (if other than committer).
string actual_author;
//! The destination path for checkout.
string path;
//! The path to display in the commit message.
string display_path;
//! The SHA1 hash of the data as checked out.
string sha;
//! The keyword expansion rules and other flags for this revision.
RevisionFlags revision_flags = EXPAND_GUESS;
//! Optional list of merge links.
array(string) merges;
}
//! Revisions that don't actually exist in the RCS file.
//!
//! Used to keep track of out of band changes.
class FakeRevision
{
inherit Revision;
constant is_fake_revision = 1;
string base_rev;
//! Create the specified revision based on @[base].
protected void create(string rev, Revision base, Calendar.TimeRange time,
string author, string message)
{
revision = rev;
base_rev = base->is_fake_revision?base->base_rev:base->revision;
path = base->path;
sha = base->sha;
text = base->text;
revision_flags = base->revision_flags & ~(REVISION_COPY|REVISION_MERGE);
this_program::time = time;
this_program::author = author;
this_program::log = message;
// Some magic to get the content correct...
rcs_text = ""; // No differences from
rcs_prev = base->revision; // our parent.
}
}
}
//! @appears GitRepository
//!
//! A git repository.
//!
//! This class is @[add_constant()]ed before compiling the
//! configuration file.
class GitRepository
{
//! Indicates that the repository needs a pre-commit hook for
//! handling blocking of commit of expanded CVS identifiers.
int need_pre_commit_hook;
//! @appears GitHandler
//!
//! A custom repository handler.
//!
//! This class is @[add_constant()]ed before compiling the
//! configuration file.
class GitHandler(GitRepository git, Flags git_flags)
{
//! The RCS file was renamed at revision @[rev].
//!
//! @param rev
//! The first revision on @[new_path].
//! @[UNDEFINED] to rename all revisions of the file.
protected void rename_revision(RCSFile rcs_file, string old_path,
string new_path, string rev)
{
#if 0
werror("rename_revision(%O, %O, %O, %O)\n",
rcs_file, old_path, new_path, rev);
#endif
if (!rev) {
// Move all revisions.
foreach(rcs_file->revisions;; RCSFile.Revision r) {
if (r->path == new_path) {
r->path = old_path;
if (r->display_path && has_suffix(r->display_path, new_path)) {
r->display_path =
r->display_path[..<sizeof(new_path)] + old_path;
}
}
}
} else {
RCSFile.Revision root_rev = rcs_file->revisions[rev];
if (!root_rev) return;
RCSFile.Revision r = root_rev;
while (r = rcs_file->revisions[r->ancestor]) {
if (r->path == new_path) r->path = old_path;
if (r->display_path && has_suffix(r->display_path, new_path)) {
r->display_path =
r->display_path[..<sizeof(new_path)] + old_path;
}
}
foreach(rcs_file->revisions;; r) {
if ((r->path == new_path) && (r->time < root_rev->time)) {
r->path = old_path;
if (r->display_path && has_suffix(r->display_path, new_path)) {
r->display_path =
r->display_path[..<sizeof(new_path)] + old_path;
}
}
}
}
#if 0
foreach(map(sort(indices(rcs_file->revisions)), rcs_file->revisions),
RCSFile.Revision r) {
werror("\t%O\t%O:%O\n", r->revision, r->path, r->display_path);
}
#endif /* 0 */
}
//! The RCS file was copied from @[old_path] at revision @[rev].
//!
//! @param rev
//! The first revision on @[new_path].
protected void copy_revision(RCSFile rcs_file, string old_path,
string new_path, string rev)
{
rename_revision(rcs_file, old_path, new_path, rev);
// Mark the revision as a copy.
RCSFile.Revision root_rev = rcs_file->revisions[rev];
if (!root_rev) return;
root_rev->revision_flags |= REVISION_COPY;
}
//! Hide a specific revision.
protected void hide_revision(RCSFile rcs_file, string rev)
{
RCSFile.Revision r = rcs_file->revisions[rev];
if (r) r->path = UNDEFINED;
}
//! Convert rcs_time to a @[Calendar.TimeRange].
protected Calendar.TimeRange parse_rcs_time(string rcs_time)
{
return Calendar.ISO.parse("%y.%M.%D.%h.%m.%s %z",
rcs_time + " UTC");
}
//! Find the revision that was current on branch @[branch] at
//! @[rcs_time].
//!
//! @param branch
//! @mixed
//! @value 0
//! The main branch.
//! @value ""
//! The branch indicated by @[rcs_file->branch] if any,
//! otherwise the branch @expr{"1.1.1"@}.
//! @value "tag"
//! The branch with the tag @[branch] if any, otherwise
//! the main branch.
//! @endmixed
protected string find_revision(RCSFile rcs_file, string branch,
string rcs_time)
{
if (rcs_time[2] == '.') rcs_time = "19" + rcs_time;
Calendar.TimeRange time = Calendar.ISO.parse("%y.%M.%D.%h.%m.%s %z",
rcs_time + " UTC");
// Get a suitable starting revision.
string prev_rev;
if (branch == "") {
prev_rev = rcs_file->branch || "1.1.1";
} else {
prev_rev = rcs_file->tags[branch] || rcs_file->head;
}
if (rcs_file->symbol_is_branch(prev_rev) || (prev_rev == "1.1.1")) {
// FIXME: Use rcs_file->branch_heads
string branch_prefix;
if (prev_rev == "1.1.1") {
branch_prefix = "1.1.1";
prev_rev = "1.1";
// NB: In some obscure cases version 1.1 doesn't exist.
// In that case we start at HEAD, and scan until we reach
// the root revision.
if (!rcs_file->revisions[prev_rev]) {
prev_rev = rcs_file->head;
while(rcs_file->revisions[prev_rev]->ancestor) {
prev_rev = rcs_file->revisions[prev_rev]->ancestor;
}
branch_prefix = prev_rev + ".1";
}
} else {
array(string) frags = prev_rev/".";
prev_rev = frags[..<2]*".";
branch_prefix = prev_rev + "." + frags[-1];
}
prev_rev = rcs_file->branch_heads[branch_prefix];
}
// At this point prev_rev is a revision at the end of a branch.
// Search for the first revision that is older than rcs_time.
for(RCSFile.Revision r = rcs_file->revisions[prev_rev];
r && r->time >= time; r = rcs_file->revisions[prev_rev]) {
prev_rev = r->ancestor;
}
return prev_rev;
}
//! Add a new branch @[branch] rooted at @[rev].
//!
//! @returns
//! Returns the new branch prefix.
protected string add_branch(RCSFile rcs_file, string branch, string rev)
{
if (!rev) return UNDEFINED;
string branch_prefix;
if (branch_prefix = rcs_file->tags[branch]) {
// The branch already exists.
// FIXME: Not supported yet.
error("Branch %O already exists!\n", branch);
}
// Create a new branch.
branch_prefix = rev + ".0.";
multiset(string) existing_branches = (<>);
foreach(rcs_file->tags;;string r) {
if (has_prefix(r, branch_prefix)) {
existing_branches[r] = 1;
}
}
int i;
for (i = 2; existing_branches[branch_prefix + i]; i+=2)
;
#if 0
werror("Creating a new branch: %O\n", branch_prefix + i);
#endif
rcs_file->tags[branch] = branch_prefix + i;
branch_prefix = rev + "." + i;
rcs_file->branches[branch_prefix] = branch;
rcs_file->branch_heads[branch_prefix] = rev;
return branch_prefix;
}
//! Add a new fake revision to an RCS file.
//!
//! @param branch
//! Branch to adjust. @expr{0@} is the default branch.
//! If the branch doesn't exist, it will be created.
//!
//! @param prev_rev
//! The revision immediately preceeding the created revision
//! if known. Otherwise a suitable revision will be selected.
//!
//! @param rcs_time
//! The RCS timestamp of the created revision.
//!
//! @param committer
//! The committer of the created revision.
//!
//! @param message
//! The commit message for the created revision.
//!
//! @param state
//! The state of the created revision.
//! Depending on the state of @[prev_rev] and @[branch], this defaults to:
//! @string
//! @value "dead"
//! If the state of @[prev_rev] is @expr{"dead"@}.
//! @value "fake"
//! If @[branch] is @expr{0@} (ie the new revision has been
//! inserted somewhere potentially inbetween two old revisions).
//! @value "Exp"
//! (Or rather same as the state of @[prev_rev]), if a
//! new branch has been created.
//! @endstring
//!
//! This function is typically used to create artificial commits
//! when there's no suitable commit to hook an out-of-band event
//! to.
//!
//! @returns
//! Returns the new revision.
protected string append_revision(RCSFile rcs_file, string|void branch,
string|void prev_rev, string rcs_time,
string committer, string message,
string|void state)
{
if (rcs_time[2] == '.') rcs_time = "19" + rcs_time;
Calendar.TimeRange time = Calendar.ISO.parse("%y.%M.%D.%h.%m.%s %z",
rcs_time + " UTC");
string main_rev = prev_rev;
if (!prev_rev) {
// Get a suitable starting revision.
prev_rev = main_rev = find_revision(rcs_file, branch, rcs_time);
if (!rcs_file->tags[branch]) {
// Check the vendor branch as well.
prev_rev = find_revision(rcs_file, "", rcs_time);
}
if (!prev_rev ||
(rcs_file->revisions[main_rev]->time >
rcs_file->revisions[prev_rev]->time)) {
// The main branch is more recent than the vendor branch.
prev_rev = main_rev;
} else if (branch) main_rev = prev_rev;
}
// We now have a suitable prev_rev and main_rev.
#if 0
werror("append_revision(%O, %O, %O, %O, %O, %O, %O)\n",
rcs_file, branch, prev_rev, rcs_time, committer, message, state);
#endif
if (!prev_rev) return UNDEFINED;
int new_branch;
// Now it's time to generate a suitable result_rev.
string result_rev;
if (branch) {
string branch_prefix;
if (rcs_file->tags[branch]) {
// Note that there are such things as revision "3.0"
// in some RCS files...
array(string) fields = rcs_file->tags[branch]/".";
branch_prefix = (fields[..<2] + fields[<0..]) * ".";
if (has_prefix(prev_rev, branch_prefix + ".")) {
int i;
for (i = 'a'; rcs_file->revisions[sprintf("%s%c", prev_rev, i)]; i++)
;
result_rev = sprintf("%s%c", prev_rev, i);
}
} else {
branch_prefix = add_branch(rcs_file, branch, prev_rev);
}
if (!result_rev) {
// We add a revision to our new branch...
result_rev = branch_prefix + ".1";
new_branch = 1;
}
if ((< UNDEFINED, prev_rev >)[rcs_file->branch_heads[branch_prefix]]) {
rcs_file->branch_heads[branch_prefix] = result_rev;
}
} else {
int i;
for (i = 'a'; rcs_file->revisions[sprintf("%s%c", main_rev, i)]; i++)
;
result_rev = sprintf("%s%c", main_rev, i);
}
if (!state && (rcs_file->revisions[main_rev]->state != "dead")) {
state = "fake";
}
// FIXME!
RCSFile.Revision rev = rcs_file->append_revision(prev_rev, main_rev, time,
committer, message,
result_rev, state);
if (branch) {
if (new_branch) {
RCSFile.Revision brev = rcs_file->revisions[prev_rev];
if (brev->branches) {
brev->branches += ({ rev->revision });
} else {
brev->branches = ({ rev->revision });
}
}
} else if (rcs_file->head == main_rev) {
// We have a new HEAD revision.
rcs_file->head = rev->revision;
if (rcs_file->branch &&
(time > rcs_file->revisions[rcs_file->branch_heads[rcs_file->branch]]->time)) {
// The new revision is newer than the latest vendor branch commit.
rcs_file->branch = UNDEFINED;
}
}
//werror("Revision: %O\n", rev->revision);
return rev->revision;
}
//! Split of a branch @[branch] at time @[branch_time], duplicating
//! all revisions on the path to (and including) @[stop_time].
//!
//! @param move_tag
//! Filter function that is called with the name of any tags
//! that may be considered for moving to the new branch. The
//! argument is the name of a tag, and the function should
//! return @expr{1@} if the tag should be moved.
//!
//! @returns
//! Returns the HEAD revision of the new branch (if any).
//!
//! @note
//! This function does not handle vendor branches properly.
string split_branch(RCSFile rcsfile, string branch, string branch_time,
string stop_time, function(string: int(0..1)) move_tag)
{
string stop_rev = find_revision(rcsfile, UNDEFINED, stop_time);
if (!stop_rev) {
// The file didn't exist yet when the branch stopped being added to.
return UNDEFINED;
}
string start_rev = find_revision(rcsfile, UNDEFINED, branch_time);
ADT.Stack stack = ADT.Stack();
stack->push(0); // Sentinel.
string r = stop_rev;
while (r != start_rev) {
stack->push(r);
r = rcsfile->revisions[r]->ancestor;
}
if (!start_rev) {
// The split was before the file existed, so we need to add
// an artificial (and dead) commit before the first commit.
r = stack->top();
Calendar.TimeRange t =
Calendar.ISO.parse("%y.%M.%D.%h.%m.%s %z", branch_time + " UTC");
start_rev =
rcsfile->append_revision(r, UNDEFINED, t, "pcvs2git",
sprintf("Branch point for %s.\n", branch),
UNDEFINED, "dead")->revision;
} else if (!rcsfile->revisions[start_rev]->ancestor &&
(rcsfile->revisions[start_rev + ".1.1"])) {
// Vendor branch.
start_rev += ".1.1";
}
string branch_prefix = add_branch(rcsfile, branch, start_rev);
int i;
string prev_rev = start_rev;
while (r = stack->pop()) {
RCSFile.Revision rev = rcsfile->revisions[r];
prev_rev =
rcsfile->append_revision(r, prev_rev, rev->time, rev->author,
rev->log, branch_prefix + "." + ++i,
rev->state)->revision;
// Check if we need to move any tags.
foreach(rcsfile->tags; string tag; string tr) {
if ((tr != r) || !move_tag(tag)) continue;
rcsfile->tags[tag] = prev_rev;
}
}
rcsfile->branch_heads[branch_prefix] = prev_rev;
return prev_rev;
}
//! Make sure the revisions from this file aren't
//! merged into the branch @[branch].
void kill_branch(RCSFile rcsfile, string branch)
{
rcsfile->tags[branch] = "0.0.0.0";
}
//! Make sure the revisions from this file aren't
//! merged into the tag @[branch].
void kill_tag(RCSFile rcsfile, string tag)
{
rcsfile->tags[tag] = "0.0";
}
//! Replace all CRLF's in all revisions with plain LF's.
void fix_crlf(RCSFile rcsfile)
{
foreach(rcsfile->revisions;; RCSFile.Revision rev) {
rev->rcs_text = replace(rev->rcs_text, "\r\n", "\n");
if (rev->text) rev->text = replace(rev->text, "\r\n", "\n");
rev->sha = UNDEFINED;
rev->revision_flags = EXPAND_GUESS;
}
}
//! Add the branch tag @[branch] at the trunk at time @[branch_time] if
//! it doesn't already exist.
//!
//! @param branch
//! Branch to fixup.
//!
//! @param branch_time
//! Time at which the branch was created.
//!
//! This function is typically used to get a more stable
//! branch point for the common case where a branch only
//! exists in a few files (often just a single file) in
//! the repository.
//!
//! @note
//! This function handles vendor branches.
//!
//! @seealso
//! @[add_branch()], @[simple_add_tag()]
void simple_add_branch(RCSFile rcsfile, string branch, string branch_time)
{
if (rcsfile->tags[branch]) return;
string rev = find_revision(rcsfile, UNDEFINED, branch_time);
string rev2 = find_revision(rcsfile, "", branch_time);
if (!rev) {
rev = rev2;
} else if (rev2) {
if (rcsfile->revisions[rev2]->time >= rcsfile->revisions[rev]->time) {
rev = rev2;
}
}
if (rev) {
add_branch(rcsfile, branch, rev);
} else {
kill_branch(rcsfile, branch);
}
}
//! Add the tag @[branch] at the trunk at time @[tag_time] if
//! it doesn't already exist.
//!