mirror of
https://github.com/vitalif/viewvc-4intranet
synced 2019-04-16 04:14:59 +03:00
Compare commits
714 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
382ac29ed0 | ||
|
|
e1959ac2e5 | ||
|
|
cb38ccc929 | ||
|
|
f2b82132c2 | ||
|
|
5caf3a4437 | ||
|
|
06ee4df42d | ||
|
|
0a6b13145e | ||
|
|
65d3568c92 | ||
|
|
de5b147a6f | ||
|
|
eb6b575701 | ||
|
|
8d82b6f0d6 | ||
|
|
5a51470cbc | ||
|
|
acc0783468 | ||
|
|
c91325d40a | ||
|
|
ca1bd67b5d | ||
|
|
f1e7ef42d6 | ||
|
|
8b7eae7f14 | ||
|
|
6f3d9a3a00 | ||
|
|
1d7307c09b | ||
|
|
22d1e72c66 | ||
|
|
0e7d4061ed | ||
|
|
56dd2dcf28 | ||
|
|
5932f24a68 | ||
|
|
161421a20f | ||
|
|
2a297c5361 | ||
|
|
f43117b10f | ||
|
|
a025237f7e | ||
|
|
228db2fadb | ||
|
|
0cd26cc79f | ||
|
|
b808d5d1e8 | ||
|
|
f843c054b7 | ||
|
|
cf06a971e8 | ||
|
|
bc8f3bdd4f | ||
|
|
a6fcab67b0 | ||
|
|
4069208316 | ||
|
|
cfaa30b40f | ||
|
|
97c5a82b7b | ||
|
|
21bd391d62 | ||
|
|
9d1476ef1d | ||
|
|
151fcd05e6 | ||
|
|
22400ddcfb | ||
|
|
ae55530edc | ||
|
|
112700a12c | ||
|
|
cecffecf39 | ||
|
|
bdac8697fe | ||
|
|
3d9404b67d | ||
|
|
7c50f55153 | ||
|
|
368e4dc360 | ||
|
|
964d8bb5f9 | ||
|
|
846e8e46c5 | ||
|
|
75c719bcde | ||
|
|
fc8793bf15 | ||
|
|
65935c40fd | ||
|
|
a30d0b44cc | ||
|
|
fded8462d2 | ||
|
|
b1095ac763 | ||
|
|
e54399a169 | ||
|
|
4cc0db75be | ||
|
|
74a9cbb2a0 | ||
|
|
390e337a8e | ||
|
|
ecdac77d5f | ||
|
|
79158c2ee7 | ||
|
|
96fdfbdbcf | ||
|
|
361538da21 | ||
|
|
6250d4134b | ||
|
|
8589949521 | ||
|
|
f12e262fa5 | ||
|
|
f379070697 | ||
|
|
d47fc0ff3b | ||
|
|
f2b6f0ba86 | ||
|
|
ddbe150be1 | ||
|
|
6b5ed7c857 | ||
|
|
391f7d8237 | ||
|
|
141cf5ff10 | ||
|
|
bfe148eb45 | ||
|
|
6fb28f2198 | ||
|
|
02cc53d34b | ||
|
|
5928918da4 | ||
|
|
2e9f84427c | ||
|
|
df599031a0 | ||
|
|
6311c93298 | ||
|
|
b982cccbcd | ||
|
|
33b8224714 | ||
|
|
2f05f570b1 | ||
|
|
d737657e1d | ||
|
|
3d5294635e | ||
|
|
75c3fc2346 | ||
|
|
80bce159af | ||
|
|
7ff9b84ee1 | ||
|
|
b6acd3c114 | ||
|
|
fa5d40caa3 | ||
|
|
a9cbd4c6a6 | ||
|
|
882cdaa46b | ||
|
|
58e64cb28d | ||
|
|
779e67653b | ||
|
|
0381a772b7 | ||
|
|
048b2c8033 | ||
|
|
e354ab302c | ||
|
|
5f931c6cf1 | ||
|
|
576837351a | ||
|
|
6084ccf877 | ||
|
|
514da91629 | ||
|
|
d2c8bb9878 | ||
|
|
fe274e9b9c | ||
|
|
6603a67a91 | ||
|
|
b50de1c92e | ||
|
|
dfd8dc82f6 | ||
|
|
1a942d1062 | ||
|
|
289a78eb99 | ||
|
|
2897763020 | ||
|
|
47886423e5 | ||
|
|
0890821839 | ||
|
|
2903389d3e | ||
|
|
5bdea369e8 | ||
|
|
3699e8e0c7 | ||
|
|
55c0b30bc0 | ||
|
|
6fd321529a | ||
|
|
531877db4a | ||
|
|
51df003d8e | ||
|
|
7d05859d7e | ||
|
|
2e48cd2ba1 | ||
|
|
267b61f347 | ||
|
|
7f53d5cc3c | ||
|
|
af591ed9f7 | ||
|
|
17492a3856 | ||
|
|
e990ff9f2f | ||
|
|
d832930c8d | ||
|
|
8a0c66c7cf | ||
|
|
292c2d7ce0 | ||
|
|
16937af30f | ||
|
|
d067d49afa | ||
|
|
132a01a88f | ||
|
|
4db5103ff1 | ||
|
|
41943f67c5 | ||
|
|
1f37623526 | ||
|
|
7e92babad8 | ||
|
|
97e45101d9 | ||
|
|
62d5a3b649 | ||
|
|
c96c585b39 | ||
|
|
136063e5a7 | ||
|
|
f59db1597d | ||
|
|
c6c3f55c2c | ||
|
|
8063d46a53 | ||
|
|
fbc7737465 | ||
|
|
23f3f3ad55 | ||
|
|
0efc53a373 | ||
|
|
0cfe027753 | ||
|
|
df3b75cdb0 | ||
|
|
24dd18bf0f | ||
|
|
bbf27f4afd | ||
|
|
d4ac97de3c | ||
|
|
3039b1f43b | ||
|
|
13597f89cd | ||
|
|
870ae9aecc | ||
|
|
c94b0709ec | ||
|
|
5632e63bcb | ||
|
|
ae7b1103f6 | ||
|
|
2882178be7 | ||
|
|
bfc59256ca | ||
|
|
3abe695c3c | ||
|
|
624dfdf0f8 | ||
|
|
28bf22f279 | ||
|
|
830a48f88c | ||
|
|
a23543d763 | ||
|
|
586a183d5c | ||
|
|
7ae02a5887 | ||
|
|
4020fd2e5f | ||
|
|
af871d59ce | ||
|
|
b8d36c8c14 | ||
|
|
4b5721a3d7 | ||
|
|
10235b8fdf | ||
|
|
45332f577a | ||
|
|
148fd7b462 | ||
|
|
6f66313b51 | ||
|
|
cf32af4816 | ||
|
|
df3addacdb | ||
|
|
a1428600b7 | ||
|
|
4a4a3b1d61 | ||
|
|
8c65f1b2c5 | ||
|
|
55046f2261 | ||
|
|
b6cedd7c1a | ||
|
|
771d6736fb | ||
|
|
455157e413 | ||
|
|
c875582cfe | ||
|
|
c3d896d5a8 | ||
|
|
236069b068 | ||
|
|
6de96c4fc6 | ||
|
|
e45c2fcf6e | ||
|
|
8c620c8c1a | ||
|
|
c6d6dc4bdf | ||
|
|
355fac5da1 | ||
|
|
77d0c3dd06 | ||
|
|
d95cb540f5 | ||
|
|
98b757de23 | ||
|
|
efb811d20c | ||
|
|
a57e6b7054 | ||
|
|
fe52bbb079 | ||
|
|
76d6b541c3 | ||
|
|
269a9ca864 | ||
|
|
a5aafe3172 | ||
|
|
d30bd89c42 | ||
|
|
d76ce85625 | ||
|
|
b2b247f417 | ||
|
|
3ab2ec665b | ||
|
|
2806f0e9a2 | ||
|
|
8b426f6fb3 | ||
|
|
19ee8b32df | ||
|
|
a5389019fa | ||
|
|
7f272fbe04 | ||
|
|
89edf0ac04 | ||
|
|
71fbec8871 | ||
|
|
504b1e1a8a | ||
|
|
cf9381e0ee | ||
|
|
3907172131 | ||
|
|
c34752ce92 | ||
|
|
40cc23f12a | ||
|
|
9b7fea99d4 | ||
|
|
85949a6464 | ||
|
|
d5291a6b98 | ||
|
|
9e41d171c9 | ||
|
|
da54e91dea | ||
|
|
41b2ae2c7a | ||
|
|
1815220abe | ||
|
|
a080d8eef4 | ||
|
|
8c4af42d56 | ||
|
|
7f1d0952fc | ||
|
|
222aa4ec1f | ||
|
|
c03bfd4b89 | ||
|
|
6fa8cdf117 | ||
|
|
c4c777ffe6 | ||
|
|
3680903ed4 | ||
|
|
6fe61a11bb | ||
|
|
437975652f | ||
|
|
7ff75bec7b | ||
|
|
ea03f20f46 | ||
|
|
a5df515d79 | ||
|
|
fabe8e8a66 | ||
|
|
f01fcc4ec4 | ||
|
|
05e99f4c7b | ||
|
|
d8aff4e58f | ||
|
|
e8d3d6ad89 | ||
|
|
1317088e3c | ||
|
|
fe81ee5969 | ||
|
|
c481fe3ace | ||
|
|
563b5a9f6d | ||
|
|
360bdfd5a9 | ||
|
|
d4749c20f2 | ||
|
|
bf7676d7f1 | ||
|
|
8d5d67888d | ||
|
|
08454df121 | ||
|
|
a5b8f9176f | ||
|
|
badc9e3ff0 | ||
|
|
9d6c204efb | ||
|
|
429a9e5aed | ||
|
|
574de8ddac | ||
|
|
cf5491852c | ||
|
|
d525dd0737 | ||
|
|
502b9891d4 | ||
|
|
e72570dc20 | ||
|
|
1fa6bf4978 | ||
|
|
f00bf732df | ||
|
|
d13fd6f02a | ||
|
|
ba0e4598d3 | ||
|
|
899de5536f | ||
|
|
5b8d3cf325 | ||
|
|
0855e910ac | ||
|
|
fbb4e04dbc | ||
|
|
329ec7e9bf | ||
|
|
e9d1da3b8f | ||
|
|
3f8ecf1b67 | ||
|
|
b35d6b27d0 | ||
|
|
3738505ad7 | ||
|
|
a74cefa63c | ||
|
|
b90ed5fffa | ||
|
|
c264c62e3d | ||
|
|
286b8a2925 | ||
|
|
3a499652df | ||
|
|
d272630eac | ||
|
|
cb79c1f793 | ||
|
|
43337f8b0f | ||
|
|
7f149c01d0 | ||
|
|
2df94a5d37 | ||
|
|
91c5418de7 | ||
|
|
1adb46f2b3 | ||
|
|
410a80df6b | ||
|
|
eb6c6964c4 | ||
|
|
e157f923ed | ||
|
|
a8a6d5c10b | ||
|
|
473951fa96 | ||
|
|
0aaeeb4398 | ||
|
|
9761bb7818 | ||
|
|
46f42bf09a | ||
|
|
24984a78d1 | ||
|
|
ec3deccc34 | ||
|
|
7e85c56317 | ||
|
|
371a97fbb2 | ||
|
|
3effe02607 | ||
|
|
7e5d3b27b1 | ||
|
|
079b596424 | ||
|
|
fb0264a563 | ||
|
|
c58fc4008b | ||
|
|
16ccaa931c | ||
|
|
656a46ad8c | ||
|
|
8404a211c0 | ||
|
|
71d3617c0d | ||
|
|
67d4261729 | ||
|
|
066b22585a | ||
|
|
0ad2b1f095 | ||
|
|
f89786a6fa | ||
|
|
417e2bbedf | ||
|
|
d6bae15b7d | ||
|
|
cad4e52746 | ||
|
|
9acf966bd7 | ||
|
|
6c635e75a4 | ||
|
|
5b51bf9008 | ||
|
|
b5a2dd486d | ||
|
|
041c83fabe | ||
|
|
850422c786 | ||
|
|
1273f1da81 | ||
|
|
c1b351f78b | ||
|
|
c4ad7fc501 | ||
|
|
31a1a6f49c | ||
|
|
b0fb8b3ad7 | ||
|
|
0d0aafe7b2 | ||
|
|
0fb5f8eb85 | ||
|
|
19643d366c | ||
|
|
4492bbaa1b | ||
|
|
e41d5a9064 | ||
|
|
2a4b0eeb99 | ||
|
|
d768f2c850 | ||
|
|
88d4d62c39 | ||
|
|
64c38e8248 | ||
|
|
3f8390b18a | ||
|
|
62883ba20b | ||
|
|
e400232039 | ||
|
|
12c81ffae6 | ||
|
|
c252e41fc4 | ||
|
|
940bac640b | ||
|
|
20c13ec06a | ||
|
|
8274303424 | ||
|
|
e689f77881 | ||
|
|
43f5b14afa | ||
|
|
263920a326 | ||
|
|
97ed841022 | ||
|
|
802cf3cc5a | ||
|
|
2253c4361c | ||
|
|
debb3c26a3 | ||
|
|
2e6f3713c9 | ||
|
|
e7a8f0336f | ||
|
|
f81abbfd39 | ||
|
|
555614bf5d | ||
|
|
14e815a39b | ||
|
|
e33aea9c56 | ||
|
|
3a3f309545 | ||
|
|
48eb269c38 | ||
|
|
f30ceb4336 | ||
|
|
efd0ac1c26 | ||
|
|
f5cb5c1517 | ||
|
|
3d2cf8380d | ||
|
|
e11d706f30 | ||
|
|
d2d405513e | ||
|
|
45cf2fa972 | ||
|
|
89b012eb9f | ||
|
|
2c6ccc0191 | ||
|
|
acff76174e | ||
|
|
5fb74427f2 | ||
|
|
4f45a6b364 | ||
|
|
f38dfba74d | ||
|
|
f97eeac785 | ||
|
|
f8ae6503fd | ||
|
|
7cdabe099a | ||
|
|
e8123f68c2 | ||
|
|
c1d3602bf3 | ||
|
|
18954aa9a9 | ||
|
|
c227c0a6e3 | ||
|
|
4783fa5642 | ||
|
|
e2b02ff5f2 | ||
|
|
3ab9ffbbc0 | ||
|
|
81b39b70a1 | ||
|
|
35a1dd9938 | ||
|
|
b4bf269cf2 | ||
|
|
62f82645fd | ||
|
|
8acb06b609 | ||
|
|
96be9c14cd | ||
|
|
eda0309c55 | ||
|
|
b03113f864 | ||
|
|
5f8d6a1ff4 | ||
|
|
8bc4d0109c | ||
|
|
261db1bb7f | ||
|
|
e1f287360a | ||
|
|
4395bc3b1c | ||
|
|
c58e82a6e2 | ||
|
|
8916fe3969 | ||
|
|
301281228b | ||
|
|
7ed848b084 | ||
|
|
cf1f2ff5dd | ||
|
|
af559e2665 | ||
|
|
8dadbb3674 | ||
|
|
343ca81dbd | ||
|
|
0e129c4c77 | ||
|
|
f535d8e599 | ||
|
|
08e86c9a45 | ||
|
|
5a238e6a56 | ||
|
|
6ac32c2f1b | ||
|
|
7cf7328238 | ||
|
|
10fa46063c | ||
|
|
02f7f5b203 | ||
|
|
431e05efd2 | ||
|
|
13a6a53f9d | ||
|
|
3cbf07551b | ||
|
|
d620498e9e | ||
|
|
2a9584a0fb | ||
|
|
e6d8e1306d | ||
|
|
34f764bc8f | ||
|
|
34e7003241 | ||
|
|
cedb97c3d4 | ||
|
|
47ad6e149c | ||
|
|
c7a392b75c | ||
|
|
e0dff2601a | ||
|
|
fcf52b5c5f | ||
|
|
8f0623abc0 | ||
|
|
c3572f452c | ||
|
|
a9243564bb | ||
|
|
b049f1dca8 | ||
|
|
057ab0fe25 | ||
|
|
220792f72c | ||
|
|
c696f32d70 | ||
|
|
3a8717b11d | ||
|
|
edaae720da | ||
|
|
eb363507cb | ||
|
|
44f0356312 | ||
|
|
d966338c1d | ||
|
|
8f49d24d7f | ||
|
|
aa6b9dcf8e | ||
|
|
37373d8ab2 | ||
|
|
5ab76ea1cf | ||
|
|
0e6c162246 | ||
|
|
1f2a3399ea | ||
|
|
53b48c9b4f | ||
|
|
e906090e8c | ||
|
|
af7956447b | ||
|
|
939c980e5b | ||
|
|
7a8d3c0453 | ||
|
|
7e0b5d2a9c | ||
|
|
d7c87b8b2f | ||
|
|
8087c0e643 | ||
|
|
4c04da27da | ||
|
|
34f76bdc87 | ||
|
|
57bf0cb554 | ||
|
|
9adcdfae2f | ||
|
|
075b580fdb | ||
|
|
ae57f73e2f | ||
|
|
a70372d2ed | ||
|
|
9ce172e673 | ||
|
|
bac09990f6 | ||
|
|
e2b783a403 | ||
|
|
db6fbcdf1f | ||
|
|
fcf688c20c | ||
|
|
6c7de54d87 | ||
|
|
94017f01e8 | ||
|
|
2856ac9ebc | ||
|
|
525523e465 | ||
|
|
7d4ce47182 | ||
|
|
efad0b6cd6 | ||
|
|
3d6f1cc7d1 | ||
|
|
3d7b8a7a1a | ||
|
|
ed5898c37d | ||
|
|
c487361c95 | ||
|
|
b037efb91e | ||
|
|
29057c811f | ||
|
|
8ca49db267 | ||
|
|
d4e115bd3a | ||
|
|
d09280ce6b | ||
|
|
98aeeab676 | ||
|
|
061b30a108 | ||
|
|
01baf48a1b | ||
|
|
46e7cdf817 | ||
|
|
703327df1c | ||
|
|
da80a5898a | ||
|
|
2601c02f5d | ||
|
|
ad877bd5a5 | ||
|
|
2fe022c3a4 | ||
|
|
f40a836c23 | ||
|
|
c791123a08 | ||
|
|
be4ee9d817 | ||
|
|
889b3cdb0e | ||
|
|
e1f3b9f4f7 | ||
|
|
5c26dab649 | ||
|
|
c089f3bd73 | ||
|
|
01b87f562e | ||
|
|
d421c3bc22 | ||
|
|
53e1cd8f30 | ||
|
|
6c80a17d45 | ||
|
|
b5b1272ebb | ||
|
|
8b0b3632ac | ||
|
|
3abdb58fc1 | ||
|
|
65de16dd8f | ||
|
|
feca32c985 | ||
|
|
c3bd8d5aea | ||
|
|
fc2e5b9bd1 | ||
|
|
9e64e86927 | ||
|
|
a98023499d | ||
|
|
2ce86a5227 | ||
|
|
5a5e3755f6 | ||
|
|
10a2463c9f | ||
|
|
abb8486da4 | ||
|
|
f5defb51ae | ||
|
|
0f57c5b5e3 | ||
|
|
0d6e57a823 | ||
|
|
ed4c21c012 | ||
|
|
e5d47a1314 | ||
|
|
5321434ca4 | ||
|
|
dc19cf90ba | ||
|
|
fac695eec4 | ||
|
|
93696bc9c2 | ||
|
|
141419f852 | ||
|
|
e1f28b1bc0 | ||
|
|
ee45fd32af | ||
|
|
6982bbc7b0 | ||
|
|
204c6eb21c | ||
|
|
0c773e789b | ||
|
|
63aba8c173 | ||
|
|
a0a1ea7a42 | ||
|
|
62dff59068 | ||
|
|
a2d187d4be | ||
|
|
0961f1bb6c | ||
|
|
5a4e2c343b | ||
|
|
25b411f04f | ||
|
|
f5bfd66019 | ||
|
|
0dfb93969d | ||
|
|
8336ed6dce | ||
|
|
bfa13e4161 | ||
|
|
00b8f8a74e | ||
|
|
ffe04321f3 | ||
|
|
90109ce020 | ||
|
|
e2c6ed5202 | ||
|
|
1324797377 | ||
|
|
0d0f8c789d | ||
|
|
e144eb6942 | ||
|
|
1d5c053757 | ||
|
|
e2f383baf8 | ||
|
|
f7095d713d | ||
|
|
3b5ae57380 | ||
|
|
3b15cf11cc | ||
|
|
a5f6b657cf | ||
|
|
3edd4b3b01 | ||
|
|
d25e5e3098 | ||
|
|
9672b6d711 | ||
|
|
4ee8ea34e1 | ||
|
|
a0e92a8679 | ||
|
|
bb246bf7c7 | ||
|
|
2f9a4df28d | ||
|
|
f7bf5a511e | ||
|
|
d234219a4f | ||
|
|
580463a8ed | ||
|
|
5ce1ab531d | ||
|
|
2860672524 | ||
|
|
2efd7cc4af | ||
|
|
676ee09745 | ||
|
|
ac39b5ec6c | ||
|
|
68a7d107e2 | ||
|
|
ea04c73b36 | ||
|
|
5443c117e6 | ||
|
|
ef89f0db73 | ||
|
|
3c0ab3e163 | ||
|
|
300a8236cb | ||
|
|
891dfa4b85 | ||
|
|
e4527a3732 | ||
|
|
169aba8c30 | ||
|
|
3c426916d4 | ||
|
|
8254d2d876 | ||
|
|
85f7720267 | ||
|
|
436e10e74c | ||
|
|
473b853a5b | ||
|
|
77446e313a | ||
|
|
310db99e62 | ||
|
|
b597b6a485 | ||
|
|
e7014db4cf | ||
|
|
8f5e381f21 | ||
|
|
b7af5fc568 | ||
|
|
7e0d117de8 | ||
|
|
95f5f3bc9c | ||
|
|
b9044a9a9d | ||
|
|
9e07c413fa | ||
|
|
c8645305c5 | ||
|
|
4750fd345c | ||
|
|
4f1259d64d | ||
|
|
ead2d603a2 | ||
|
|
79fe3a2919 | ||
|
|
c3aabd8b7c | ||
|
|
5977b3c4fe | ||
|
|
59ecb71c51 | ||
|
|
ed664972cb | ||
|
|
9b625017bb | ||
|
|
e0221b21bf | ||
|
|
04cb1cd74d | ||
|
|
cf862e4e68 | ||
|
|
497cc24bbe | ||
|
|
39eab459b4 | ||
|
|
942d84818e | ||
|
|
6a03c7a4dd | ||
|
|
dbf67f4c0b | ||
|
|
a1c093ca2c | ||
|
|
c6db566501 | ||
|
|
1d2b2d9c35 | ||
|
|
862d5b7132 | ||
|
|
bc295fb361 | ||
|
|
87fb74e580 | ||
|
|
01e10dd1bb | ||
|
|
44d86248d1 | ||
|
|
cb78995c4a | ||
|
|
1592197d08 | ||
|
|
bb39ba6b22 | ||
|
|
40c37a55f8 | ||
|
|
9d16bcb2a7 | ||
|
|
5fcdcd8b8a | ||
|
|
fa035c8ae7 | ||
|
|
d83ff39472 | ||
|
|
118c6cc35e | ||
|
|
5f22029349 | ||
|
|
1920391b2a | ||
|
|
a962fae87a | ||
|
|
ff2ac1d824 | ||
|
|
e4967b894a | ||
|
|
3d65a28a3c | ||
|
|
ac60888eaf | ||
|
|
9caee0c04d | ||
|
|
818c168978 | ||
|
|
aaca4217a7 | ||
|
|
63d07e6065 | ||
|
|
21f68a5118 | ||
|
|
7e55acdc5d | ||
|
|
9115900b3a | ||
|
|
8845bc551f | ||
|
|
4cebbf43df | ||
|
|
1171d112b7 | ||
|
|
c41ffe04e2 | ||
|
|
36e0d19fe3 | ||
|
|
5bf80b54f4 | ||
|
|
a6cad82e55 | ||
|
|
7784009c8e | ||
|
|
692f539c36 | ||
|
|
bfdd4c0988 | ||
|
|
6b84e92bdf | ||
|
|
3e882435f2 | ||
|
|
aaf85a14a6 | ||
|
|
acd8ab25e9 | ||
|
|
41e1ae4171 | ||
|
|
eb70cfaaec | ||
|
|
7a51a39ba0 | ||
|
|
3dcc42d802 | ||
|
|
696feb50e0 | ||
|
|
edc66442b4 | ||
|
|
f64490de1c | ||
|
|
491e02f504 | ||
|
|
60991a4743 | ||
|
|
2bcda59f31 | ||
|
|
9a73f275ec | ||
|
|
aff7923450 | ||
|
|
06c202f98a | ||
|
|
7a038950d7 | ||
|
|
e34725c830 | ||
|
|
528d4aec3d | ||
|
|
789f13c9f7 | ||
|
|
090dbd7d3b | ||
|
|
a92daef556 | ||
|
|
83b1c6937a | ||
|
|
f817d8149c | ||
|
|
8bbe853ffe | ||
|
|
0bb2dc9f16 | ||
|
|
8612acd3c2 | ||
|
|
3d98a33ce7 | ||
|
|
362ff5cde6 | ||
|
|
d6489c2026 | ||
|
|
b8cbcda7ae | ||
|
|
2fa593f359 | ||
|
|
dcc79ade05 | ||
|
|
323f09f7af | ||
|
|
b12a220928 | ||
|
|
f448035ca4 | ||
|
|
d8d2075d65 | ||
|
|
08c145ddc0 | ||
|
|
bb4204af9a | ||
|
|
2fcd41ded0 | ||
|
|
df68135a6f | ||
|
|
87bbbba821 | ||
|
|
3b40b8e245 | ||
|
|
b9102d4649 | ||
|
|
dba9027f85 | ||
|
|
70530d1018 | ||
|
|
464cf13729 | ||
|
|
f70e9fdc4e | ||
|
|
0e750439f8 | ||
|
|
eedbe8b649 | ||
|
|
a9de320c0e | ||
|
|
dabc194191 | ||
|
|
3fdfabc8ee | ||
|
|
87c95ac6f8 | ||
|
|
4b30fca3bf | ||
|
|
bebbc247c9 | ||
|
|
c72910f422 | ||
|
|
669ea3162b | ||
|
|
d9777eef27 | ||
|
|
eb5ccaf906 | ||
|
|
d61e0b8e2c | ||
|
|
e0d1a728a5 | ||
|
|
b749971cdf | ||
|
|
171670a621 | ||
|
|
4c96e86027 | ||
|
|
24558586a8 | ||
|
|
9bc2a1e1f2 | ||
|
|
9ec5d1f09e | ||
|
|
956482e96b | ||
|
|
3c55e5eaf8 |
201
CHANGES
201
CHANGES
@@ -1,3 +1,203 @@
|
||||
Version 1.1.15 (released 22-Jun-2012)
|
||||
|
||||
* security fix: complete authz support for remote SVN views (issue #353)
|
||||
* security fix: log msg leak in SVN revision view with unreadable copy source
|
||||
* fix several instances of incorrect information in remote SVN views
|
||||
* increase performance of some revision metadata lookups in remote SVN views
|
||||
* fix RSS feed regression introduced in 1.1.14
|
||||
|
||||
Version 1.1.14 (released 12-Jun-2012)
|
||||
|
||||
* fix annotation of svn files with non-URI-safe paths (issue #504)
|
||||
* handle file:/// Subversion rootpaths as local roots (issue #446)
|
||||
* fix bug caused by trying to case-normalize anon usernames (issue #505)
|
||||
* speed up log handling by reusing tokenization results (issue #506)
|
||||
* add support for custom review log markup rules (issue #429)
|
||||
|
||||
Version 1.1.13 (released 23-Jan-2012)
|
||||
|
||||
* fix svndbadmin failure on deleted paths under Subversion 1.7 (issue #499)
|
||||
* fix annotation of files in svn roots with non-URI-safe paths
|
||||
* fix stray annotation warning in markup display of images
|
||||
* more gracefully handle attempts to display binary content (issue #501)
|
||||
|
||||
Version 1.1.12 (released 03-Nov-2011)
|
||||
|
||||
* fix path display in patch and certain diff views (issue #485)
|
||||
* fix broken cvsdb glob searching (issue 486)
|
||||
* allow svn revision specifiers to have leading r's (issue #441, #448)
|
||||
* allow environmental override of configuration location (issue #494)
|
||||
* fix exception HTML-escaping non-string data under WSGI (issue #454)
|
||||
* add links to root logs from roots view (issue #470)
|
||||
* use Pygments lexer-guessing functionality (issue #495)
|
||||
|
||||
Version 1.1.11 (released 17-May-2011)
|
||||
|
||||
* security fix: remove user-reachable override of cvsdb row limit
|
||||
* fix broken standalone.py -c and -d options handling
|
||||
* add --help option to standalone.py
|
||||
* fix stack trace when asked to checkout a directory (issue #478)
|
||||
* improve memory usage and speed of revision log markup (issue #477)
|
||||
* fix broken annotation view in CVS keyword-bearing files (issue #479)
|
||||
* warn users when query results are incomplete (issue #433)
|
||||
* avoid parsing errors on RCS newphrases in the admin section (issue #483)
|
||||
* make rlog parsing code more robust in certain error cases (issue #444)
|
||||
|
||||
Version 1.1.10 (released 15-Mar-2011)
|
||||
|
||||
* fix stack trace in Subversion revision info logic (issue #475, issue #476)
|
||||
|
||||
Version 1.1.9 (released 18-Feb-2011)
|
||||
|
||||
* vcauth universal access determinations (issue #425)
|
||||
* rework svn revision info cache for performance
|
||||
* make revision log "extra pages" count configurable
|
||||
* fix Subversion 1.4.x revision log compatibility code regression
|
||||
* display sanitized error when authzfile is malformed
|
||||
* restore markup of URLs in file contents (issue #455)
|
||||
* optionally display last-committed metadata in roots view (issue #457)
|
||||
|
||||
Version 1.1.8 (released 02-Dec-2010)
|
||||
|
||||
* fix slowness triggered by allow_compress=1 configuration (issue #467)
|
||||
* allow use of 'fcrypt' for Windows standalone.py authn support (issue #471)
|
||||
* yield more useful error on directory markup/annotate request (issue #472)
|
||||
|
||||
Version 1.1.7 (released 09-Sep-2010)
|
||||
|
||||
* display Subversion revision properties in the revision view (issue #453)
|
||||
* fix exception in 'standalone.py -r REPOS' when run without a config file
|
||||
* fix standalone.py server root deployments (--script-alias='')
|
||||
* add Basic authentication support to standalone.py (Unix only) (issue #49)
|
||||
* fix obscure "unexpected NULL parent pool" Subversion bindings error
|
||||
* enable path info / link display in remote Subversion root revision view
|
||||
* fix vhost name case handling inconsistency (issue #466)
|
||||
* use svn:mime-type property charset param as encoding hint
|
||||
* markup Subversion revision references in log messages (issue #313)
|
||||
* add rudimentary support for FastCGI-based deployments (issue #464)
|
||||
* fix query script WSGI deployment
|
||||
* add configuration to fix query script cross-linking to ViewVC
|
||||
|
||||
Version 1.1.6 (released 02-Jun-2010)
|
||||
|
||||
* add rudimentary support for WSGI-based deployments (issue #397)
|
||||
* fix exception caused by trying to HTML-escape non-string data (issue #454)
|
||||
* fix incorrect RSS feed Content-Type header (issue #449)
|
||||
* fix RSS <title> encoding problem (issue #451)
|
||||
* allow 'svndbadmin purge' to work on missing repositories (issue #452)
|
||||
|
||||
Version 1.1.5 (released 29-Mar-2010)
|
||||
|
||||
* security fix: escape user-provided search_re input to avoid XSS attack
|
||||
|
||||
Version 1.1.4 (released 10-Mar-2010)
|
||||
|
||||
* security fix: escape user-provided query form input to avoid XSS attack
|
||||
* fix standalone.py failure (when per-root options aren't used) (issue #445)
|
||||
* fix annotate failure caused by ignored svn_config_dir (issue #447)
|
||||
|
||||
Version 1.1.3 (released 22-Dec-2009)
|
||||
|
||||
* security fix: add root listing support of per-root authz config
|
||||
* security fix: query.py requires 'forbidden' authorizer (or none) in config
|
||||
* fix URL-ification of truncated log messages (issue #3)
|
||||
* fix regexp input validation (issue #426, #427, #440)
|
||||
* add support for configurable tab-to-spaces conversion
|
||||
* fix not-a-sequence error in diff view
|
||||
* allow viewvc-install to work when templates-contrib is absent
|
||||
* minor template improvements/corrections
|
||||
* expose revision metadata in diff view (issue #431)
|
||||
* markup file/directory item property URLs and email addresses (issue #434)
|
||||
* make ViewVC cross copies in Subversion history by default
|
||||
* fix bug that caused standalone.py failure under Python 1.5.2 (issue #442)
|
||||
* fix support for per-vhost overrides of authorizer parameters (issue #411)
|
||||
* fix root name identification in query.py interface
|
||||
|
||||
Version 1.1.2 (released 11-Aug-2009)
|
||||
|
||||
* security fix: validate the 'view' parameter to avoid XSS attack
|
||||
* security fix: avoid printing illegal parameter names and values
|
||||
* add optional support for character encoding detection (issue #400)
|
||||
* fix username case handling in svnauthz module (issue #419)
|
||||
* fix cvsdbadmin/svnadmin rebuild error on missing repos (issue #420)
|
||||
* don't drop leading blank lines from colorized file contents (issue #422)
|
||||
* add file.ezt template logic for optionally hiding binary file contents
|
||||
|
||||
Version 1.1.1 (released 03-Jun-2009)
|
||||
|
||||
* fix broken query form (missing required template variables) (issue #416)
|
||||
* fix bug in cvsdb which caused rebuild operations to lose data (issue #417)
|
||||
* fix cvsdb purge/rebuild repos lookup to error on missing repos
|
||||
* fix misleading file contents view page title
|
||||
|
||||
Version 1.1.0 (released 13-May-2009)
|
||||
|
||||
* add support for full content diffs (issue #153)
|
||||
* make many more data dictionary items available to all views
|
||||
* various rcsparse and tparse module fixes
|
||||
* add daemon mode to standalone.py (issue #235)
|
||||
* rework helper application configuration options (issues #229, #62)
|
||||
* teach standalone.py to recognize Subversion repositories via -r option
|
||||
* now interpret relative paths in "viewvc.conf" as relative to that file
|
||||
* add 'purge' subcommand to cvsdbadmin and svndbadmin (issue #271)
|
||||
* fix orphaned data bug in cvsdbadmin/svndbadmin rebuild (issue #271)
|
||||
* add support for query by log message (issues #22, #121)
|
||||
* fix bug parsing 'svn blame' output with too-long author names (issue #221)
|
||||
* fix default standalone.py port to be within private IANA range (issue #234)
|
||||
* add unified configury of allowed views; checkout view disabled by default
|
||||
* add support for ranges of revisions to svndbadmin (issue #224)
|
||||
* make the query handling more forgiving of malformatted subdirs (issue #244)
|
||||
* add support for per-root configuration overrides (issue #371)
|
||||
* add support for optional email address mangling (issue #290)
|
||||
* extensible path-based authorization subsystem (issue #268), supporting:
|
||||
- Subversion authz files (new)
|
||||
- regexp-based path hiding (for compat with 1.0.x)
|
||||
- file glob top-level directory hiding (for compat with 1.0.x)
|
||||
* allow default file view to be "markup" (issue #305)
|
||||
* add support for displaying file/directory properties (issue #39)
|
||||
* pagination improvements
|
||||
* add gzip output encoding support for template-driven pages
|
||||
* fix cache control bugs (issue #259)
|
||||
* add RSS feed URL generation for file history
|
||||
* add support for remote creation of ViewVC checkins database
|
||||
* add integration with Pygments for syntax highlighting
|
||||
* preserve executability of Subversion files in tarballs (issue #233)
|
||||
* add ability to set Subversion runtime config dir (issue #351, issue #339)
|
||||
* show RSS/query links only for roots found in commits database (issue #357)
|
||||
* recognize Subversion svn:mime-type property values (issue #364)
|
||||
* hide CVS files when viewing tags/branches on which they don't exist
|
||||
* allow hiding of errorful entries from the directory view (issue #105)
|
||||
* fix directory view sorting UI
|
||||
* tolerate malformed Accept-Language headers (issue #396)
|
||||
* allow MIME type mapping overrides in ViewVC configuration (issue #401)
|
||||
* fix exception in rev-sorted remote Subversion directory views (issue #409)
|
||||
* allow setting of page sizes for log and dir views individually (issue #402)
|
||||
|
||||
Version 1.0.9 (released 11-Aug-2009)
|
||||
|
||||
* security fix: validate the 'view' parameter to avoid XSS attack
|
||||
* security fix: avoid printing illegal parameter names and values
|
||||
|
||||
Version 1.0.8 (released 05-May-2009)
|
||||
|
||||
* fix directory view sorting UI
|
||||
* tolerate malformed Accept-Language headers (issue #396)
|
||||
* fix directory log views in revision-less Subversion repositories
|
||||
* fix exception in rev-sorted remote Subversion directory views (issue #409)
|
||||
|
||||
Version 1.0.7 (released 14-Oct-2008)
|
||||
|
||||
* fix regression in the 'as text' download view (issue #373)
|
||||
|
||||
Version 1.0.6 (released 16-Sep-2008)
|
||||
|
||||
* security fix: ignore arbitrary user-provided MIME types (issue #354)
|
||||
* fix bug in regexp search filter when used with sticky tag (issue #346)
|
||||
* fix bug in handling of certain 'co' output (issue #348)
|
||||
* fix regexp search filter template bug
|
||||
* fix annotate code syntax error
|
||||
* fix mod_python import cycle (issue #369)
|
||||
|
||||
Version 1.0.5 (released 28-Feb-2008)
|
||||
|
||||
* security fix: omit commits of all-forbidden files from query results
|
||||
@@ -188,7 +388,6 @@ Version 0.9 (released 23-Dec-2001)
|
||||
* create dir_alternate.ezt for the flipped rev/name links
|
||||
* various UI tweaks for the directory pages
|
||||
|
||||
|
||||
Version 0.8 (released 10-Dec-2001)
|
||||
|
||||
* add EZT templating mechanism for generating output pages
|
||||
|
||||
@@ -20,6 +20,7 @@ directly.
|
||||
jamesh James Henstridge <???>
|
||||
maxb Max Bowsher <maxb1@ukf.net>
|
||||
eh Erik Hülsmann <e.huelsmann@gmx.net>
|
||||
mhagger Michael Haggerty <mhagger@alum.mit.edu>
|
||||
|
||||
## Local Variables:
|
||||
## coding:utf-8
|
||||
|
||||
257
INSTALL
257
INSTALL
@@ -19,7 +19,7 @@ Congratulations on getting this far. :-)
|
||||
|
||||
For CVS Support:
|
||||
|
||||
* Python 1.5.2 or later
|
||||
* Python 1.5.2 or later (sorry, no 3.x support yet)
|
||||
(http://www.python.org/)
|
||||
* RCS, Revision Control System
|
||||
(http://www.cs.purdue.edu/homes/trinkle/RCS/)
|
||||
@@ -30,11 +30,11 @@ Congratulations on getting this far. :-)
|
||||
|
||||
For Subversion Support:
|
||||
|
||||
* Python 2.0 or later
|
||||
* Python 2.0 or later (sorry, no 3.x support yet)
|
||||
(http://www.python.org/)
|
||||
* Subversion, Version Control System, 1.2.0 or later
|
||||
* Subversion, Version Control System, 1.3.1 or later
|
||||
(binary installation and Python bindings)
|
||||
(http://subversion.tigris.org/)
|
||||
(http://subversion.apache.org/)
|
||||
|
||||
Optional:
|
||||
|
||||
@@ -43,11 +43,8 @@ Congratulations on getting this far. :-)
|
||||
* MySQL 3.22 and MySQLdb 0.9.0 or later to create a commit database
|
||||
(http://www.mysql.com/)
|
||||
(http://sourceforge.net/projects/mysql-python)
|
||||
* Enscript, code colorizer
|
||||
(http://www.codento.com/people/mtr/genscript/)
|
||||
* Highlight, code colorizer, 2.2.10 or later required, 2.4.5 or
|
||||
later recommended for reliable line numbering
|
||||
(http://www.andre-simon.de/)
|
||||
* Pygments 0.9 or later, syntax highlighting engine
|
||||
(http://pygments.org)
|
||||
* CvsGraph 1.5.0 or later, graphical CVS revision tree generator
|
||||
(http://www.akhphd.au.dk/~bertho/cvsgraph/)
|
||||
|
||||
@@ -58,7 +55,7 @@ Congratulations on getting this far. :-)
|
||||
|
||||
$ bin/standalone.py -r /PATH/TO/REPOSITORY
|
||||
|
||||
This will start a tiny ViewVC server at http://localhost:7467/viewvc/,
|
||||
This will start a tiny ViewVC server at http://localhost:49152/viewvc/,
|
||||
to which you can connect with your browser.
|
||||
|
||||
Standard operation:
|
||||
@@ -94,15 +91,9 @@ Visitors viewing those versioned controlled documents get the
|
||||
malicious code, too, which might not be what the original author
|
||||
intended.
|
||||
|
||||
If you wish to disable ViewVC's "checkout" view which implements this
|
||||
feature, you can do so by editing lib/viewvc.py, and modifying the
|
||||
function view_checkout() like so, adding the lines indicated:
|
||||
|
||||
def view_checkout(request):
|
||||
>> raise debug.ViewVCException('Checkout view is disabled',
|
||||
>> '403 Forbidden')
|
||||
path, rev = _orig_path(request)
|
||||
fp, revision = request.repos.openfile(path, rev)
|
||||
For this reason, ViewVC's "checkout" view is disabled by default. If
|
||||
you wish to enable it, simply add "co" to the list of views enabled in
|
||||
the allowed_views configuration option.
|
||||
|
||||
|
||||
INSTALLING VIEWVC
|
||||
@@ -147,8 +138,8 @@ installation instructions.
|
||||
root_parents (for CVS or Subversion)
|
||||
default_root
|
||||
root_as_url_component
|
||||
rcs_path
|
||||
mime_types_file
|
||||
rcs_dir
|
||||
mime_types_files
|
||||
|
||||
There are some other options that are usually nice to change. See
|
||||
viewvc.conf for more information. ViewVC provides a working,
|
||||
@@ -177,72 +168,145 @@ checkin database working are below.
|
||||
APACHE CONFIGURATION
|
||||
--------------------
|
||||
|
||||
1) Find out where the web server configuration file is kept. Typical
|
||||
locations are /etc/httpd/httpd.conf, /etc/httpd/conf/httpd.conf,
|
||||
and /etc/apache/httpd.conf. Depending on how apache was installed,
|
||||
you may also look under /usr/local/etc or /etc/local. Use the vendor
|
||||
documentation or the find utility if in doubt.
|
||||
1) Locate your Apache configuration file(s).
|
||||
|
||||
Either METHOD A:
|
||||
2) The ScriptAlias directive is very useful for pointing
|
||||
Typical locations are /etc/httpd/httpd.conf,
|
||||
/etc/httpd/conf/httpd.conf, and /etc/apache/httpd.conf. Depending
|
||||
on how Apache was installed, you may also look under /usr/local/etc
|
||||
or /etc/local. Use the vendor documentation or the find utility if
|
||||
in doubt.
|
||||
|
||||
2) Depending on how your Apache configuration is setup by default, you
|
||||
might need to explicitly allow high-level access to the ViewVC
|
||||
install location.
|
||||
|
||||
<Directory <VIEWVC_INSTALLATION_DIRECTORY>>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
For example, if ViewVC is installed in /usr/local/viewvc-1.0 on
|
||||
your system:
|
||||
|
||||
<Directory /usr/local/viewvc-1.0>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
3) Configure Apache to expose ViewVC to users at the URL of your choice.
|
||||
|
||||
ViewVC provides several different ways to do this. Choose one of
|
||||
the following methods:
|
||||
|
||||
-----------------------------------
|
||||
METHOD A: CGI mode via ScriptAlias
|
||||
-----------------------------------
|
||||
The ScriptAlias directive is very useful for pointing
|
||||
directly to the viewvc.cgi script. Simply insert a line containing
|
||||
|
||||
ScriptAlias /viewvc <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/viewvc.cgi
|
||||
ScriptAlias /viewvc <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/viewvc.cgi
|
||||
|
||||
into your httpd.conf file. Choose the location in httpd.conf where
|
||||
also the other ScriptAlias lines reside. Some examples:
|
||||
|
||||
ScriptAlias /viewvc /usr/local/viewvc-1.0/bin/cgi/viewvc.cgi
|
||||
ScriptAlias /query /usr/local/viewvc-1.0/bin/cgi/query.cgi
|
||||
ScriptAlias /viewvc /usr/local/viewvc-1.0/bin/cgi/viewvc.cgi
|
||||
ScriptAlias /query /usr/local/viewvc-1.0/bin/cgi/query.cgi
|
||||
|
||||
continue with step 3).
|
||||
|
||||
or alternatively METHOD B:
|
||||
2) Copy the CGI scripts from
|
||||
----------------------------------------
|
||||
METHOD B: CGI mode in cgi-bin directory
|
||||
----------------------------------------
|
||||
Copy the CGI scripts from
|
||||
<VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/*.cgi
|
||||
to the /cgi-bin/ directory configured in your httpd.conf file.
|
||||
|
||||
continue with step 3).
|
||||
You can override configuration file location using:
|
||||
|
||||
SetEnv VIEWVC_CONF_PATHNAME /etc/viewvc.conf
|
||||
|
||||
and then there's METHOD C:
|
||||
2) Copy the CGI scripts from
|
||||
------------------------------------------
|
||||
METHOD C: CGI mode in ExecCGI'd directory
|
||||
------------------------------------------
|
||||
Copy the CGI scripts from
|
||||
<VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/*.cgi
|
||||
to the directory of your choosing in the Document Root adding the following
|
||||
apache directives for the directory in httpd.conf or an .htaccess file:
|
||||
Apache directives for the directory in httpd.conf or an .htaccess file:
|
||||
|
||||
Options +ExecCGI
|
||||
AddHandler cgi-script .cgi
|
||||
Options +ExecCGI
|
||||
AddHandler cgi-script .cgi
|
||||
|
||||
(Note: For this to work mod_cgi has to be loaded. And for the .htaccess file
|
||||
Note: For this to work mod_cgi has to be loaded. And for the .htaccess file
|
||||
to be effective, "AllowOverride All" or "AllowOverride Options FileInfo"
|
||||
need to have been specified for the directory.)
|
||||
needs to have been specified for the directory.
|
||||
|
||||
continue with step 3).
|
||||
|
||||
or if you've got Mod_Python installed you can use METHOD D:
|
||||
2) Copy the Python scripts and .htaccess file from
|
||||
------------------------------------------
|
||||
METHOD D: Using mod_python (if installed)
|
||||
------------------------------------------
|
||||
Copy the Python scripts and .htaccess file from
|
||||
<VIEWVC_INSTALLATION_DIRECTORY>/bin/mod_python/
|
||||
to a directory being served by apache.
|
||||
to a directory being served by Apache.
|
||||
|
||||
In httpd.conf, make sure that "AllowOverride All" or at least
|
||||
"AllowOverride FileInfo Options" are enabled for the directory
|
||||
you copied the files to.
|
||||
|
||||
You can override configuration file location using:
|
||||
|
||||
SetEnv VIEWVC_CONF_PATHNAME /etc/viewvc.conf
|
||||
|
||||
Note: If you are using Mod_Python under Apache 1.3 the tarball generation
|
||||
and enscript colorizing features may not work because they use
|
||||
multithreading. They do work fine with Apache 2.
|
||||
feature may not work because it uses multithreading. This works fine
|
||||
under Apache 2.
|
||||
|
||||
continue with step 3).
|
||||
----------------------------------------
|
||||
METHOD E: Using mod_wsgi (if installed)
|
||||
----------------------------------------
|
||||
Copy the Python scripts file from
|
||||
<VIEWVC_INSTALLATION_DIRECTORY>/bin/mod_python/
|
||||
to the directory of your choosing. Modify httpd.conf with the
|
||||
following directives:
|
||||
|
||||
3) Restart apache. The commands to do this vary. "httpd -k restart" and
|
||||
"apache -k restart" are two common variants. On RedHat Linux it is
|
||||
done using the command "/sbin/service httpd restart" and on SuSE Linux
|
||||
it is done with "rcapache restart"
|
||||
WSGIScriptAlias /viewvc <VIEWVC_INSTALLATION_DIRECTORY>/bin/wsgi/viewvc.wsgi
|
||||
WSGIScriptAlias /query <VIEWVC_INSTALLATION_DIRECTORY>/bin/wsgi/query.wsgi
|
||||
|
||||
4) Optional: Add access control.
|
||||
You'll probably also need the following directive because of the
|
||||
not-quite-sanctioned way that ViewVC manipulates Python objects.
|
||||
|
||||
In your httpd.conf you can control access to certain modules by adding
|
||||
directives like this:
|
||||
WSGIApplicationGroup %{GLOBAL}
|
||||
|
||||
Note: WSGI support in ViewVC is at this time quite rudimentary,
|
||||
bordering on downright experimental. Your mileage may vary.
|
||||
|
||||
-----------------------------------------
|
||||
METHOD F: Using mod_fcgid (if installed)
|
||||
-----------------------------------------
|
||||
|
||||
This uses ViewVC's WSGI support (from above), but supports using FastCGI,
|
||||
and is a somewhat hybrid approach of several of the above methods.
|
||||
|
||||
Especially if fcgi is already being used for other purposes, e.g. PHP,
|
||||
also using fcgi can prevent the need for including additional modules
|
||||
(e.g. mod_python or mod_wsgi) within Apache, which may help lessen Apache's
|
||||
memory usage and/or help improve performance.
|
||||
|
||||
This depends on mod_fcgid:
|
||||
|
||||
http://httpd.apache.org/mod_fcgid/
|
||||
|
||||
as well as the fcgi server from Python's flup package:
|
||||
|
||||
http://pypi.python.org/pypi/flup
|
||||
http://trac.saddi.com/flup
|
||||
|
||||
The following are some example httpd.conf fragments you can use to
|
||||
support this configuration:
|
||||
|
||||
ScriptAlias /viewvc /usr/local/viewvc/bin/wsgi/viewvc.fcgi
|
||||
ScriptAlias /query /usr/local/viewvc/bin/wsgi/query.fcgi
|
||||
|
||||
4) [Optional] Add access control.
|
||||
|
||||
In your httpd.conf you can control access to certain modules by
|
||||
adding directives like this:
|
||||
|
||||
<Location "<url to viewvc.cgi>/<modname_you_wish_to_access_ctl>">
|
||||
AllowOverride None
|
||||
@@ -260,7 +324,14 @@ or if you've got Mod_Python installed you can use METHOD D:
|
||||
http://<server_name>/viewvc/~checkout~/<module_name>
|
||||
http://<server_name>/viewvc/<module_name>.tar.gz?view=tar
|
||||
|
||||
5) Optional: Protect your ViewVC instance from server-whacking webcrawlers.
|
||||
5) Restart Apache.
|
||||
|
||||
The commands to do this vary. "httpd -k restart" and "apache -k
|
||||
restart" are two common variants. On RedHat Linux it is done using
|
||||
the command "/sbin/service httpd restart" and on SuSE Linux it is
|
||||
done with "rcapache restart". Other systems use "apachectl restart".
|
||||
|
||||
6) Optional: Protect your ViewVC instance from server-whacking webcrawlers.
|
||||
|
||||
As ViewVC is a web-based application which each page containing various
|
||||
links to other pages and views, you can expect your server's performance
|
||||
@@ -361,27 +432,37 @@ there are some additional steps required to get the database working.
|
||||
ALL <VIEWVC_INSTALLATION_DIRECTORY>/bin/loginfo-handler %{sVv} cvsnt
|
||||
|
||||
To publish Subversion commits into the database:
|
||||
|
||||
|
||||
To build a database of all the commits in the Subversion
|
||||
repository /home/svn, invoke: "./svndbadmin rebuild /home/svn".
|
||||
If you want to update the checkin database, invoke:
|
||||
"./svndbadmin update /home/svn".
|
||||
|
||||
|
||||
To get real time updates, you will need to add a post-commit
|
||||
hook (for the repository example above, the script should go in
|
||||
/home/svn/hooks/post-commit). The script should look something
|
||||
like this:
|
||||
|
||||
#!/bin/sh
|
||||
REPOS="$1"
|
||||
REV="$2"
|
||||
<VIEWVC_INSTALLATION_DIRECTORY>/bin/svndbadmin rebuild "$REPOS" "$REV"
|
||||
|
||||
|
||||
#!/bin/sh
|
||||
REPOS="$1"
|
||||
REV="$2"
|
||||
<VIEWVC_INSTALLATION_DIRECTORY>/bin/svndbadmin update \
|
||||
"$REPOS" "$REV"
|
||||
|
||||
If you allow revision property changes in your repository,
|
||||
create a post-revprop-change hook script containing the same
|
||||
commands as the post-commit one. This will make sure that the
|
||||
checkin database stays consistent when you change the svn:log,
|
||||
svn:author or svn:date revision properties.
|
||||
create a post-revprop-change hook script which uses the same
|
||||
'svndbadmin update' command as the post-commit script, except
|
||||
with the addition of the --force option:
|
||||
|
||||
#!/bin/sh
|
||||
REPOS="$1"
|
||||
REV="$2"
|
||||
<VIEWVC_INSTALLATION_DIRECTORY>/bin/svndbadmin update --force \
|
||||
"$REPOS" "$REV"
|
||||
|
||||
This will make sure that the checkin database stays consistent
|
||||
when you change the svn:log, svn:author or svn:date revision
|
||||
properties.
|
||||
|
||||
You should be ready to go. Click one of the "Query revision history"
|
||||
links in ViewVC directory listings and give it a try.
|
||||
@@ -390,20 +471,11 @@ links in ViewVC directory listings and give it a try.
|
||||
ENABLING SYNTAX COLORATION
|
||||
--------------------------
|
||||
|
||||
Enscript and Highlight are two programs that can colorize source code
|
||||
for a lot of languages. ViewVC can be configured to use either one.
|
||||
|
||||
1) Install Enscript or Highlight using your system's package manager
|
||||
or downloading from the project home pages.
|
||||
|
||||
2) Set either the 'use_enscript' or 'use_highlight' options in
|
||||
viewvc.conf to 1.
|
||||
|
||||
3) You may also need to set 'enscript_path' or 'highlight_path' option
|
||||
if the executables are not located on the system PATH.
|
||||
|
||||
That's it! Now when you view the contents of recognized filetypes in
|
||||
ViewVC, you should see colorized syntax.
|
||||
ViewVC uses Pygments (http://pygments.org) for syntax coloration. You
|
||||
need only install a suitable version of that module, and if ViewVC
|
||||
finds it in your Python module path, it will use it (unless you
|
||||
specifically disable the feature by setting use_pygments = 0 in your
|
||||
viewvc.conf file).
|
||||
|
||||
|
||||
CVSGRAPH CONFIGURATION
|
||||
@@ -441,9 +513,12 @@ SUBVERSION INTEGRATION
|
||||
Unlike the CVS integration, which simply wraps the RCS and CVS utility
|
||||
programs, the Subversion integration requires additional Python
|
||||
libraries. To use ViewVC with Subversion, make sure you have both
|
||||
Subversion itself and the Subversion Python bindings installed. See
|
||||
Subversion's installation notes for more details on how to build and
|
||||
install these items.
|
||||
Subversion itself and the Subversion Python bindings installed. These
|
||||
can be obtained through typical package distribution mechanisms, or
|
||||
may be build from source. (See the files 'INSTALL' and
|
||||
'subversion/bindings/swig/INSTALL' in the Subversion source tree for
|
||||
more details on how to build and install Subversion and its Python
|
||||
bindings.)
|
||||
|
||||
Generally speaking, you'll know when your installation of Subversion's
|
||||
bindings has been successful if you can import the 'svn.core' module
|
||||
@@ -457,7 +532,7 @@ Python binding you have:
|
||||
Type "help", "copyright", "credits" or "license" for more information.
|
||||
>>> from svn.core import *
|
||||
>>> "%s.%s.%s" % (SVN_VER_MAJOR, SVN_VER_MINOR, SVN_VER_PATCH)
|
||||
'1.2.0'
|
||||
'1.3.1'
|
||||
>>>
|
||||
|
||||
Note that by default, Subversion installs its bindings in a location
|
||||
@@ -465,7 +540,7 @@ that is not in Python's default module search path (for example, on
|
||||
Linux systems the default is usually /usr/local/lib/svn-python). You
|
||||
need to remedy this, either by adding this path to Python's module
|
||||
search path, or by relocating the bindings to some place in that
|
||||
search path.
|
||||
search path.
|
||||
|
||||
For example, you might want to create .pth file in your Python
|
||||
installation directory's site-packages area which tells Python where
|
||||
@@ -501,7 +576,7 @@ error: you can't see any files)
|
||||
CVS-Repository. The CGI-script generally runs as the same user
|
||||
that the web server does, often user 'nobody' or 'httpd'.
|
||||
|
||||
* Does ViewVC find your RCS utilities? (edit rcs_path)
|
||||
* Does ViewVC find your RCS utilities? (edit rcs_dir)
|
||||
|
||||
If something else happens or you can't get it to work:
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<blockquote>
|
||||
|
||||
<p><strong>Copyright © 1999-2008 The ViewCVS Group. All rights
|
||||
<p><strong>Copyright © 1999-2012 The ViewCVS Group. All rights
|
||||
reserved.</strong></p>
|
||||
|
||||
<p>By using ViewVC, you agree to the terms and conditions set forth
|
||||
@@ -59,6 +59,10 @@
|
||||
<li>March 17, 2006 — software renamed from "ViewCVS"</li>
|
||||
<li>April 10, 2007 — copyright years updated</li>
|
||||
<li>February 22, 2008 — copyright years updated</li>
|
||||
<li>March 18, 2009 — copyright years updated</li>
|
||||
<li>March 29, 2010 — copyright years updated</li>
|
||||
<li>February 18, 2011 — copyright years updated</li>
|
||||
<li>January 23, 2012 — copyright years updated</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
|
||||
53
TODO
53
TODO
@@ -1,53 +0,0 @@
|
||||
PREFACE
|
||||
-------
|
||||
This file will go away soon after release 0.8. Please use the SourceForge
|
||||
tracker to resubmit any of the items listed below, if you think, it is
|
||||
still an issue:
|
||||
http://sourceforge.net/tracker/?group_id=18760
|
||||
Before reporting please check, whether someone else has already done this.
|
||||
Working patches increase the chance to be included into the next release.
|
||||
-- PeFu / October 2001
|
||||
|
||||
TODO ITEMS
|
||||
----------
|
||||
*) add Tamminen Eero's comments on how to make Linux directly execute
|
||||
the Python script. From email on Feb 19.
|
||||
[ add other examples, such as my /bin/sh hack or the teeny CGI stub
|
||||
importing the bulk hack ]
|
||||
|
||||
*) insert rcs_path into PATH before calling "rcsdiff". rcsdiff might
|
||||
use "co" and needs to find it on the path.
|
||||
|
||||
*) show the "locked" flag (attach it to the LogEntry objects).
|
||||
Idea from Russell Gordon <russell@hoopscotch.dhs.org>
|
||||
|
||||
*) committing with a specific revision number:
|
||||
http://mailman.lyra.org/pipermail/viewcvs/2000q1/000008.html
|
||||
|
||||
*) add capability similar to cvs2cl.pl:
|
||||
http://mailman.lyra.org/pipermail/viewcvs/2000q2/000050.html
|
||||
suggestion from Chris Meyer <cmeyer@gatan.com>.
|
||||
|
||||
*) add a tree view of the directory structure (and files?)
|
||||
|
||||
*) include a ConfigParser.py to help older Python installations
|
||||
|
||||
*) add a check for the rcs programs/paths to viewvc-install. clarify the
|
||||
dependency on RCS in the docs.
|
||||
|
||||
*) have a "check" mode that verifies binaries are available on rcs_path
|
||||
|
||||
-> alternately (probably?): use rcsparse rather than external tools
|
||||
|
||||
KNOWN BUGS
|
||||
----------
|
||||
*) time.timezone seems to not be available on some 1.5.2 installs.
|
||||
I was unable to verify this. On RedHat and SuSE Linux this bug
|
||||
is non existant.
|
||||
|
||||
*) With old repositories containing many branches, tags or thousands
|
||||
or revisions, the cvsgraph feature becomes unusable (see INSTALL).
|
||||
ViewVC can't do much about this, but it might be possible to
|
||||
investigate the number of branches, tags and revision in advance
|
||||
and disable the cvsgraph links, if the numbers exceed a certain
|
||||
treshold.
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -54,7 +54,10 @@ import query
|
||||
server = sapi.AspServer(Server, Request, Response, Application)
|
||||
try:
|
||||
cfg = viewvc.load_config(CONF_PATHNAME, server)
|
||||
query.main(server, cfg, "viewvc.asp")
|
||||
viewvc_base_url = cfg.query.viewvc_base_url
|
||||
if viewvc_base_url is None:
|
||||
viewvc_base_url = "viewvc.asp"
|
||||
query.main(server, cfg, viewvc_base_url)
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -54,4 +54,7 @@ import query
|
||||
|
||||
server = sapi.CgiServer()
|
||||
cfg = viewvc.load_config(CONF_PATHNAME, server)
|
||||
query.main(server, cfg, "viewvc.cgi")
|
||||
viewvc_base_url = cfg.query.viewvc_base_url
|
||||
if viewvc_base_url is None:
|
||||
viewvc_base_url = "viewvc.cgi"
|
||||
query.main(server, cfg, viewvc_base_url)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
|
||||
127
bin/cvsdbadmin
127
bin/cvsdbadmin
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -43,10 +43,10 @@ import os
|
||||
import string
|
||||
import cvsdb
|
||||
import viewvc
|
||||
import vclib.bincvs
|
||||
import vclib.ccvs
|
||||
|
||||
|
||||
def UpdateFile(db, repository, path, update):
|
||||
def UpdateFile(db, repository, path, update, quiet_level):
|
||||
try:
|
||||
if update:
|
||||
commit_list = cvsdb.GetUnrecordedCommitList(repository, path, db)
|
||||
@@ -57,20 +57,27 @@ def UpdateFile(db, repository, path, update):
|
||||
return
|
||||
|
||||
file = string.join(path, "/")
|
||||
printing = 0
|
||||
if update:
|
||||
print '[%s [%d new commits]]' % (file, len(commit_list)),
|
||||
if quiet_level < 1 or (quiet_level < 2 and len(commit_list)):
|
||||
printing = 1
|
||||
print '[%s [%d new commits]]' % (file, len(commit_list)),
|
||||
else:
|
||||
print '[%s [%d commits]]' % (file, len(commit_list)),
|
||||
if quiet_level < 2:
|
||||
printing = 1
|
||||
print '[%s [%d commits]]' % (file, len(commit_list)),
|
||||
|
||||
## add the commits into the database
|
||||
for commit in commit_list:
|
||||
db.AddCommit(commit)
|
||||
sys.stdout.write('.')
|
||||
if printing:
|
||||
sys.stdout.write('.')
|
||||
sys.stdout.flush()
|
||||
print
|
||||
if printing:
|
||||
print
|
||||
|
||||
|
||||
def RecurseUpdate(db, repository, directory, update):
|
||||
def RecurseUpdate(db, repository, directory, update, quiet_level):
|
||||
for entry in repository.listdir(directory, None, {}):
|
||||
path = directory + [entry.name]
|
||||
|
||||
@@ -78,13 +85,13 @@ def RecurseUpdate(db, repository, directory, update):
|
||||
continue
|
||||
|
||||
if entry.kind is vclib.DIR:
|
||||
RecurseUpdate(db, repository, path, update)
|
||||
RecurseUpdate(db, repository, path, update, quiet_level)
|
||||
continue
|
||||
|
||||
if entry.kind is vclib.FILE:
|
||||
UpdateFile(db, repository, path, update)
|
||||
UpdateFile(db, repository, path, update, quiet_level)
|
||||
|
||||
def RootPath(path):
|
||||
def RootPath(path, quiet_level):
|
||||
"""Break os path into cvs root path and other parts"""
|
||||
root = os.path.abspath(path)
|
||||
path_parts = []
|
||||
@@ -93,14 +100,16 @@ def RootPath(path):
|
||||
while 1:
|
||||
if os.path.exists(os.path.join(p, 'CVSROOT')):
|
||||
root = p
|
||||
print "Using repository root `%s'" % root
|
||||
if quiet_level < 2:
|
||||
print "Using repository root `%s'" % root
|
||||
break
|
||||
|
||||
p, pdir = os.path.split(p)
|
||||
if not pdir:
|
||||
del path_parts[:]
|
||||
print "Using repository root `%s'" % root
|
||||
print "Warning: CVSROOT directory not found."
|
||||
if quiet_level < 1:
|
||||
print "Using repository root `%s'" % root
|
||||
print "Warning: CVSROOT directory not found."
|
||||
break
|
||||
|
||||
path_parts.append(pdir)
|
||||
@@ -110,46 +119,76 @@ def RootPath(path):
|
||||
return root, path_parts
|
||||
|
||||
def usage():
|
||||
print 'Usage: %s <command> [arguments]' % (sys.argv[0])
|
||||
print 'Performs administrative functions for the CVSdb database'
|
||||
print 'Commands:'
|
||||
print ' rebuild <repository> rebuilds the CVSdb database'
|
||||
print ' for all files in the repository'
|
||||
print ' update <repository> updates the CVSdb database for'
|
||||
print ' all unrecorded commits'
|
||||
print
|
||||
cmd = os.path.basename(sys.argv[0])
|
||||
sys.stderr.write(
|
||||
"""Administer the ViewVC checkins database data for the CVS repository
|
||||
located at REPOS-PATH.
|
||||
|
||||
Usage: 1. %s [[-q] -q] rebuild REPOS-PATH
|
||||
2. %s [[-q] -q] update REPOS-PATH
|
||||
3. %s [[-q] -q] purge REPOS-PATH
|
||||
|
||||
1. Rebuild the commit database information for the repository located
|
||||
at REPOS-PATH, after first purging information specific to that
|
||||
repository (if any).
|
||||
|
||||
2. Update the commit database information for all unrecorded commits
|
||||
in the repository located at REPOS-PATH.
|
||||
|
||||
3. Purge information specific to the repository located at REPOS-PATH
|
||||
from the database.
|
||||
|
||||
Use the -q flag to cause this script to be less verbose; use it twice to
|
||||
invoke a peaceful state of noiselessness.
|
||||
|
||||
""" % (cmd, cmd, cmd))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
## main
|
||||
if __name__ == '__main__':
|
||||
## check that a command was given
|
||||
if len(sys.argv) <= 2:
|
||||
args = sys.argv
|
||||
|
||||
# check the quietness level (0 = verbose, 1 = new commits, 2 = silent)
|
||||
quiet_level = 0
|
||||
while 1:
|
||||
try:
|
||||
index = args.index('-q')
|
||||
quiet_level = quiet_level + 1
|
||||
del args[index]
|
||||
except ValueError:
|
||||
break
|
||||
|
||||
# validate the command
|
||||
if len(args) <= 2:
|
||||
usage()
|
||||
command = args[1].lower()
|
||||
if command not in ('rebuild', 'update', 'purge'):
|
||||
sys.stderr.write('ERROR: unknown command %s\n' % command)
|
||||
usage()
|
||||
|
||||
## set the handler function for the command
|
||||
command = sys.argv[1]
|
||||
if string.lower(command) == 'rebuild':
|
||||
update = 0
|
||||
elif string.lower(command) == 'update':
|
||||
update = 1
|
||||
else:
|
||||
print 'ERROR: unknown command %s' % (command)
|
||||
usage()
|
||||
|
||||
# get repository path
|
||||
root, path_parts = RootPath(sys.argv[2])
|
||||
|
||||
## run command
|
||||
# get repository and path, and do the work
|
||||
root, path_parts = RootPath(args[2], quiet_level)
|
||||
rootpath = vclib.ccvs.canonicalize_rootpath(root)
|
||||
try:
|
||||
## connect to the database we are updating
|
||||
cfg = viewvc.load_config(CONF_PATHNAME)
|
||||
db = cvsdb.ConnectDatabase(cfg)
|
||||
cfg = viewvc.load_config(CONF_PATHNAME)
|
||||
db = cvsdb.ConnectDatabase(cfg)
|
||||
|
||||
repository = vclib.bincvs.BinCVSRepository(None, root, cfg.general)
|
||||
|
||||
RecurseUpdate(db, repository, path_parts, update)
|
||||
if command in ('rebuild', 'purge'):
|
||||
if quiet_level < 2:
|
||||
print "Purging existing data for repository root `%s'" % root
|
||||
try:
|
||||
db.PurgeRepository(root)
|
||||
except cvsdb.UnknownRepositoryError, e:
|
||||
if command == 'purge':
|
||||
sys.stderr.write("ERROR: " + str(e) + "\n")
|
||||
sys.exit(1)
|
||||
|
||||
if command in ('rebuild', 'update'):
|
||||
repository = vclib.ccvs.CVSRepository(None, rootpath, None,
|
||||
cfg.utilities, 0)
|
||||
RecurseUpdate(db, repository, path_parts,
|
||||
command == 'update', quiet_level)
|
||||
except KeyboardInterrupt:
|
||||
print
|
||||
print '** break **'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -44,7 +44,7 @@ import getopt
|
||||
import re
|
||||
import cvsdb
|
||||
import viewvc
|
||||
import vclib.bincvs
|
||||
import vclib.ccvs
|
||||
|
||||
DEBUG_FLAG = 0
|
||||
|
||||
@@ -223,7 +223,8 @@ def NextFile(s, pos = 0):
|
||||
def ProcessLoginfo(rootpath, directory, files):
|
||||
cfg = viewvc.load_config(CONF_PATHNAME)
|
||||
db = cvsdb.ConnectDatabase(cfg)
|
||||
repository = vclib.bincvs.BinCVSRepository(None, rootpath, cfg.general)
|
||||
repository = vclib.ccvs.CVSRepository(None, rootpath, None,
|
||||
cfg.utilities, 0)
|
||||
|
||||
# split up the directory components
|
||||
dirpath = filter(None, string.split(os.path.normpath(directory), os.sep))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -18,21 +18,22 @@
|
||||
|
||||
import os, sys, string
|
||||
import popen2
|
||||
import getopt
|
||||
|
||||
INTRO_TEXT = """\
|
||||
This script creates the database and tables in MySQL used by the ViewVC
|
||||
checkin database. You will be prompted for: database user, database user
|
||||
password, and database name. This script will use mysql to create the
|
||||
database for you. You will then need to set the appropriate parameters
|
||||
in your viewvc.conf file under the [cvsdb] section.
|
||||
"""
|
||||
|
||||
DATABASE_SCRIPT="""\
|
||||
## ------------------------------------------------------------------------
|
||||
## Stuff common to all schemas
|
||||
##
|
||||
DATABASE_SCRIPT_COMMON="""\
|
||||
DROP DATABASE IF EXISTS <dbname>;
|
||||
CREATE DATABASE <dbname>;
|
||||
|
||||
USE <dbname>;
|
||||
"""
|
||||
|
||||
## ------------------------------------------------------------------------
|
||||
## Version 0: The original, Bonsai-compatible schema.
|
||||
##
|
||||
DATABASE_SCRIPT_VERSION_0="""\
|
||||
DROP TABLE IF EXISTS branches;
|
||||
CREATE TABLE branches (
|
||||
id mediumint(9) NOT NULL auto_increment,
|
||||
@@ -92,7 +93,7 @@ CREATE TABLE files (
|
||||
DROP TABLE IF EXISTS people;
|
||||
CREATE TABLE people (
|
||||
id mediumint(9) NOT NULL auto_increment,
|
||||
who varchar(32) binary DEFAULT '' NOT NULL,
|
||||
who varchar(128) binary DEFAULT '' NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE who (who)
|
||||
) TYPE=MyISAM;
|
||||
@@ -120,25 +121,213 @@ CREATE TABLE tags (
|
||||
) TYPE=MyISAM;
|
||||
"""
|
||||
|
||||
## ------------------------------------------------------------------------
|
||||
## Version 1: Adds the 'metadata' table. Adds 'descid' index to
|
||||
## 'checkins' table, and renames that table to 'commits'.
|
||||
##
|
||||
DATABASE_SCRIPT_VERSION_1="""\
|
||||
DROP TABLE IF EXISTS branches;
|
||||
CREATE TABLE branches (
|
||||
id mediumint(9) NOT NULL auto_increment,
|
||||
branch varchar(64) binary DEFAULT '' NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE branch (branch)
|
||||
) TYPE=MyISAM;
|
||||
|
||||
DROP TABLE IF EXISTS commits;
|
||||
CREATE TABLE commits (
|
||||
type enum('Change','Add','Remove'),
|
||||
ci_when datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
|
||||
whoid mediumint(9) DEFAULT '0' NOT NULL,
|
||||
repositoryid mediumint(9) DEFAULT '0' NOT NULL,
|
||||
dirid mediumint(9) DEFAULT '0' NOT NULL,
|
||||
fileid mediumint(9) DEFAULT '0' NOT NULL,
|
||||
revision varchar(32) binary DEFAULT '' NOT NULL,
|
||||
stickytag varchar(255) binary DEFAULT '' NOT NULL,
|
||||
branchid mediumint(9) DEFAULT '0' NOT NULL,
|
||||
addedlines int(11) DEFAULT '0' NOT NULL,
|
||||
removedlines int(11) DEFAULT '0' NOT NULL,
|
||||
descid mediumint(9),
|
||||
UNIQUE repositoryid (repositoryid,dirid,fileid,revision),
|
||||
KEY ci_when (ci_when),
|
||||
KEY whoid (whoid),
|
||||
KEY repositoryid_2 (repositoryid),
|
||||
KEY dirid (dirid),
|
||||
KEY fileid (fileid),
|
||||
KEY branchid (branchid),
|
||||
KEY descid (descid)
|
||||
) TYPE=MyISAM;
|
||||
|
||||
DROP TABLE IF EXISTS descs;
|
||||
CREATE TABLE descs (
|
||||
id mediumint(9) NOT NULL auto_increment,
|
||||
description text,
|
||||
hash bigint(20) DEFAULT '0' NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY hash (hash)
|
||||
) TYPE=MyISAM;
|
||||
|
||||
DROP TABLE IF EXISTS dirs;
|
||||
CREATE TABLE dirs (
|
||||
id mediumint(9) NOT NULL auto_increment,
|
||||
dir varchar(255) binary DEFAULT '' NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE dir (dir)
|
||||
) TYPE=MyISAM;
|
||||
|
||||
DROP TABLE IF EXISTS files;
|
||||
CREATE TABLE files (
|
||||
id mediumint(9) NOT NULL auto_increment,
|
||||
file varchar(255) binary DEFAULT '' NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE file (file)
|
||||
) TYPE=MyISAM;
|
||||
|
||||
DROP TABLE IF EXISTS people;
|
||||
CREATE TABLE people (
|
||||
id mediumint(9) NOT NULL auto_increment,
|
||||
who varchar(128) binary DEFAULT '' NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE who (who)
|
||||
) TYPE=MyISAM;
|
||||
|
||||
DROP TABLE IF EXISTS repositories;
|
||||
CREATE TABLE repositories (
|
||||
id mediumint(9) NOT NULL auto_increment,
|
||||
repository varchar(64) binary DEFAULT '' NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE repository (repository)
|
||||
) TYPE=MyISAM;
|
||||
|
||||
DROP TABLE IF EXISTS tags;
|
||||
CREATE TABLE tags (
|
||||
repositoryid mediumint(9) DEFAULT '0' NOT NULL,
|
||||
branchid mediumint(9) DEFAULT '0' NOT NULL,
|
||||
dirid mediumint(9) DEFAULT '0' NOT NULL,
|
||||
fileid mediumint(9) DEFAULT '0' NOT NULL,
|
||||
revision varchar(32) binary DEFAULT '' NOT NULL,
|
||||
UNIQUE repositoryid (repositoryid,dirid,fileid,branchid,revision),
|
||||
KEY repositoryid_2 (repositoryid),
|
||||
KEY dirid (dirid),
|
||||
KEY fileid (fileid),
|
||||
KEY branchid (branchid)
|
||||
) TYPE=MyISAM;
|
||||
|
||||
DROP TABLE IF EXISTS metadata;
|
||||
CREATE TABLE metadata (
|
||||
name varchar(255) binary DEFAULT '' NOT NULL,
|
||||
value text,
|
||||
PRIMARY KEY (name),
|
||||
UNIQUE name (name)
|
||||
) TYPE=MyISAM;
|
||||
INSERT INTO metadata (name, value) VALUES ('version', '1');
|
||||
"""
|
||||
|
||||
BONSAI_COMPAT="""
|
||||
WARNING: Creating Bonsai-compatible legacy database version. Some ViewVC
|
||||
features may not be available, or may not perform especially well.
|
||||
|
||||
"""
|
||||
|
||||
## ------------------------------------------------------------------------
|
||||
|
||||
def usage_and_exit(errmsg=None):
|
||||
stream = errmsg is None and sys.stdout or sys.stderr
|
||||
stream.write("""\
|
||||
Usage: %s [OPTIONS]
|
||||
|
||||
This script creates the database and tables in MySQL used by the
|
||||
ViewVC checkin database. In order to operate correctly, it needs to
|
||||
know the following: your database server hostname, database user,
|
||||
database user password, and database name. (You will be prompted for
|
||||
any of this information that you do not provide via command-line
|
||||
options.) This script will use the 'mysql' program to create the
|
||||
database for you. You will then need to set the appropriate
|
||||
parameters in the [cvsdb] section of your viewvc.conf file.
|
||||
|
||||
Options:
|
||||
|
||||
--dbname=ARG Use ARG as the ViewVC database name to create.
|
||||
[Default: ViewVC]
|
||||
|
||||
--help Show this usage message.
|
||||
|
||||
--hostname=ARG Use ARG as the hostname for the MySQL connection.
|
||||
[Default: localhost]
|
||||
|
||||
--password=ARG Use ARG as the password for the MySQL connection.
|
||||
|
||||
--username=ARG Use ARG as the username for the MySQL connection.
|
||||
|
||||
--version=ARG Create the database using the schema employed by
|
||||
version ARG of ViewVC. Valid values are:
|
||||
[ "1.0" ]
|
||||
|
||||
""" % (os.path.basename(sys.argv[0])))
|
||||
if errmsg is not None:
|
||||
stream.write("[ERROR] %s.\n" % (errmsg))
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print INTRO_TEXT
|
||||
|
||||
user = raw_input("MySQL User: ")
|
||||
passwd = raw_input("MySQL Password: ")
|
||||
dbase = raw_input("ViewVC Database Name [default: ViewVC]: ")
|
||||
if not dbase:
|
||||
dbase = "ViewVC"
|
||||
try:
|
||||
# Parse the command-line options, if any.
|
||||
dbname = version = hostname = username = password = None
|
||||
opts, args = getopt.getopt(sys.argv[1:], '', [ 'dbname=',
|
||||
'help',
|
||||
'hostname=',
|
||||
'password=',
|
||||
'username=',
|
||||
'version=',
|
||||
])
|
||||
if len(args) > 0:
|
||||
usage_and_exit("Unexpected command-line parameters")
|
||||
for name, value in opts:
|
||||
if name == '--help':
|
||||
usage_and_exit(0)
|
||||
elif name == '--dbname':
|
||||
dbname = value
|
||||
elif name == '--hostname':
|
||||
hostname = value
|
||||
elif name == '--username':
|
||||
username = value
|
||||
elif name == '--password':
|
||||
password = value
|
||||
elif name == '--version':
|
||||
if value in ["1.0"]:
|
||||
version = value
|
||||
else:
|
||||
usage_and_exit("Invalid version specified")
|
||||
|
||||
dscript = string.replace(DATABASE_SCRIPT, "<dbname>", dbase)
|
||||
# Prompt for information not provided via command-line options.
|
||||
if hostname is None:
|
||||
hostname = raw_input("MySQL Hostname [default: localhost]: ") or ""
|
||||
if username is None:
|
||||
username = raw_input("MySQL User: ")
|
||||
if password is None:
|
||||
password = raw_input("MySQL Password: ")
|
||||
if dbname is None:
|
||||
dbname = raw_input("ViewVC Database Name [default: ViewVC]: ") or "ViewVC"
|
||||
|
||||
# Create the database
|
||||
dscript = string.replace(DATABASE_SCRIPT_COMMON, "<dbname>", dbname)
|
||||
if version == "1.0":
|
||||
print BONSAI_COMPAT
|
||||
dscript = dscript + DATABASE_SCRIPT_VERSION_0
|
||||
else:
|
||||
dscript = dscript + DATABASE_SCRIPT_VERSION_1
|
||||
|
||||
host_option = hostname and "--host=%s" % (hostname) or ""
|
||||
if sys.platform == "win32":
|
||||
# popen2.Popen3 is not provided on windows
|
||||
cmd = "mysql --user=%s --password=%s" % (user, passwd)
|
||||
mysql = os.popen(cmd, "w")
|
||||
cmd = "mysql --user=%s --password=%s %s "\
|
||||
% (username, password, host_option)
|
||||
mysql = os.popen(cmd, "w") # popen2.Popen3 is not provided on windows
|
||||
mysql.write(dscript)
|
||||
status = mysql.close()
|
||||
else:
|
||||
cmd = "{ mysql --user=%s --password=%s ; } 2>&1" % (user, passwd)
|
||||
cmd = "{ mysql --user=%s --password=%s %s ; } 2>&1" \
|
||||
% (username, password, host_option)
|
||||
pipes = popen2.Popen3(cmd)
|
||||
pipes.tochild.write(dscript)
|
||||
pipes.tochild.close()
|
||||
@@ -146,9 +335,12 @@ if __name__ == "__main__":
|
||||
status = pipes.wait()
|
||||
|
||||
if status:
|
||||
print "[ERROR] the database did not create sucessfully."
|
||||
sys.exit(1)
|
||||
print "[ERROR] The database did not create sucessfully."
|
||||
sys.exit(1)
|
||||
|
||||
print "Database created successfully."
|
||||
sys.exit(0)
|
||||
print "Database created successfully. Don't forget to configure the "
|
||||
print "[cvsdb] section of your viewvc.conf file."
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -42,16 +42,33 @@ if LIBRARY_DIR:
|
||||
sys.path.insert(0, LIBRARY_DIR)
|
||||
|
||||
import sapi
|
||||
import viewvc
|
||||
import query
|
||||
reload(query) # need reload because initial import loads this stub file
|
||||
import imp
|
||||
|
||||
# Import real ViewVC module
|
||||
fp, pathname, description = imp.find_module('viewvc', [LIBRARY_DIR])
|
||||
try:
|
||||
viewvc = imp.load_module('viewvc', fp, pathname, description)
|
||||
finally:
|
||||
if fp:
|
||||
fp.close()
|
||||
|
||||
# Import real ViewVC Query modules
|
||||
fp, pathname, description = imp.find_module('query', [LIBRARY_DIR])
|
||||
try:
|
||||
query = imp.load_module('query', fp, pathname, description)
|
||||
finally:
|
||||
if fp:
|
||||
fp.close()
|
||||
|
||||
cfg = viewvc.load_config(CONF_PATHNAME)
|
||||
|
||||
def index(req):
|
||||
server = sapi.ModPythonServer(req)
|
||||
try:
|
||||
query.main(server, cfg, "viewvc.py")
|
||||
viewvc_base_url = cfg.query.viewvc_base_url
|
||||
if viewvc_base_url is None:
|
||||
viewvc_base_url = "viewvc.py"
|
||||
query.main(server, cfg, viewvc_base_url)
|
||||
finally:
|
||||
server.close()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -42,9 +42,15 @@ if LIBRARY_DIR:
|
||||
sys.path.insert(0, LIBRARY_DIR)
|
||||
|
||||
import sapi
|
||||
import viewvc
|
||||
reload(viewvc) # need reload because initial import loads this stub file
|
||||
import imp
|
||||
|
||||
# Import real ViewVC module
|
||||
fp, pathname, description = imp.find_module('viewvc', [LIBRARY_DIR])
|
||||
try:
|
||||
viewvc = imp.load_module('viewvc', fp, pathname, description)
|
||||
finally:
|
||||
if fp:
|
||||
fp.close()
|
||||
|
||||
def index(req):
|
||||
server = sapi.ModPythonServer(req)
|
||||
|
||||
1272
bin/standalone.py
1272
bin/standalone.py
File diff suppressed because it is too large
Load Diff
205
bin/svndbadmin
205
bin/svndbadmin
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 2004 James Henstridge
|
||||
# Copyright (C) 2004-2012 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 2004-2007 James Henstridge
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -27,8 +28,8 @@
|
||||
#
|
||||
# If you allow changes to revision properties in your repository, you
|
||||
# might also want to set up something similar in the
|
||||
# post-revprop-change hook using "rebuild" instead of "update" to keep
|
||||
# the checkin database consistent with the repository.
|
||||
# post-revprop-change hook using "update" with the --force option to
|
||||
# keep the checkin database consistent with the repository.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
#
|
||||
@@ -49,9 +50,9 @@ import sys
|
||||
import os
|
||||
|
||||
if LIBRARY_DIR:
|
||||
sys.path.insert(0, LIBRARY_DIR)
|
||||
sys.path.insert(0, LIBRARY_DIR)
|
||||
else:
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
|
||||
|
||||
#########################################################################
|
||||
|
||||
@@ -66,24 +67,22 @@ import svn.delta
|
||||
|
||||
import cvsdb
|
||||
import viewvc
|
||||
import vclib
|
||||
|
||||
class SvnRepo:
|
||||
"""Class used to manage a connection to a SVN repository."""
|
||||
def __init__(self, path, pool):
|
||||
self.scratch_pool = svn.core.svn_pool_create(pool)
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.repo = svn.repos.svn_repos_open(path, pool)
|
||||
self.repo = svn.repos.svn_repos_open(path)
|
||||
self.fs = svn.repos.svn_repos_fs(self.repo)
|
||||
# youngest revision of base of file system is highest revision number
|
||||
self.rev_max = svn.fs.youngest_rev(self.fs, pool)
|
||||
self.rev_max = svn.fs.youngest_rev(self.fs)
|
||||
def __getitem__(self, rev):
|
||||
if rev is None:
|
||||
rev = self.rev_max
|
||||
elif rev < 0:
|
||||
rev = rev + self.rev_max + 1
|
||||
assert 0 <= rev <= self.rev_max
|
||||
rev = SvnRev(self, rev, self.scratch_pool)
|
||||
svn.core.svn_pool_clear(self.scratch_pool)
|
||||
rev = SvnRev(self, rev)
|
||||
return rev
|
||||
|
||||
_re_diff_change_command = re.compile('(\d+)(?:,(\d+))?([acd])(\d+)(?:,(\d+))?')
|
||||
@@ -133,57 +132,65 @@ def _get_diff_counts(diff_fp):
|
||||
class SvnRev:
|
||||
"""Class used to hold information about a particular revision of
|
||||
the repository."""
|
||||
def __init__(self, repo, rev, pool):
|
||||
def __init__(self, repo, rev):
|
||||
self.repo = repo
|
||||
self.rev = rev
|
||||
self.pool = pool
|
||||
self.rev_roots = {} # cache of revision roots
|
||||
|
||||
subpool = svn.core.svn_pool_create(pool)
|
||||
|
||||
# revision properties ...
|
||||
properties = svn.fs.revision_proplist(repo.fs, rev, pool)
|
||||
self.author = str(properties.get(svn.core.SVN_PROP_REVISION_AUTHOR,''))
|
||||
self.date = str(properties.get(svn.core.SVN_PROP_REVISION_DATE, ''))
|
||||
self.log = str(properties.get(svn.core.SVN_PROP_REVISION_LOG, ''))
|
||||
revprops = svn.fs.revision_proplist(repo.fs, rev)
|
||||
self.author = str(revprops.get(svn.core.SVN_PROP_REVISION_AUTHOR,''))
|
||||
self.date = str(revprops.get(svn.core.SVN_PROP_REVISION_DATE, ''))
|
||||
self.log = str(revprops.get(svn.core.SVN_PROP_REVISION_LOG, ''))
|
||||
|
||||
# convert the date string to seconds since epoch ...
|
||||
self.date = svn.core.secs_from_timestr(self.date, pool)
|
||||
try:
|
||||
self.date = svn.core.svn_time_from_cstring(self.date) / 1000000
|
||||
except:
|
||||
self.date = None
|
||||
|
||||
# get a root for the current revisions
|
||||
fsroot = self._get_root_for_rev(rev)
|
||||
|
||||
# find changes in the revision
|
||||
editor = svn.repos.RevisionChangeCollector(repo.fs, rev, pool)
|
||||
e_ptr, e_baton = svn.delta.make_editor(editor, pool)
|
||||
svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton, pool)
|
||||
editor = svn.repos.ChangeCollector(repo.fs, fsroot)
|
||||
e_ptr, e_baton = svn.delta.make_editor(editor)
|
||||
svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton)
|
||||
|
||||
self.changes = []
|
||||
for path, change in editor.changes.items():
|
||||
|
||||
# clear the iteration subpool
|
||||
svn.core.svn_pool_clear(subpool)
|
||||
|
||||
# skip non-file changes
|
||||
if change.item_kind != svn.core.svn_node_file: continue
|
||||
if change.item_kind != svn.core.svn_node_file:
|
||||
continue
|
||||
|
||||
# deal with the change types we handle
|
||||
action = None
|
||||
base_root = None
|
||||
base_path = change.base_path
|
||||
if change.base_path:
|
||||
base_root = self._get_root_for_rev(change.base_rev)
|
||||
|
||||
if not change.path:
|
||||
|
||||
# figure out what kind of change this is, and get a diff
|
||||
# object for it. note that prior to 1.4 Subversion's
|
||||
# bindings didn't give us change.action, but that's okay
|
||||
# because back then deleted paths always had a change.path
|
||||
# of None.
|
||||
if hasattr(change, 'action') \
|
||||
and change.action == svn.repos.CHANGE_ACTION_DELETE:
|
||||
action = 'remove'
|
||||
elif not change.path:
|
||||
action = 'remove'
|
||||
elif change.added:
|
||||
action = 'add'
|
||||
else:
|
||||
action = 'change'
|
||||
|
||||
diffobj = svn.fs.FileDiff(base_root and base_root or None,
|
||||
base_root and change.base_path or None,
|
||||
change.path and fsroot or None,
|
||||
change.path and change.path or None,
|
||||
subpool, [])
|
||||
if action == 'remove':
|
||||
diffobj = svn.fs.FileDiff(base_root, base_path, None, None)
|
||||
else:
|
||||
diffobj = svn.fs.FileDiff(base_root, base_path,
|
||||
fsroot, change.path)
|
||||
|
||||
diff_fp = diffobj.get_pipe()
|
||||
plus, minus = _get_diff_counts(diff_fp)
|
||||
self.changes.append((path, action, plus, minus))
|
||||
@@ -192,12 +199,11 @@ class SvnRev:
|
||||
"""Fetch a revision root from a cache of such, or a fresh root
|
||||
(which is then cached for later use."""
|
||||
if not self.rev_roots.has_key(rev):
|
||||
self.rev_roots[rev] = svn.fs.revision_root(self.repo.fs, rev,
|
||||
self.pool)
|
||||
self.rev_roots[rev] = svn.fs.revision_root(self.repo.fs, rev)
|
||||
return self.rev_roots[rev]
|
||||
|
||||
|
||||
def handle_revision(db, command, repo, rev, verbose):
|
||||
def handle_revision(db, command, repo, rev, verbose, force=0):
|
||||
"""Adds a particular revision of the repository to the checkin database."""
|
||||
revision = repo[rev]
|
||||
committed = 0
|
||||
@@ -231,7 +237,7 @@ def handle_revision(db, command, repo, rev, verbose):
|
||||
|
||||
if command == 'update':
|
||||
result = db.CheckCommit(commit)
|
||||
if result:
|
||||
if result and not force:
|
||||
continue # already recorded
|
||||
|
||||
# commit to database
|
||||
@@ -244,38 +250,83 @@ def handle_revision(db, command, repo, rev, verbose):
|
||||
else:
|
||||
print "skipped (already recorded)."
|
||||
|
||||
def main(pool, command, repository, rev=None, verbose=0):
|
||||
def main(command, repository, revs=[], verbose=0, force=0):
|
||||
cfg = viewvc.load_config(CONF_PATHNAME)
|
||||
db = cvsdb.ConnectDatabase(cfg)
|
||||
|
||||
repo = SvnRepo(repository, pool)
|
||||
if rev:
|
||||
handle_revision(db, command, repo, rev, verbose)
|
||||
# Purge what must be purged.
|
||||
if command in ('rebuild', 'purge'):
|
||||
if verbose:
|
||||
print "Purging commit info for repository root `%s'" % repository
|
||||
try:
|
||||
db.PurgeRepository(repository)
|
||||
except cvsdb.UnknownRepositoryError, e:
|
||||
if command == 'purge':
|
||||
sys.stderr.write("ERROR: " + str(e) + "\n")
|
||||
sys.exit(1)
|
||||
|
||||
# Record what must be recorded.
|
||||
if command in ('rebuild', 'update'):
|
||||
if not os.path.exists(repository):
|
||||
sys.stderr.write('ERROR: could not find repository %s\n'
|
||||
% (repository))
|
||||
sys.exit(1)
|
||||
repo = SvnRepo(repository)
|
||||
if command == 'rebuild' or (command == 'update' and not revs):
|
||||
for rev in range(repo.rev_max+1):
|
||||
handle_revision(db, command, repo, rev, verbose)
|
||||
elif command == 'update':
|
||||
if revs[0] is None:
|
||||
revs[0] = repo.rev_max
|
||||
if revs[1] is None:
|
||||
revs[1] = repo.rev_max
|
||||
revs.sort()
|
||||
for rev in range(revs[0], revs[1]+1):
|
||||
handle_revision(db, command, repo, rev, verbose, force)
|
||||
|
||||
def _rev2int(r):
|
||||
if r == 'HEAD':
|
||||
r = None
|
||||
else:
|
||||
for rev in range(repo.rev_max+1):
|
||||
handle_revision(db, command, repo, rev, verbose)
|
||||
r = int(r)
|
||||
if r < 0:
|
||||
raise ValueError, "invalid revision '%d'" % (r)
|
||||
return r
|
||||
|
||||
def usage():
|
||||
cmd = os.path.basename(sys.argv[0])
|
||||
sys.stderr.write("""
|
||||
Usage: 1. %s [-v] rebuild REPOSITORY [REVISION]
|
||||
2. %s [-v] update REPOSITORY [REVISION]
|
||||
sys.stderr.write(
|
||||
"""Administer the ViewVC checkins database data for the Subversion repository
|
||||
located at REPOS-PATH.
|
||||
|
||||
1. Rebuild the commit database information for REPOSITORY across all revisions
|
||||
or, optionally, only for the specified REVISION.
|
||||
Usage: 1. %s [-v] rebuild REPOS-PATH
|
||||
2. %s [-v] update REPOS-PATH [REV[:REV2]] [--force]
|
||||
3. %s [-v] purge REPOS-PATH
|
||||
|
||||
2. Update the commit database information for REPOSITORY across all revisions
|
||||
or, optionally, only for the specified REVISION. This is just like
|
||||
rebuilding, except that no commit information will be stored for
|
||||
commits already present in the database.
|
||||
1. Rebuild the commit database information for the repository located
|
||||
at REPOS-PATH across all revisions, after first purging
|
||||
information specific to that repository (if any).
|
||||
|
||||
2. Update the commit database information for the repository located
|
||||
at REPOS-PATH across all revisions or, optionally, only for the
|
||||
specified revision REV (or revision range REV:REV2). This is just
|
||||
like rebuilding, except that, unless --force is specified, no
|
||||
commit information will be stored for commits already present in
|
||||
the database. If a range is specified, the revisions will be
|
||||
processed in ascending order, and you may specify "HEAD" to
|
||||
indicate "the youngest revision currently in the repository".
|
||||
|
||||
3. Purge information specific to the repository located at REPOS-PATH
|
||||
from the database.
|
||||
|
||||
Use the -v flag to cause this script to give progress information as it works.
|
||||
""" % (cmd, cmd))
|
||||
|
||||
""" % (cmd, cmd, cmd))
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
verbose = 0
|
||||
|
||||
force = 0
|
||||
args = sys.argv
|
||||
try:
|
||||
index = args.index('-v')
|
||||
@@ -283,29 +334,47 @@ if __name__ == '__main__':
|
||||
del args[index]
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
index = args.index('--force')
|
||||
force = 1
|
||||
del args[index]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if len(args) < 3:
|
||||
usage()
|
||||
|
||||
command = string.lower(args[1])
|
||||
if command not in ('rebuild', 'update'):
|
||||
if command not in ('rebuild', 'update', 'purge'):
|
||||
sys.stderr.write('ERROR: unknown command %s\n' % command)
|
||||
usage()
|
||||
|
||||
repository = args[2]
|
||||
if not os.path.exists(repository):
|
||||
sys.stderr.write('ERROR: could not find repository %s\n' % repository)
|
||||
usage()
|
||||
|
||||
revs = []
|
||||
if len(sys.argv) > 3:
|
||||
rev = sys.argv[3]
|
||||
if command == 'rebuild':
|
||||
sys.stderr.write('ERROR: rebuild no longer accepts a revision '
|
||||
'number argument. Usage update --force.')
|
||||
usage()
|
||||
elif command != 'update':
|
||||
usage()
|
||||
try:
|
||||
rev = int(rev)
|
||||
revs = map(lambda x: _rev2int(x), sys.argv[3].split(':'))
|
||||
if len(revs) > 2:
|
||||
raise ValueError, "too many revisions in range"
|
||||
if len(revs) == 1:
|
||||
revs.append(revs[0])
|
||||
except ValueError:
|
||||
sys.stderr.write('ERROR: revision "%s" is not numeric\n' % rev)
|
||||
sys.stderr.write('ERROR: invalid revision specification "%s"\n' \
|
||||
% sys.argv[3])
|
||||
usage()
|
||||
else:
|
||||
rev = None
|
||||
|
||||
repository = cvsdb.CleanRepository(os.path.abspath(repository))
|
||||
svn.core.run_app(main, command, repository, rev, verbose)
|
||||
try:
|
||||
repository = vclib.svn.canonicalize_rootpath(args[2])
|
||||
repository = cvsdb.CleanRepository(os.path.abspath(repository))
|
||||
main(command, repository, revs, verbose, force)
|
||||
except KeyboardInterrupt:
|
||||
print
|
||||
print '** break **'
|
||||
sys.exit(0)
|
||||
|
||||
54
bin/wsgi/query.fcgi
Normal file
54
bin/wsgi/query.fcgi
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/python
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
#
|
||||
# viewvc: View CVS/SVN repositories via a web browser
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
#
|
||||
# This is a fcgi entry point for the query ViewVC app. It's appropriate
|
||||
# for use with mod_fcgid and flup. It defines a single application function
|
||||
# that is a valid fcgi entry point.
|
||||
#
|
||||
# mod_fcgid: http://httpd.apache.org/mod_fcgid/
|
||||
# flup:
|
||||
# http://pypi.python.org/pypi/flup
|
||||
# http://trac.saddi.com/flup
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import sys, os
|
||||
|
||||
LIBRARY_DIR = None
|
||||
CONF_PATHNAME = None
|
||||
|
||||
if LIBRARY_DIR:
|
||||
sys.path.insert(0, LIBRARY_DIR)
|
||||
else:
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0],
|
||||
"../../../lib")))
|
||||
|
||||
import sapi
|
||||
import viewvc
|
||||
import query
|
||||
from flup.server import fcgi
|
||||
|
||||
def application(environ, start_response):
|
||||
server = sapi.WsgiServer(environ, start_response)
|
||||
cfg = viewvc.load_config(CONF_PATHNAME, server)
|
||||
viewvc_base_url = cfg.query.viewvc_base_url
|
||||
if viewvc_base_url is None:
|
||||
viewvc_base_url = "viewvc.fcgi"
|
||||
query.main(server, cfg, viewvc_base_url)
|
||||
return []
|
||||
|
||||
fcgi.WSGIServer(application).run()
|
||||
45
bin/wsgi/query.wsgi
Normal file
45
bin/wsgi/query.wsgi
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
#
|
||||
# viewvc: View CVS/SVN repositories via a web browser
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
#
|
||||
# This is a wsgi entry point for the query ViewVC app. It's appropriate
|
||||
# for use with mod_wsgi. It defines a single application function that
|
||||
# is a valid wsgi entry point.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import sys, os
|
||||
|
||||
LIBRARY_DIR = None
|
||||
CONF_PATHNAME = None
|
||||
|
||||
if LIBRARY_DIR:
|
||||
sys.path.insert(0, LIBRARY_DIR)
|
||||
else:
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0],
|
||||
"../../../lib")))
|
||||
|
||||
import sapi
|
||||
import viewvc
|
||||
import query
|
||||
|
||||
def application(environ, start_response):
|
||||
server = sapi.WsgiServer(environ, start_response)
|
||||
cfg = viewvc.load_config(CONF_PATHNAME, server)
|
||||
viewvc_base_url = cfg.query.viewvc_base_url
|
||||
if viewvc_base_url is None:
|
||||
viewvc_base_url = "viewvc.wsgi"
|
||||
query.main(server, cfg, viewvc_base_url)
|
||||
return []
|
||||
50
bin/wsgi/viewvc.fcgi
Normal file
50
bin/wsgi/viewvc.fcgi
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/python
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
#
|
||||
# viewvc: View CVS/SVN repositories via a web browser
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
#
|
||||
# This is a fcgi entry point for the main ViewVC app. It's appropriate
|
||||
# for use with mod_fcgid and flup. It defines a single application function
|
||||
# that is a valid fcgi entry point.
|
||||
#
|
||||
# mod_fcgid: http://httpd.apache.org/mod_fcgid/
|
||||
# flup:
|
||||
# http://pypi.python.org/pypi/flup
|
||||
# http://trac.saddi.com/flup
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import sys, os
|
||||
|
||||
LIBRARY_DIR = None
|
||||
CONF_PATHNAME = None
|
||||
|
||||
if LIBRARY_DIR:
|
||||
sys.path.insert(0, LIBRARY_DIR)
|
||||
else:
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0],
|
||||
"../../../lib")))
|
||||
|
||||
import sapi
|
||||
import viewvc
|
||||
from flup.server import fcgi
|
||||
|
||||
def application(environ, start_response):
|
||||
server = sapi.WsgiServer(environ, start_response)
|
||||
cfg = viewvc.load_config(CONF_PATHNAME, server)
|
||||
viewvc.main(server, cfg)
|
||||
return []
|
||||
|
||||
fcgi.WSGIServer(application).run()
|
||||
41
bin/wsgi/viewvc.wsgi
Normal file
41
bin/wsgi/viewvc.wsgi
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
#
|
||||
# viewvc: View CVS/SVN repositories via a web browser
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
#
|
||||
# This is a wsgi entry point for the main ViewVC app. It's appropriate
|
||||
# for use with mod_wsgi. It defines a single application function that
|
||||
# is a valid wsgi entry point.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import sys, os
|
||||
|
||||
LIBRARY_DIR = None
|
||||
CONF_PATHNAME = None
|
||||
|
||||
if LIBRARY_DIR:
|
||||
sys.path.insert(0, LIBRARY_DIR)
|
||||
else:
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0],
|
||||
"../../../lib")))
|
||||
|
||||
import sapi
|
||||
import viewvc
|
||||
|
||||
def application(environ, start_response):
|
||||
server = sapi.WsgiServer(environ, start_response)
|
||||
cfg = viewvc.load_config(CONF_PATHNAME, server)
|
||||
viewvc.main(server, cfg)
|
||||
return []
|
||||
32
conf/mimetypes.conf.dist
Normal file
32
conf/mimetypes.conf.dist
Normal file
@@ -0,0 +1,32 @@
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
# MIME type mapping file for ViewVC
|
||||
#
|
||||
# Information on ViewVC is located at the following web site:
|
||||
# http://viewvc.org/
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
|
||||
# THE FORMAT OF THIS FILE
|
||||
#
|
||||
# This file contains records -- one per line -- of the following format:
|
||||
#
|
||||
# MIME_TYPE [EXTENSION [EXTENSION ...]]
|
||||
#
|
||||
# where whitespace separates the MIME_TYPE from the EXTENSION(s),
|
||||
# and the EXTENSIONs from each other.
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# text/x-csh csh
|
||||
# text/x-csrc c
|
||||
# text/x-diff diff patch
|
||||
# image/png png
|
||||
# image/jpeg jpeg jpg jpe
|
||||
#
|
||||
# By default, this file is left empty, allowing ViewVC to continue
|
||||
# consulting it first without overriding the MIME type mappings
|
||||
# found in more standard mapping files (such as those provided as
|
||||
# part of the operating system or web server software).
|
||||
#
|
||||
#
|
||||
1153
conf/viewvc.conf.dist
Normal file
1153
conf/viewvc.conf.dist
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,35 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>ViewVC: Upgrading</title>
|
||||
<link rel="stylesheet" type="text/css" href="./styles.css"/>
|
||||
<style>
|
||||
body {
|
||||
background-color: rgb(180,193,205);
|
||||
color: black;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
table { margin-left: 1em; }
|
||||
td, th { padding: 0 0.5em; }
|
||||
th {
|
||||
vertical-align: bottom;
|
||||
background: rgb(60%,70%,90%);
|
||||
}
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
.h2, .h3 {
|
||||
padding: 0.5em 1em;
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
margin-bottom: 1em;
|
||||
background: white;
|
||||
}
|
||||
.h2 { border-width: 1px 2px 2px 1px; }
|
||||
.h3 { border-width: 1px 0 0 0; }
|
||||
.toc-list { font-size: 90%; }
|
||||
.varname { font-family: monospace; }
|
||||
.unchanged { background: rgb(50%,75%,50%); }
|
||||
.renamed { background: rgb(75%,100%,75%); }
|
||||
.added { background: rgb(50%,75%,25%); }
|
||||
.unchanged { background: rgb(75%,75%,75%); }
|
||||
.renamed { background: rgb(75%,50%,75%); }
|
||||
.changed { background: rgb(100%,100%,25%); }
|
||||
.replaced { background: rgb(100%,75%,0%); }
|
||||
.removed { background: rgb(100%,25%,25%); }
|
||||
@@ -15,45 +39,10 @@
|
||||
|
||||
<body>
|
||||
|
||||
<div id="title">
|
||||
<a href="http://www.viewvc.org/"><img
|
||||
src="./images/title.jpg" alt="ViewVC: Repository Browsing"/></a>
|
||||
</div>
|
||||
<h1>Upgrading ViewVC</h1>
|
||||
|
||||
<div id="menu">
|
||||
<p><a href="./index.html">Home</a> |
|
||||
<a href="http://viewvc.tigris.org/">Project Page</a> |
|
||||
<a href="./download.html">Download</a> |
|
||||
<a href="./upgrading.html">Upgrading</a> |
|
||||
<a href="./contributing.html">Contributing</a> |
|
||||
<a href="./license-1.html">License</a> |
|
||||
<a href="./contact.html">Contact</a> |
|
||||
<a href="./who.html">About</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<table id="pagetable">
|
||||
<tr>
|
||||
<td id="pagecolumn1">
|
||||
|
||||
<h4>On this page:</h4>
|
||||
|
||||
<ul id="bookmarks">
|
||||
<li><a href="#sec-upgrading">Upgrading Basics</a></li>
|
||||
<li><a href="#sec-from-0-9">Upgrading from 0.9</a></li>
|
||||
<li><a href="#sec-from-0-8">Upgrading from 0.8</a></li>
|
||||
</ul>
|
||||
|
||||
<hr/>
|
||||
|
||||
<address><a href="mailto:users@viewvc.tigris.org">ViewVC Users Group</a></address>
|
||||
|
||||
</td>
|
||||
<td id="pagecolumn2">
|
||||
|
||||
<div class="section">
|
||||
|
||||
<h2 id="sec-upgrading">Upgrading Basics</h2>
|
||||
<div class="h2">
|
||||
<h2 id="introduction">Introduction</h2>
|
||||
|
||||
<p>This document describes some of the things that you will need to
|
||||
consider, change, or handle when upgrading an existing ViewVC
|
||||
@@ -67,18 +56,632 @@
|
||||
directory and to carefully compare the configuration files. A
|
||||
possible approach is to name the directories
|
||||
<code>/usr/local/viewvc-1.0</code>,
|
||||
<code>/usr/local/viewcvs-1.1</code> and so on and than create a
|
||||
<code>/usr/local/viewvc-1.1</code> and so on and than create a
|
||||
symbolic link <code>viewvc</code> pointing to the production
|
||||
version. This way you can easily test several versions and switch
|
||||
back if your users start to complain.</p>
|
||||
|
||||
</div>
|
||||
<div class="section">
|
||||
|
||||
<div class="h2">
|
||||
<h2 id="toc">Table of Contents</h2>
|
||||
<ul class="toc-list">
|
||||
<li><a href="#introduction">Introduction</a></li>
|
||||
<li><a href="#sec-from-1-0">Upgrading From ViewVC 1.0</a></li>
|
||||
<li><a href="#sec-from-0-9">Upgrading From ViewCVS 0.9</a></li>
|
||||
<li><a href="#sec-from-0-8">Upgrading From ViewCVS 0.8</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="h2">
|
||||
<h2 id="sec-from-1-0">Upgrading From ViewVC 1.0</h2>
|
||||
|
||||
<p>This section discusses how to upgrade ViewVC 1.0.x to ViewVC 1.1.x.</p>
|
||||
|
||||
<div class="h3">
|
||||
<h3>root_as_url_component Now Enabled by Default</h3>
|
||||
|
||||
<p>In ViewVC 1.1, the <code>root_as_url_component</code> configuration
|
||||
option is now enabled by default. This option causes ViewVC URLs
|
||||
to be of the form
|
||||
<code>…/root-name/path-in-root[?…]</code> instead of
|
||||
<code>…/path-in-root/?root=root-name[&…]</code>,
|
||||
and makes for a much more intuitive user experience,
|
||||
navigation-wise, when ViewVC is serving up multiple version control
|
||||
repositories. When in this mode, ViewVC will automatically perform
|
||||
the obvious redirection for URLs which have a <code>root=</code>
|
||||
CGI parameter.</p>
|
||||
|
||||
<p>Unfortunately, there's a catch. Older URLs for the default root
|
||||
(specified by the <code>default_root</code> configuration option)
|
||||
were optimized to <em>not</em> include the <code>root=</code> CGI
|
||||
parameter. This means they look unfortunately similar to the newer
|
||||
root-in-the-path URL format, and ViewVC will not attempt to
|
||||
redirect them. But ViewVC won't be able to handle them, either.
|
||||
So, old-style default-root URLs, when aimed at a ViewVC for which
|
||||
the <code>root_as_url_component</code> option has been subsequently
|
||||
enabled, will result in an error. If you need to preserve the
|
||||
functionality of those old URLs, you'll need to either disable
|
||||
<code>root_as_url_component</code>, or use some other mechanism
|
||||
(like server URL rewriting) to morph them into compliance with the
|
||||
new URL format.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="h3">
|
||||
<h3>Path-Based Authorization / Forbidden Modules</h3>
|
||||
|
||||
<p>ViewVC 1.1 introduces a new pluggable authorization (authz)
|
||||
subsystem which gives administrators greater control over the
|
||||
accessibility of the information ViewVC displays in its output.
|
||||
ViewVC provides a number of working authz modules and a framework for
|
||||
configuring them. But of specific interest to folks upgrading from
|
||||
ViewVC 1.0 is that one of these new modules has replaced the
|
||||
handling of forbidden modules. As such, the <code>forbidden</code>
|
||||
configuration option now lives under the configuration section
|
||||
specific to that authz module.</p>
|
||||
|
||||
<p>Migrating your existing configuration of forbidden modules should
|
||||
be fairly straightforward:</p>
|
||||
|
||||
<ol>
|
||||
|
||||
<li>In the new "authz-forbidden" section of viewvc.conf, set the
|
||||
<code>forbidden</code> option to the same value as the
|
||||
<code>forbidden</code> option in your ViewVC 1.0.x
|
||||
configuration's "general" section.</li>
|
||||
|
||||
<li>In the new "authz-forbiddenre" section of viewvc.conf, set the
|
||||
<code>forbiddenre</code> option to the same value as the
|
||||
<code>forbiddenre</code> option in your ViewVC 1.0.x
|
||||
configuration's "general" section.</li>
|
||||
|
||||
<li>Finally, ensure that that the new <code>authorizer</code>
|
||||
option is set to either "forbidden" or "forbiddenre", depending
|
||||
on which of those you were using in ViewVC 1.0.x.</li>
|
||||
|
||||
</ol>
|
||||
|
||||
<p>Of course, you might wish to take advantage of another of the
|
||||
provided authz modules. Or, you might wish to write a brand new
|
||||
one for your purposes. The flexibility is yours.</p>
|
||||
|
||||
<p><strong>Known Issues:</strong></p>
|
||||
|
||||
<ul>
|
||||
|
||||
<li>ViewVC does not provide an <em>authentication</em> framework.
|
||||
It does, however, inherit authenticated usernames as determined
|
||||
by the HTTP server (Apache, e.g.) via the CGI environment. So,
|
||||
any authorization module that assigns privileges based on
|
||||
usernames will work only if ViewVC is deployed in a way that
|
||||
requires successful authentication (as opposed to allowing
|
||||
anonymous access).</li>
|
||||
|
||||
<li>Currently, the root listing view only honors the global or
|
||||
vhost-specific configurations, <em>not</em> any root-specific
|
||||
configuration. In the event that ViewVC is using root-specific
|
||||
configuration for its authorization stuffs, this may cause
|
||||
either the unintended leak of root names to users or the
|
||||
inability to see roots at all. However, for root-specific
|
||||
ViewVC views, all configuration — include root-specific
|
||||
configuration — is honored. If you are concerned about
|
||||
leaking root names in the root listing view, you might consider
|
||||
disabling that view altogether by removing <code>roots</code>
|
||||
from the list of views specified in the
|
||||
<code>allowed_views</code> configuration option.</li>
|
||||
|
||||
<li>The experimental module which allows ViewVC to serve up views
|
||||
of remote Subversion repositories is not yet fully integrated
|
||||
with the authorization subsystem, and almost certainly will
|
||||
leak privileged data. Sorry. That's (one reason) why it's
|
||||
experimental.</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="h3">
|
||||
<h3>Syntax Highlighting</h3>
|
||||
|
||||
<p>ViewVC 1.0.x supports syntax highlighting provided by multiple
|
||||
third-party highlighting engines, including GNU enscript, GNU
|
||||
source-highlight, highlight, php, and py2html. Unfortunately, each
|
||||
of those integrations worked differently than the others. Some
|
||||
supported line numbers, some didn't; some were under active
|
||||
development and recognized newer languages; some weren't; each had
|
||||
its own set of CSS stylations that needed to be customized;
|
||||
etc.</p>
|
||||
|
||||
<p>In ViewVC 1.1, we've dropped support for all of those integations
|
||||
in favor of a single integration with <a
|
||||
href="http://www.pygments.org/" >Pygments</a>, a
|
||||
Python-module-based syntax highlighting engine. As such, the
|
||||
configuration options for the various other engines (both those
|
||||
that enabled the integration and those that specified the locations
|
||||
of the third-party tools) have been removed from ViewVC, and have
|
||||
been replaced by a single new configuration option:
|
||||
<code>enable_syntax_coloration</code>.</p>
|
||||
|
||||
<p>The list of removed options is as follows:</p>
|
||||
|
||||
<ul>
|
||||
<li>options/enscript_path</li>
|
||||
<li>options/highlight_convert_tabs</li>
|
||||
<li>options/highlight_path</li>
|
||||
<li>options/markup_line_numbers</li>
|
||||
<li>options/php_exe</li>
|
||||
<li>options/py2html_path</li>
|
||||
<li>options/use_enscript</li>
|
||||
<li>options/use_highlight</li>
|
||||
<li>options/use_pagesize</li>
|
||||
<li>options/use_php</li>
|
||||
<li>options/use_py2html</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="h3">
|
||||
<h3>Checkin Database</h3>
|
||||
|
||||
<p>ViewVC 1.1 introduces to the <code>cvsdbadmin</code>
|
||||
and <code>svndbadmin</code> tools a new "purge" operation, which
|
||||
allows you to remove all the information related to a given root
|
||||
from your checkins database (without disturbing the information
|
||||
associated with other roots). Likewise, the "rebuild" command in
|
||||
those tools now implies a "purge" followed by an update.</p>
|
||||
|
||||
<p>As a related change, the <code>svndbadmin</code> program's
|
||||
"rebuild" subcommand has had its purpose become more defined. It
|
||||
no longer accepts a revision argument, and therefore can now only
|
||||
be used to completely rebuild the entirety of the checkin database
|
||||
information for a Subversion repository (instead of being able to
|
||||
only update the information related to single Subversion revision).
|
||||
For per-revision updating, use <code>svndbadmin update</code> and
|
||||
provide a revision (or revision range). And to get the previous
|
||||
rebuild-a-revision effect, pass the new <code>--force</code> option
|
||||
to <code>svndbadmin update</code>.</p>
|
||||
|
||||
<p>In other words, where you once did this:</p>
|
||||
|
||||
<blockquote><pre>$ svndbadmin rebuild /path/to/repository 1234
|
||||
</pre>
|
||||
</blockquote>
|
||||
|
||||
<p>you now need to do this:</p>
|
||||
|
||||
<blockquote><pre>$ svndbadmin update /path/to/repository 1234 --force
|
||||
</pre>
|
||||
</blockquote>
|
||||
|
||||
<p>To enhance the performance of the new "purge" operation, ViewVC 1.1
|
||||
introduces some slight changes to the checkin database schema. If
|
||||
you use the <code>make-database</code> tool to (re)create your
|
||||
checkins database, it will by default employ the new database
|
||||
schema. This should cause the database to be virtually unusable by
|
||||
previous versions of ViewVC, and that's by design. If, however,
|
||||
you need to (re)create your checkins database and you require
|
||||
compatibility with previous versions of ViewVC or ViewCVS, simply
|
||||
pass the "--version=1.0" option to the <code>make-database</code>
|
||||
script. Note that your "purge" and "rebuild" operations could be
|
||||
abysmally slow, though, as that version of the database schema is
|
||||
not optimized for those operations.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="h3">
|
||||
<h3>Configuration Options</h3>
|
||||
|
||||
<p>This section covers changes to configuration options not already
|
||||
discussed in other sections pertaining to this upgrade.</p>
|
||||
|
||||
<p>In ViewVC 1.1, a new "utilities" section was added to the
|
||||
viewvc.conf file. All the options used for configuring the
|
||||
locations of various helper applications that ViewVC uses which
|
||||
were previously scattered throughout the configuration file are now
|
||||
all centralized in this one new section. To accomplish this, the
|
||||
following options were added:</p>
|
||||
|
||||
<ul>
|
||||
<li>utilities/cvsgraph</li>
|
||||
<li>utilities/cvsnt</li>
|
||||
<li>utilities/diff</li>
|
||||
<li>utilities/rcs_dir</li>
|
||||
<li>utilities/svn</li>
|
||||
</ul>
|
||||
|
||||
<p>…and these were removed:</p>
|
||||
|
||||
<ul>
|
||||
<li>general/cvsnt_ext_path</li>
|
||||
<li>general/rcs_path</li>
|
||||
<li>general/svn_path</li>
|
||||
<li>options/cvsgraph_path</li>
|
||||
</ul>
|
||||
|
||||
<p>All the options which governed which ViewVC views were enabled have
|
||||
been consolidated into a single new option. This new option:</p>
|
||||
|
||||
<ul>
|
||||
<li>options/allowed_views</li>
|
||||
</ul>
|
||||
|
||||
<p>…replaces these, which have been removed:</p>
|
||||
|
||||
<ul>
|
||||
<li>options/allow_annotate</li>
|
||||
<li>options/allow_markup</li>
|
||||
<li>options/allow_tar</li>
|
||||
</ul>
|
||||
|
||||
<p>ViewVC now honors the "svn:mime-type" property stored on
|
||||
Subversion-versioned files as the primary source of MIME type
|
||||
determination (before falling back to name-based MIME mappings and
|
||||
such). However, this can negatively affect the viewability of
|
||||
certain files — especially images — whose
|
||||
"svn:mime-type" properties are set incorrectly, such as will happen
|
||||
if Subversion itself merely determines that the file isn't
|
||||
human-readable and uses the "application/octet-stream" MIME type to
|
||||
record this determination. To make ViewVC <em>not</em> honor the
|
||||
"svn:mime-type" property value, set the <code>svn_ignore_mimetype</code>
|
||||
configuration option.</p>
|
||||
|
||||
<p>Speaking of MIME types, the option <code>mime_types_file</code> is
|
||||
now <code>mime_types_files</code>, as it now carries multiple paths
|
||||
to MIME mappings files, ordered by preference.</p>
|
||||
|
||||
<p>The <code>use_rcsparse</code> option was moved from the "general"
|
||||
section to the "options" section.</p>
|
||||
|
||||
<p>The <code>log_sort</code> option's value "cvs" has been renamed to
|
||||
"none" (for general application across all supported version
|
||||
control systems).</p>
|
||||
|
||||
<p>Custom sections which define per-virtual-host configuration option
|
||||
overrides must now have their names prefixed with "vhost-". Also,
|
||||
instead of a hyphen (-) between the virtual host name and the base
|
||||
configuration section being overridden, now there should be a
|
||||
forward slash character (/). For example, the following
|
||||
configuration which was valid in ViewVC 1.0 is no longer valid:</p>
|
||||
|
||||
<blockquote><pre>[vhosts]
|
||||
all = viewvc.*
|
||||
|
||||
[all-options]
|
||||
allowed_views = annotate, diff, markup, tar
|
||||
</pre>
|
||||
</blockquote>
|
||||
|
||||
<p>This now needs to be written like so:</p>
|
||||
|
||||
<blockquote><pre>[vhosts]
|
||||
all = viewvc.*
|
||||
|
||||
[vhost-all/options]
|
||||
allowed_views = annotate, diff, markup, tar
|
||||
</pre>
|
||||
</blockquote>
|
||||
|
||||
<p>The following is a grab-bag of additional new options:</p>
|
||||
|
||||
<ul>
|
||||
<li>options/hide_errorful_entries</li>
|
||||
<li>options/mangle_email_addresses</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="h3">
|
||||
<h3>Templates</h3>
|
||||
|
||||
<p>This section describes template variable changes introduced in this
|
||||
release. See the <a href="./template-authoring-guide.html">Template
|
||||
Authoring Guide</a> for the current set of variables available to
|
||||
each templates.</p>
|
||||
|
||||
<p>One notable change in ViewVC 1.1 is that the markup.ezt and
|
||||
annotate.ezt templates have been combined into a single file.ezt
|
||||
template.</p>
|
||||
|
||||
<p>Also, the configuration options under the <code>[templates]</code>
|
||||
section are now paths relative to the configured template directory
|
||||
(either the value of the <code>options/template_dir</code>
|
||||
option, or the default "templates" directory), instead of being
|
||||
relative to the configuration location.</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Location</th>
|
||||
<th>Changes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="added">
|
||||
<td class="varname">rootpath</td>
|
||||
<td><em>all templates</em></td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="removed">
|
||||
<td class="varname">change_root_action</td>
|
||||
<td><em>all templates</em></td>
|
||||
<td>removed</td>
|
||||
</tr>
|
||||
<tr class="removed">
|
||||
<td class="varname">change_root_hidden_values</td>
|
||||
<td><em>all templates</em></td>
|
||||
<td>removed</td>
|
||||
</tr>
|
||||
<tr class="removed">
|
||||
<td class="varname">roots</td>
|
||||
<td><em>all templates</em> except roots.ezt</td>
|
||||
<td>removed</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">roots.path</td>
|
||||
<td>roots.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">queryform_href</td>
|
||||
<td>diff.ezt, file.ezt, graph.ezt, log.ezt, log_table.ezt,
|
||||
query_form.ezt, revision.ezt, roots.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">tarball_href</td>
|
||||
<td>diff.ezt, file.ezt, graph.ezt, log.ezt, log_table.ezt,
|
||||
query_form.ezt, query_results.ezt, revision.ezt, roots.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">properties</td>
|
||||
<td>directory.ezt, file.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">properties.name</td>
|
||||
<td>directory.ezt, file.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">properties.undisplayable</td>
|
||||
<td>directory.ezt, file.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">properties.value</td>
|
||||
<td>directory.ezt, file.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">diff_format_hidden_values</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>now is an iterable list of objects with .name and .value attributes</td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">diff_type</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>new value: <code>f</code></td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">date_left</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>renamed to <code>left.date</code></td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">path_left</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>renamed to <code>left.path</code></td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">rev_left</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>renamed to <code>left.rev</code></td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">tag_left</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>renamed to <code>left.tag</code></td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">date_right</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>renamed to <code>right.date</code></td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">path_right</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>renamed to <code>right.path</code></td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">rev_right</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>renamed to <code>right.rev</code></td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">tag_right</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>renamed to <code>right.tag</code></td>
|
||||
</tr>
|
||||
<tr class="removed">
|
||||
<td class="varname">annotate_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>removed, use <code>right.annotate_href</code> instead</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">left.annotate_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">left.download_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">left.download_text_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">left.prefer_markup</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">left.revision_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">left.view_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">right.annotate_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">right.download_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">right.download_text_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">right.prefer_markup</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">right.revision_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">right.view_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">right.view_href</td>
|
||||
<td>diff.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">dir_paging_hidden_values</td>
|
||||
<td>directory.ezt, , dir_alternate.ezt</td>
|
||||
<td>now is an iterable list of objects with .name and .value attributes</td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">entries.log</td>
|
||||
<td>directory.ezt, dir_alternate.ezt</td>
|
||||
<td>now always contains untruncated log message</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">entries.short_log</td>
|
||||
<td>directory.ezt, dir_alternate.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">search_re_hidden_values</td>
|
||||
<td>directory.ezt, dir_alternate.ezt</td>
|
||||
<td>now is an iterable list of objects with .name and .value attributes</td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">search_tag_hidden_values</td>
|
||||
<td>directory.ezt, dir_alternate.ezt</td>
|
||||
<td>now is an iterable list of objects with .name and .value attributes</td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">pathrev_clear_hidden_values</td>
|
||||
<td>log.ezt, log_table.ezt, directory.ezt, dir_alternate.ezt</td>
|
||||
<td>now is an iterable list of objects with .name and .value attributes</td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">pathrev_hidden_values</td>
|
||||
<td>log.ezt, log_table.ezt, directory.ezt, dir_alternate.ezt</td>
|
||||
<td>now is an iterable list of objects with .name and .value attributes</td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">annotate_href</td>
|
||||
<td>log.ezt, log_table.ezt</td>
|
||||
<td>renamed to <code>head_annotate_href</code></td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">diff_form_hidden_values</td>
|
||||
<td>log.ezt, log_table.ezt</td>
|
||||
<td>now is an iterable list of objects with .name and .value attributes</td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">download_href</td>
|
||||
<td>log.ezt, log_table.ezt</td>
|
||||
<td>renamed to <code>head_download_href</code></td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">download_text_href</td>
|
||||
<td>log.ezt, log_table.ezt</td>
|
||||
<td>renamed to <code>head_download_text_href</code></td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">log_paging_hidden_values</td>
|
||||
<td>log.ezt, log_table.ezt</td>
|
||||
<td>now is an iterable list of objects with .name and .value attributes</td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">logsort_hidden_values</td>
|
||||
<td>log.ezt, log_table.ezt</td>
|
||||
<td>now is an iterable list of objects with .name and .value attributes</td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">prefer_markup</td>
|
||||
<td>log.ezt, log_table.ezt</td>
|
||||
<td>renamed to <code>head_prefer_markup</code></td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">view_href</td>
|
||||
<td>log.ezt, log_table.ezt</td>
|
||||
<td>renamed to <code>head_view_href</code></td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">rss_link_href</td>
|
||||
<td>query.ezt, rss.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">query_hidden_values</td>
|
||||
<td>query_form.ezt</td>
|
||||
<td>now is an iterable list of objects with .name and .value attributes</td>
|
||||
</tr>
|
||||
<tr class="changed">
|
||||
<td class="varname">jump_rev_hidden_values</td>
|
||||
<td>revision.ezt</td>
|
||||
<td>now is an iterable list of objects with .name and .value attributes</td>
|
||||
</tr>
|
||||
<tr class="added">
|
||||
<td class="varname">num_changes</td>
|
||||
<td>revision.ezt</td>
|
||||
<td>added</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h2">
|
||||
<h2 id="sec-from-0-9">Upgrading From ViewCVS 0.9</h2>
|
||||
|
||||
<p>This section discusses how to upgrade ViewCVS 0.9 to ViewVC 1.0.</p>
|
||||
|
||||
<div class="h3">
|
||||
<h3>CGI Stubs</h3>
|
||||
|
||||
<p>The CGI stub scripts haved been moved from
|
||||
@@ -89,16 +692,22 @@
|
||||
changed, so you may need to replace copies of the old scripts you
|
||||
put in other directories.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="h3">
|
||||
<h3>Checkin Database</h3>
|
||||
|
||||
<p>ViewVC 1.0 reads and writes commit times in the MySQL database in
|
||||
UTC time rather than local time. This can cause times displayed on
|
||||
UTC time rather than local time. This can cause times displayed on
|
||||
the query page to be a few hours off if an old database is being
|
||||
used with a new version of ViewVC. The best way to fix this is to
|
||||
rebuild the database with the new version of cvsdbadmin, but it
|
||||
is also possible to enable a backwards compatibility mode by
|
||||
setting <code>utc_time = 0</code> at the top of lib/dbi.py</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="h3">
|
||||
<h3>"checkout_magic" Option</h3>
|
||||
|
||||
<p>In ViewVC 1.0, the <code>checkout_magic</code> option has been
|
||||
@@ -113,6 +722,9 @@
|
||||
be re-enabled. The <code>viewcvs.conf</code> file describes these
|
||||
options in detail.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="h3">
|
||||
<h3>Configuration Options</h3>
|
||||
|
||||
<p>The following options have been added:</p>
|
||||
@@ -129,7 +741,8 @@
|
||||
<li>options/root_as_url_component</li>
|
||||
<li>options/default_file_view</li>
|
||||
<li>options/sort_group_dirs</li>
|
||||
<li>options/use_pagesize</li>
|
||||
<li>options/dir_pagesize</li>
|
||||
<li>options/log_pagesize</li>
|
||||
<li>options/limit_changes</li>
|
||||
<li>options/use_localtime</li>
|
||||
<li>options/cross_copies</li>
|
||||
@@ -157,6 +770,9 @@
|
||||
<li>templates/footer</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="h3">
|
||||
<h3>Templates</h3>
|
||||
|
||||
<p>The templates have changed drastically in this version of ViewVC.
|
||||
@@ -761,7 +1377,7 @@
|
||||
<tr class="renamed">
|
||||
<td class="varname">rows.log</td>
|
||||
<td>dir_alternate.ezt, directory.ezt</td>
|
||||
<td>renamed to <code>entries.log</code></td>
|
||||
<td>renamed to <code>entries.short_log</code></td>
|
||||
</tr>
|
||||
<tr class="renamed">
|
||||
<td class="varname">rows.log_file</td>
|
||||
@@ -961,7 +1577,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Template Arrangement</h4>
|
||||
</div>
|
||||
|
||||
<div class="h3">
|
||||
<h3>Template Arrangement</h3>
|
||||
|
||||
<p>The default templates have been rearranged a bit in ViewVC 1.0.
|
||||
Specifically, "header.ezt" and "footer.ezt" have moved into the
|
||||
@@ -979,29 +1598,26 @@
|
||||
configuration file.</p>
|
||||
|
||||
</div>
|
||||
<div class="section">
|
||||
</div>
|
||||
|
||||
<div class="h2">
|
||||
<h2 id="sec-from-0-8">Upgrading From ViewCVS 0.8</h2>
|
||||
|
||||
<p>
|
||||
This section discusses how to upgrade ViewCVS 0.8 to version
|
||||
0.9 or a later version of the software.
|
||||
</p>
|
||||
<p>This section discusses how to upgrade ViewCVS 0.8 to version
|
||||
0.9 or a later version of the software.</p>
|
||||
|
||||
<p>
|
||||
<strong>NOTE:</strong> these changes will bring you up to the
|
||||
requirements of version 0.9. You must also follow the directions
|
||||
for <a href="#from9">upgrading from 0.9</a>.
|
||||
</p>
|
||||
<p><strong>NOTE:</strong> these changes will bring you up to the
|
||||
requirements of version 0.9. You must also follow the directions
|
||||
for <a href="#sec-from-0-9">upgrading from 0.9</a>.</p>
|
||||
|
||||
<h3>Configuration Options</h3>
|
||||
<p>
|
||||
More templates were introduced in version 0.8 of the software,
|
||||
which made many of the configuration options obsolete. This
|
||||
section covers which options were removed. If you made any
|
||||
changes to these options, then you will need to make
|
||||
corresponding changes in the templates.
|
||||
</p>
|
||||
<div class="h3">
|
||||
<h3>Configuration Options</h3>
|
||||
|
||||
<p>More templates were introduced in version 0.8 of the software,
|
||||
which made many of the configuration options obsolete. This section
|
||||
covers which options were removed. If you made any changes to these
|
||||
options, then you will need to make corresponding changes in the
|
||||
templates.</p>
|
||||
|
||||
<dl>
|
||||
<dt>
|
||||
@@ -1090,12 +1706,14 @@
|
||||
|
||||
</dl>
|
||||
|
||||
<h3>Template Variables</h3>
|
||||
<p>
|
||||
Some template variables that were available in 0.8 have been
|
||||
removed in 0.9. If you have custom templates that refer to these
|
||||
variables, then you will need to modify your templates.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="h3">
|
||||
<h3>Template Variables</h3>
|
||||
|
||||
<p>Some template variables that were available in 0.8 have been
|
||||
removed in 0.9. If you have custom templates that refer to these
|
||||
variables, then you will need to modify your templates.</p>
|
||||
|
||||
<dl>
|
||||
<dt><code>directory.ezt</code>: <var>headers</var></dt>
|
||||
@@ -1146,9 +1764,7 @@
|
||||
</dl>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>ViewVC 1.0 URL Reference</title>
|
||||
<title>ViewVC 1.1 URL Reference</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: rgb(180,193,205);
|
||||
@@ -37,14 +37,22 @@ th.caption {
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>ViewVC 1.0 URL Reference</h1>
|
||||
<h1>ViewVC 1.1 URL Reference</h1>
|
||||
|
||||
<div class="h2">
|
||||
<h2 id="introduction">Introduction</h2>
|
||||
|
||||
<p>This document describes the format of URLs accepted by ViewVC 1.0 and is intended to be useful to users and outside software developers who want to create links to ViewVC pages. It should also be useful to ViewVC developers as a way of tracking and documenting the many quirks of ViewVC's URL handling logic.</p>
|
||||
<p>This document describes the format of URLs accepted by ViewVC 1.1
|
||||
and is intended to be useful to users and outside software
|
||||
developers who want to create links to ViewVC pages. It should also
|
||||
be useful to ViewVC developers as a way of tracking and documenting
|
||||
the many quirks of ViewVC's URL handling logic.</p>
|
||||
|
||||
<p>Future releases of ViewVC will support the URL formats described in this document. Certain URLs currently accepted by ViewVC, such as redirecting URLs that result from the submission of form elements on ViewVC pages, are not documented here and therefore may not be supported by future releases of ViewVC.</p>
|
||||
<p>Future releases of ViewVC will support the URL formats described in
|
||||
this document. Certain URLs currently accepted by ViewVC, such as
|
||||
redirecting URLs that result from the submission of form elements
|
||||
on ViewVC pages, are not documented here and therefore may not be
|
||||
supported by future releases of ViewVC.</p>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -78,8 +86,9 @@ th.caption {
|
||||
</ul>
|
||||
<li><a href="#compat">Backwards Compatibility</a></li>
|
||||
<ul>
|
||||
<li><a href="#compat-viewrev">'<code>view=rev</code>' Parameter ⇒ '<code>view=revision</code>'</a></li>
|
||||
<li><a href="#compat-cvsroot">'<code>cvsroot</code>' Parameter ⇒ '<code>root</code>'</a></li>
|
||||
<li><a href="#compat-only_with_tag>'<code>only_with_tag</code>' Parameter ⇒ '<code>pathrev</code>'</a></li>
|
||||
<li><a href="#compat-only_with_tag">'<code>only_with_tag</code>' Parameter ⇒ '<code>pathrev</code>'</a></li>
|
||||
<li><a href="#compat-oldcheckout">'<code>~checkout~</code>' Magic Path Prefix ⇒ '<code>*checkout*</code>'</a></li>
|
||||
<li><a href="#compat-checkout">'<code>*checkout*</code>' Magic Path Prefix ⇒ '<code>view=co</code>'</a></li>
|
||||
<li><a href="#compat-root">'<code>root</code>' Parameter ⇒ Root Path Component</a></li>
|
||||
@@ -89,7 +98,7 @@ th.caption {
|
||||
<li><a href="#compat-tarball">'<code>tarball=1</code>' Parameter ⇒ '<code>view=tar</code>'</a></li>
|
||||
<li><a href="#compat-graph">'<code>graph=1</code>' Parameter ⇒ '<code>view=graph</code>'</a></li>
|
||||
<li><a href="#compat-makeimage">'<code>graph=1&makeimage=1</code>' Parameters ⇒ '<code>view=graphimg</code>'</a></li>
|
||||
<li><a href="#compat-content_type">'<code>content_type=text/vnd.viewcvs-markup</code>' and '<code>content_type=text/x-cvsweb-markup</code>' Parameters⇒ '<code>view=markup</code>'
|
||||
<li><a href="#compat-content_type">'<code>content-type=text/vnd.viewcvs-markup</code>' and '<code>content-type=text/x-cvsweb-markup</code>' Parameters⇒ '<code>view=markup</code>'
|
||||
<li><a href="#compat-attic">'<code>Attic/FILE</code>' Paths ⇒ '<code>FILE</code>'</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -98,7 +107,11 @@ th.caption {
|
||||
|
||||
<div class="h2">
|
||||
<h2 id="components">URL Components</h2>
|
||||
<p>A ViewVC URL consists of 3 components: a <a href="#script-component">Script Location</a>, a <a href="#path-component">Repository Path</a>, and some optional <a href="#query-component">Query Parameters</a>. Some examples:</p>
|
||||
|
||||
<p>A ViewVC URL consists of 3 components: a <a
|
||||
href="#script-component">Script Location</a>, a <a
|
||||
href="#path-component">Repository Path</a>, and some optional <a
|
||||
href="#query-component">Query Parameters</a>. Some examples:</p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@@ -134,22 +147,60 @@ th.caption {
|
||||
</table>
|
||||
|
||||
<h3 id="script-component">ViewVC Script Location</h3>
|
||||
<p>The script location is a common base for all ViewVC URL's pertaining to a particular installation. It's whatever location the web server is configured to serve ViewVC pages from.</p>
|
||||
|
||||
<p>The script location is a common base for all ViewVC URL's
|
||||
pertaining to a particular installation. It's whatever location the
|
||||
web server is configured to serve ViewVC pages from.</p>
|
||||
|
||||
<h3 id="path-component">Repository Path</h3>
|
||||
<p>Since ViewVC is essentially a file system browser for repositories, repository paths referring to the files and directories being browsed get their own section in ViewVC URLs immediately following the script location. Repository paths are always case sensitive and separated by forward slashes, regardless of the underlying filesystem.</p>
|
||||
|
||||
<p>Repository paths can be given certain "magic" prefixes and suffixes. For example, a "/*checkout*" prefix can be added to file views to force a checkout even without an explicit "view=co" query parameter. And a ".tar.gz" suffix is added to download tarball URLs so downloaded tarballs will be saved with sensible default names.</p>
|
||||
<p>Since ViewVC is essentially a file system browser for repositories,
|
||||
repository paths referring to the files and directories being
|
||||
browsed get their own section in ViewVC URLs immediately following
|
||||
the script location. Repository paths are always case sensitive and
|
||||
separated by forward slashes, regardless of the underlying
|
||||
filesystem.</p>
|
||||
|
||||
<p>Repository paths can be given certain "magic" prefixes and
|
||||
suffixes. For example, a "/*checkout*" prefix can be added to file
|
||||
views to force a checkout even without an explicit "view=co" query
|
||||
parameter. And a ".tar.gz" suffix is added to download tarball URLs
|
||||
so downloaded tarballs will be saved with sensible default
|
||||
names.</p>
|
||||
|
||||
<p>If the <code>root_as_url_component</code> configuration option is enabled, the first directory name in a repository path (after any magic prefix) is taken to be the name (from the ViewVC configuration) of the repository to browse rather than the name of an actual directory. The repository name can also be specified in a "root=" query parameter instead (see <a href="#root-param"><code>root</code> parameter</a> below).</p>
|
||||
<p>If the <code>root_as_url_component</code> configuration option is
|
||||
enabled, the first directory name in a repository path (after any
|
||||
magic prefix) is taken to be the name (from the ViewVC
|
||||
configuration) of the repository to browse rather than the name of
|
||||
an actual directory. The repository name can also be specified in a
|
||||
"root=" query parameter instead (see <a
|
||||
href="#root-param"><code>root</code> parameter</a> below).</p>
|
||||
|
||||
<p>Paths beginning with "/*docroot*/" are an exception the rules above. These paths are used to provide access to files in the ViewVC template directory: image files, static HTML pages, and the ViewVC stylesheet. For more information, see the <a href="#docroot-view">Docroot View</a> syntax below.</p>
|
||||
<p>Paths beginning with "/*docroot*/" are an exception the rules
|
||||
above. These paths are used to provide access to files in the
|
||||
ViewVC template directory: image files, static HTML pages, and the
|
||||
ViewVC stylesheet. For more information, see the <a
|
||||
href="#docroot-view">Docroot View</a> syntax below.</p>
|
||||
|
||||
<h3 id="query-component">Query Parameters</h3>
|
||||
|
||||
<p>Following the ViewVC and repository locations in URLs is an optional query string, a list of parameter/value pairs written like "?param1=value1&param2=value2..." Some parameters, like "revision", "pathrev", and "root", augment repository path information, specifying revisions and repositories. Other parameters like "logsort" and "diff_format" control ViewVC display options. Some parameters are also "sticky" and get embedded into links inside the generated pages, sticking around for future page views.</p>
|
||||
<p>Following the ViewVC and repository locations in URLs is an
|
||||
optional query string, a list of parameter/value pairs written like
|
||||
"?param1=value1&param2=value2..." Some parameters, like
|
||||
"revision", "pathrev", and "root", augment repository path
|
||||
information, specifying revisions and repositories. Other
|
||||
parameters like "logsort" and "diff_format" control ViewVC display
|
||||
options. Some parameters are also "sticky" and get embedded into
|
||||
links inside the generated pages, sticking around for future page
|
||||
views.</p>
|
||||
|
||||
<p>ViewVC provides a number of different views of repository data including a directory listing view, a log view, a file diff view, and others. (The views are listed and described in the <code>help_rootview.html</code> ViewVC help page). URLs for each of these views accept specific parameters described in the <a href="#syntax">URL Syntax</a> section, but some parameters are used by multiple views and they are described below:</p>
|
||||
<p>ViewVC provides a number of different views of repository data
|
||||
including a directory listing view, a log view, a file diff view,
|
||||
and others. (The views are listed and described in the
|
||||
<code>help_rootview.html</code> ViewVC help page). URLs for each of
|
||||
these views accept specific parameters described in the <a
|
||||
href="#syntax">URL Syntax</a> section, but some parameters are used
|
||||
by multiple views and they are described below:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
@@ -161,19 +212,34 @@ th.caption {
|
||||
<tbody>
|
||||
<tr>
|
||||
<td id="view-param"><code>view</code></td>
|
||||
<td>The name of the view to display, does not need to be explicitly specified in many URLs. Possible values are shown in the <a href="#url-syntax">URL Syntax</a> section below.</td>
|
||||
<td>The name of the view to display, does not need to be
|
||||
explicitly specified in many URLs. Possible values are shown
|
||||
in the <a href="#url-syntax">URL Syntax</a> section
|
||||
below.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="revision-param"><code>revision</code></td>
|
||||
<td>The revision or tag to display information about, used by several different views.</td>
|
||||
<td>The revision or tag to display information about, used by
|
||||
several different views.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="pathrev-param"><code>pathrev</code></td>
|
||||
<td>The current sticky revision (Subversion) or sticky tag (CVS), as described in the <code>help_rootview.html</code> ViewVC help page. In Subversion, because path information is revision controlled, this value is also used to look up paths in the repository, providing a means of accessing paths that no longer exist in HEAD.</td>
|
||||
<td>The current sticky revision (Subversion) or sticky tag (CVS),
|
||||
as described in the <code>help_rootview.html</code> ViewVC
|
||||
help page. In Subversion, because path information is revision
|
||||
controlled, this value is also used to look up paths in the
|
||||
repository, providing a means of accessing paths that no
|
||||
longer exist in HEAD.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="root-param"><code>root</code></td>
|
||||
<td>The name of the repository to browse. When the <code>default_root</code> configuration option is set or the <code>root_as_url_component</code> option is enabled, it is not neccessary to to specify this parameter. When the <code>root_as_url_component</code> option is enabled, ViewVC URLs with <code>root</code> parameters redirect to locations with the root values embedded in the repository paths.</td>
|
||||
<td>The name of the repository to browse. When the
|
||||
<code>default_root</code> configuration option is set or the
|
||||
<code>root_as_url_component</code> option is enabled, it is
|
||||
not neccessary to to specify this parameter. When the
|
||||
<code>root_as_url_component</code> option is enabled, ViewVC
|
||||
URLs with <code>root</code> parameters redirect to locations
|
||||
with the root values embedded in the repository paths.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -181,7 +247,9 @@ th.caption {
|
||||
|
||||
<div class="h2">
|
||||
<h2 id="syntax">URL Syntax</h2>
|
||||
<p>This section lists URL syntax for each ViewVC view. Parts of URLs which may vary shown as variables in <var>UPPERCASE</var>.</p>
|
||||
|
||||
<p>This section lists URL syntax for each ViewVC view. Parts of URLs
|
||||
which may vary shown as variables in <var>UPPERCASE</var>.</p>
|
||||
|
||||
<h3 id="annotate-view">Annotate View</h3>
|
||||
<table>
|
||||
@@ -220,7 +288,9 @@ th.caption {
|
||||
<tr>
|
||||
<td><code>view=annotate</code></td>
|
||||
<td>depends</td>
|
||||
<td><a href="#view-param"><code>view</code> parameter</a>. Not required when an <code>annotate</code> parameter is present</td>
|
||||
<td><a href="#view-param"><code>view</code> parameter</a>. Not
|
||||
required when an <code>annotate</code> parameter is
|
||||
present</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>annotate=<var>REVISION</var></code></td>
|
||||
@@ -258,7 +328,9 @@ th.caption {
|
||||
<tr>
|
||||
<td><code>/*checkout*</code></td>
|
||||
<td>optional</td>
|
||||
<td>magic prefix. If specified when the <code>checkout_magic</code> configuration option is disabled, ViewVC will redirect to a URL without the prefix.</td>
|
||||
<td>magic prefix. If specified when the
|
||||
<code>checkout_magic</code> configuration option is disabled,
|
||||
ViewVC will redirect to a URL without the prefix.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><var>/PATH</var></code></td>
|
||||
@@ -282,12 +354,18 @@ th.caption {
|
||||
<tr>
|
||||
<td><code>view=co</code></td>
|
||||
<td>depends</td>
|
||||
<td><a href="#view-param"><code>view</code> parameter</a>, not needed if the <code>default_file_view</code> configuration variable is set to <code>co</code>, since that makes the checkout view the default view for file paths. Also not needed if the <code>/*checkout*</code> magic prefix or the <code>revision</code> parameter is present.
|
||||
<td><a href="#view-param"><code>view</code> parameter</a>, not
|
||||
needed if the <code>default_file_view</code> configuration
|
||||
variable is set to <code>co</code>, since that makes the
|
||||
checkout view the default view for file paths. Also not needed
|
||||
if the <code>/*checkout*</code> magic prefix or the
|
||||
<code>revision</code> parameter is present.
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>content-type=<var>TYPE</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>MIME type to send with checked out file, default is a guess based on file extension</td>
|
||||
<td>MIME type to send with checked out file, default is a guess
|
||||
based on file extension</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>revision=<var>REVISION</var></code></td>
|
||||
@@ -347,38 +425,52 @@ th.caption {
|
||||
<tr>
|
||||
<td><code>r1=<var>R1</var></code></td>
|
||||
<td>required</td>
|
||||
<td>starting revision or tag or the string "text" to indicate that <code><var>TR1</var></code> value (below) should override this one</td>
|
||||
<td>starting revision or tag or the string "text" to indicate that
|
||||
<code><var>TR1</var></code> value (below) should override this
|
||||
one</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>r2=<var>R2</var></code></td>
|
||||
<td>required</td>
|
||||
<td>ending revision or tag or the string "text" to indicate that <code><var>TR2</var></code> value (below) should override this one</td>
|
||||
<td>ending revision or tag or the string "text" to indicate that
|
||||
<code><var>TR2</var></code> value (below) should override this
|
||||
one</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tr1=<var>TR1</var></code></td>
|
||||
<td>depends</td>
|
||||
<td>starting revision or tag, used if <var>r1</var> parameter is present and set to "text"</td>
|
||||
<td>starting revision or tag, used if <var>r1</var> parameter is
|
||||
present and set to "text"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tr2=<var>TR2</var></code></td>
|
||||
<td>depends</td>
|
||||
<td>ending revision or tag, used if <var>r2</var> parameter is present and set to "text"</td>
|
||||
<td>ending revision or tag, used if <var>r2</var> parameter is
|
||||
present and set to "text"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>p1=<var>P1</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>starting file path that can override the <code><var>PATH</var></code> value to allow files at two different paths to be compared</td>
|
||||
<td>starting file path that can override the
|
||||
<code><var>PATH</var></code> value to allow files at two different
|
||||
paths to be compared</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>p2=<var>P2</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>ending file path that can override the <code><var>PATH</var></code> value to allow files at two different paths to be compared</td>
|
||||
<td>ending file path that can override the
|
||||
<code><var>PATH</var></code> value to allow files at two different
|
||||
paths to be compared</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>diff_format=<var>DIFF_FORMAT</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>value specifying the type of diff to display. Can be "u" for unified diff, "c" for context diff, "s" for side by side diff, "h" for human readable diff, "l" for long human readable diff. If no value is specified the default depends on the <code>diff_format</code> configuration option.
|
||||
</td>
|
||||
<td>value specifying the type of diff to display. Can be "u" for
|
||||
unified diff, "c" for context diff, "s" for side by side diff, "h"
|
||||
for human readable diff, "l" for long human readable diff, and "f"
|
||||
for a full human readable diff. If no value is specified the
|
||||
default depends on the <code>diff_format</code> configuration
|
||||
option.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pathrev=<var>PATHREV</var></code></td>
|
||||
@@ -410,7 +502,8 @@ th.caption {
|
||||
<tr>
|
||||
<td><code><var>/PATH/</var></code></td>
|
||||
<td>required</td>
|
||||
<td>directory path to view. If the trailing slash is omitted, ViewVC will redirect to a URL that has a trailing slash.</td>
|
||||
<td>directory path to view. If the trailing slash is omitted,
|
||||
ViewVC will redirect to a URL that has a trailing slash.</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br />
|
||||
@@ -434,22 +527,28 @@ th.caption {
|
||||
<tr>
|
||||
<td><code>hideattic=<var>HIDEATTIC</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"0" to show dead files in CVS directory listings or "1" to hide dead files. Default depends on the <code>hide_attic</code> configuration value.</td>
|
||||
<td>"0" to show dead files in CVS directory listings or "1" to
|
||||
hide dead files. Default depends on the <code>hide_attic</code>
|
||||
configuration value.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>search=<var>SEARCH</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>regular expression to search files in the directory with if <code>use_re_search</code> configuration option is enabled</td>
|
||||
<td>regular expression to search files in the directory with if
|
||||
<code>use_re_search</code> configuration option is enabled</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sortby=<var>SORTBY</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"file" "rev" "date" "author" or "log" to indicate how the directory listing should be sorted. Default depends on <code>sortby</code> configuration option.</td>
|
||||
<td>"file" "rev" "date" "author" or "log" to indicate how the
|
||||
directory listing should be sorted. Default depends on
|
||||
<code>sortby</code> configuration option.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sortdir=<var>SORTBY</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"up" to sort directory in ascending order or "down" for descending order. Default is "up".</td>
|
||||
<td>"up" to sort directory in ascending order or "down" for
|
||||
descending order. Default is "up".</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>dir_pagestart=<var>PAGE</var></code></td>
|
||||
@@ -491,7 +590,10 @@ th.caption {
|
||||
<tr>
|
||||
<td><code><var>/PATH</var></code></td>
|
||||
<td>required</td>
|
||||
<td>file path to retrieve. ViewVC will return the contents of the file located at <code>PATH</code>, relative to the <code>docroot</code> subdirectory of the directory specified in the <code>template_dir</code> configuration option.</td>
|
||||
<td>file path to retrieve. ViewVC will return the contents of the
|
||||
file located at <code>PATH</code>, relative to the
|
||||
<code>docroot</code> subdirectory of the directory specified in
|
||||
the <code>template_dir</code> configuration option.</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br />
|
||||
@@ -637,12 +739,20 @@ th.caption {
|
||||
<tr>
|
||||
<td><code>view=log</code></td>
|
||||
<td>depends</td>
|
||||
<td><a href="#view-param"><code>view</code> parameter</a>, does not need to be specified for file paths when the <code>default_file_view</code> configuration option is set to <code>log</code>. However it is recommended that the parameter be passed anyway for consistency with directory log URLs and compatibility with ViewVC installations that set <code>default_file_view</code> to <code>co</code>.</td>
|
||||
<td><a href="#view-param"><code>view</code> parameter</a>, does
|
||||
not need to be specified for file paths when the
|
||||
<code>default_file_view</code> configuration option is set to
|
||||
<code>log</code>. However it is recommended that the parameter be
|
||||
passed anyway for consistency with directory log URLs and
|
||||
compatibility with ViewVC installations that set
|
||||
<code>default_file_view</code> to <code>co</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logsort=<var>SORT</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"rev" to sort log entries by revision number or "date" to sort by date. Default depends on the <code>log_sort</code> configuration value.</td>
|
||||
<td>"rev" to sort log entries by revision number or "date" to sort
|
||||
by date. Default depends on the <code>log_sort</code>
|
||||
configuration value.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>log_pagestart=<var>PAGE</var></code></td>
|
||||
@@ -767,37 +877,50 @@ th.caption {
|
||||
<tr>
|
||||
<td><code>r1=<var>R1</var></code></td>
|
||||
<td>required</td>
|
||||
<td>starting revision or tag or the string "text" to indicate that <code><var>TR1</var></code> value (below) should override this one</td>
|
||||
<td>starting revision or tag or the string "text" to indicate that
|
||||
<code><var>TR1</var></code> value (below) should override this
|
||||
one</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>r2=<var>R2</var></code></td>
|
||||
<td>required</td>
|
||||
<td>ending revision or tag or the string "text" to indicate that <code><var>TR2</var></code> value (below) should override this one</td>
|
||||
<td>ending revision or tag or the string "text" to indicate that
|
||||
<code><var>TR2</var></code> value (below) should override this
|
||||
one</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tr1=<var>TR1</var></code></td>
|
||||
<td>depends</td>
|
||||
<td>starting revision or tag, only used if <var>r1</var> parameter is present and set to "text"</td>
|
||||
<td>starting revision or tag, only used if <var>r1</var> parameter
|
||||
is present and set to "text"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tr2=<var>TR2</var></code></td>
|
||||
<td>depends</td>
|
||||
<td>ending revision or tag, only used if <var>r2</var> parameter is present and set to "text"</td>
|
||||
<td>ending revision or tag, only used if <var>r2</var> parameter
|
||||
is present and set to "text"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>p1=<var>P1</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>starting file path that can override the <code><var>PATH</var></code> value to allow files at two different paths to be compared</td>
|
||||
<td>starting file path that can override the
|
||||
<code><var>PATH</var></code> value to allow files at two different
|
||||
paths to be compared</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>p2=<var>P2</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>ending file path that can override the <code><var>PATH</var></code> value to allow files at two different paths to be compared</td>
|
||||
<td>ending file path that can override the
|
||||
<code><var>PATH</var></code> value to allow files at two different
|
||||
paths to be compared</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>diff_format=<var>DIFF_FORMAT</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>value specifying the type of patch to display. Can be "u" for unified diff or "c" for context diff. If no value is specified the default depends on the <code>diff_format</code> configuration option.
|
||||
<td>value specifying the type of patch to display. Can be "u" for
|
||||
unified diff or "c" for context diff. If no value is specified the
|
||||
default depends on the <code>diff_format</code> configuration
|
||||
option.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -859,7 +982,8 @@ th.caption {
|
||||
<tr>
|
||||
<td><code>branch_match=BRANCH_MATCH</code></td>
|
||||
<td>optional</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type of branch match</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type
|
||||
of branch match</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>dir=<var>DIR</var></code></td>
|
||||
@@ -872,9 +996,10 @@ th.caption {
|
||||
<td>file query string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_match=FILE_MATCH</code></td>
|
||||
<td><code>file_match=<var>FILE_MATCH</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type of file match</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type
|
||||
of file match</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>who=<var>WHO</var></code></td>
|
||||
@@ -882,39 +1007,57 @@ th.caption {
|
||||
<td>author query string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>who_match=WHO_MATCH</code></td>
|
||||
<td><code>who_match=<var>WHO_MATCH</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type of author match</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type
|
||||
of author match</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>querysort=SORT</code></td>
|
||||
<td><code>comment=<var>COMMENT</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>log message query string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>comment_match=<var>COMMENT_MATCH</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type
|
||||
of log message match</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>querysort=<var>SORT</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"date" "author" or "file" determining order of query results</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>date=DATE</code></td>
|
||||
<td><code>date=<var>DATE</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"hours" "day" "week" "month" "all" or "explicit" to filter query results by date</td>
|
||||
<td>"hours" "day" "week" "month" "all" or "explicit" to filter
|
||||
query results by date</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>hours=HOURS</code></td>
|
||||
<td><code>hours=<var>HOURS</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>number of hours back to include results from when <code><var>DATE</var></code> is "hours"</td>
|
||||
<td>number of hours back to include results from when
|
||||
<code><var>DATE</var></code> is "hours"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>mindate=MINDATE</code></td>
|
||||
<td><code>mindate=<var>MINDATE</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>earliest date to include results from when <code><var>DATE</var></code> is "explicit"</td>
|
||||
<td>earliest date to include results from when
|
||||
<code><var>DATE</var></code> is "explicit"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>maxdate=MAXDATE</code></td>
|
||||
<td><code>maxdate=<var>MAXDATE</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>latest date to include results from when <code><var>DATE</var></code> is "explicit"</td>
|
||||
<td>latest date to include results from when
|
||||
<code><var>DATE</var></code> is "explicit"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit_changes=LIMIT_CHANGES</code></td>
|
||||
<td><code>limit_changes=<var>LIMIT_CHANGES</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>maximum number of files to list per commit in query results. Default is value of <code>limit_changes</code> configuration option</td>
|
||||
<td>maximum number of files to list per commit in query
|
||||
results. Default is value of <code>limit_changes</code>
|
||||
configuration option</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>root=<var>ROOT</var></code></td>
|
||||
@@ -970,9 +1113,10 @@ th.caption {
|
||||
<td>branch query string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>branch_match=BRANCH_MATCH</code></td>
|
||||
<td><code>branch_match=<var>BRANCH_MATCH</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type of branch match</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type
|
||||
of branch match</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>dir=<var>DIR</var></code></td>
|
||||
@@ -985,9 +1129,10 @@ th.caption {
|
||||
<td>file query string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_match=FILE_MATCH</code></td>
|
||||
<td><code>file_match=<var>FILE_MATCH</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type of file match</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type
|
||||
of file match</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>who=<var>WHO</var></code></td>
|
||||
@@ -995,49 +1140,64 @@ th.caption {
|
||||
<td>author query string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>who_match=WHO_MATCH</code></td>
|
||||
<td><code>who_match=<var>WHO_MATCH</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type of author match</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type
|
||||
of author match</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>querysort=SORT</code></td>
|
||||
<td><code>comment=<var>COMMENT</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>log message query string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>comment_match=<var>COMMENT_MATCH</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"exact" "like" "glob" "regex" or "notregex" determining type
|
||||
of log message match</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>querysort=<var>SORT</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"date" "author" or "file" determining order of query results</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>date=DATE</code></td>
|
||||
<td><code>date=<var>DATE</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"hours" "day" "week" "month" "all" or "explicit" to filter query results by date</td>
|
||||
<td>"hours" "day" "week" "month" "all" or "explicit" to filter
|
||||
query results by date</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>hours=HOURS</code></td>
|
||||
<td><code>hours=<var>HOURS</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>number of hours back to include results from when <code><var>DATE</var></code> is "hours"</td>
|
||||
<td>number of hours back to include results from when
|
||||
<code><var>DATE</var></code> is "hours"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>mindate=MINDATE</code></td>
|
||||
<td><code>mindate=<var>MINDATE</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>earliest date to include results from when <code><var>DATE</var></code> is "explicit"</td>
|
||||
<td>earliest date to include results from when
|
||||
<code><var>DATE</var></code> is "explicit"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>maxdate=MAXDATE</code></td>
|
||||
<td><code>maxdate=<var>MAXDATE</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>latest date to include results from when <code><var>DATE</var></code> is "explicit"</td>
|
||||
<td>latest date to include results from when
|
||||
<code><var>DATE</var></code> is "explicit"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>format=FORMAT</code></td>
|
||||
<td><code>format=<var>FORMAT</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>"rss" or "backout" values to generate an rss feed or list of commands to back out changes instead showing a normal query result page</td>
|
||||
<td>"rss" or "backout" values to generate an rss feed or list of
|
||||
commands to back out changes instead showing a normal query result
|
||||
page</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit=LIMIT</code></td>
|
||||
<td><code>limit_changes=<var>LIMIT_CHANGES</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>maximum number of file-revisions to process during a query. Default is value of <code>row_limit</code> configuration option</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit_changes=LIMIT_CHANGES</code></td>
|
||||
<td>optional</td>
|
||||
<td>maximum number of files to list per commit in query results. Default is value of <code>limit_changes</code> configuration option</td>
|
||||
<td>maximum number of files to list per commit in query
|
||||
results. Default is value of <code>limit_changes</code>
|
||||
configuration option</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>root=<var>ROOT</var></code></td>
|
||||
@@ -1077,7 +1237,7 @@ th.caption {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>view=rev</code></td>
|
||||
<td><code>view=revision</code></td>
|
||||
<td>required</td>
|
||||
<td><a href="#view-param"><code>view</code> parameter</a></td>
|
||||
</tr>
|
||||
@@ -1087,9 +1247,10 @@ th.caption {
|
||||
<td><a href="#revision-param"><code>revision</code> parameter</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit_changes=LIMIT_CHANGES</code></td>
|
||||
<td><code>limit_changes=<var>LIMIT_CHANGES</var></code></td>
|
||||
<td>optional</td>
|
||||
<td>maximum number of files to list per commit. Default is value of <code>limit_changes</code> configuration option</td>
|
||||
<td>maximum number of files to list per commit. Default is value
|
||||
of <code>limit_changes</code> configuration option</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>root=<var>ROOT</var></code></td>
|
||||
@@ -1130,7 +1291,10 @@ th.caption {
|
||||
<tr>
|
||||
<td><code>view=roots</code></td>
|
||||
<td>depends</td>
|
||||
<td><a href="#view-param"><code>view</code> parameter</a>. Not required if the <code>root_as_url_component</code> configuration is enabled or the <code>default_root</code> option is not set. </td>
|
||||
<td><a href="#view-param"><code>view</code> parameter</a>. Not
|
||||
required if the <code>root_as_url_component</code> configuration
|
||||
is enabled or the <code>default_root</code> option is not
|
||||
set. </td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -1157,7 +1321,11 @@ th.caption {
|
||||
<tr>
|
||||
<td><code>.tar.gz</code></td>
|
||||
<td>depends</td>
|
||||
<td>magic suffix. Only required when the name of the directory being downloaded ends in ".tar.gz" and the <code>parent</code> parameter not is present. But it is recommended to add the magic suffix to all tarball URLs to avoid this special case and give the downloaded files sensible default names.</td>
|
||||
<td>magic suffix. Only required when the name of the directory
|
||||
being downloaded ends in ".tar.gz" and the <code>parent</code>
|
||||
parameter not is present. But it is recommended to add the magic
|
||||
suffix to all tarball URLs to avoid this special case and give the
|
||||
downloaded files sensible default names.</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br />
|
||||
@@ -1181,7 +1349,12 @@ th.caption {
|
||||
<tr>
|
||||
<td><code>parent=1</code></td>
|
||||
<td>optional</td>
|
||||
<td>If the <code>parent</code> parameter is specified, the last component of the <code><var>PATH</var></code> is discarded before it is ever looked up in the repository. This feature is used when the <code>root_as_url_component</code> configuration option is disabled to allow root tarball URLs to be saved with names like "ROOT-root.tar.gz".</td>
|
||||
<td>If the <code>parent</code> parameter is specified, the last
|
||||
component of the <code><var>PATH</var></code> is discarded before
|
||||
it is ever looked up in the repository. This feature is used when
|
||||
the <code>root_as_url_component</code> configuration option is
|
||||
disabled to allow root tarball URLs to be saved with names like
|
||||
"ROOT-root.tar.gz".</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pathrev=<var>PATHREV</var></code></td>
|
||||
@@ -1198,46 +1371,111 @@ th.caption {
|
||||
|
||||
<div class="h2">
|
||||
<h2 id="compat">Backwards Compatibility</h2>
|
||||
<p>ViewVC's URL format has changed a lot over time, but ViewVC goes out of its way to support URLs using older formats so there aren't broken links when an installation of ViewVC is upgraded. The support is implemented as a set of URL transformations that recognize elements of old-style URLs and convert them to newer equivalents. If any transformations are applied (with some exceptions, mentioned below), ViewVC will issue a single redirect to the transformed URL. Descriptions of the transformations follow.</p>
|
||||
|
||||
<p>ViewVC's URL format has changed a lot over time, but ViewVC goes
|
||||
out of its way to support URLs using older formats so there aren't
|
||||
broken links when an installation of ViewVC is upgraded. The
|
||||
support is implemented as a set of URL transformations that
|
||||
recognize elements of old-style URLs and convert them to newer
|
||||
equivalents. If any transformations are applied (with some
|
||||
exceptions, mentioned below), ViewVC will issue a single redirect
|
||||
to the transformed URL. Descriptions of the transformations
|
||||
follow.</p>
|
||||
|
||||
<h3 id="compat-viewrev">'<code>view=rev</code>' Parameter ⇒ '<code>view=revision</code>'</h3>
|
||||
|
||||
<p>URLs with a <code>view=rev</code> parameter will automatically be
|
||||
redirected to URLs with a <code>view=revision</code> parameter
|
||||
instead.</p>
|
||||
|
||||
<h3 id="compat-cvsroot">'<code>cvsroot</code>' Parameter ⇒ '<code>root</code>'</h3>
|
||||
<p>URLs with a <code>cvsroot</code> parameter will automatically be redirected to URLs with a <code>root</code> parameter instead.</p>
|
||||
|
||||
<p>URLs with a <code>cvsroot</code> parameter will automatically be
|
||||
redirected to URLs with a <code>root</code> parameter instead.</p>
|
||||
|
||||
<h3 id="compat-only_with_tag">'<code>only_with_tag</code>' Parameter ⇒ '<code>pathrev</code>'</h3>
|
||||
<p>URLs with an <code>only_with_tag</code> parameter will automatically be redirected to URLs with a <code>pathrev</code> parameter instead.</p>
|
||||
|
||||
<p>URLs with an <code>only_with_tag</code> parameter will
|
||||
automatically be redirected to URLs with a <code>pathrev</code>
|
||||
parameter instead.</p>
|
||||
|
||||
<h3 id="compat-oldcheckout">'<code>~checkout~</code>' Magic Path Prefix ⇒ '<code>*checkout*</code>'</h3>
|
||||
<p>URLs with a <code>~checkout~</code> path prefix get interpreted just like URLs with a '*checkout*' prefix. There is currently no redirect, but there could be in the future.</p>
|
||||
|
||||
<p>URLs with a <code>~checkout~</code> path prefix get interpreted
|
||||
just like URLs with a '*checkout*' prefix. There is currently no
|
||||
redirect, but there could be in the future.</p>
|
||||
|
||||
<h3 id="compat-checkout">'<code>*checkout*</code>' Magic Path Prefix ⇒ '<code>view=co</code>'</h3>
|
||||
<p>When the <code>checkout_magic</code> configuration option is disabled, URLs with a <code>*checkout*</code> magic prefix will redirect to an equivalent URL that does not use the prefix.</p>
|
||||
|
||||
<p>When the <code>checkout_magic</code> configuration option is
|
||||
disabled, URLs with a <code>*checkout*</code> magic prefix will
|
||||
redirect to an equivalent URL that does not use the prefix.</p>
|
||||
|
||||
<h3 id="compat-root">'<code>root</code>' Parameter ⇒ Root Path Component</h3>
|
||||
<p>When the <code>root_as_url_component</code> configuration option is enabled, URLs with a <code>root</code> parameter will redirect to an equivalent URL with the root name embedded in the path.</p>
|
||||
|
||||
<p>When the <code>root_as_url_component</code> configuration option is
|
||||
enabled, URLs with a <code>root</code> parameter will redirect to
|
||||
an equivalent URL with the root name embedded in the path.</p>
|
||||
|
||||
<h3 id="compat-rev">'<code>rev</code>' Parameter ⇒ '<code>revision</code>' and '<code>pathrev</code>'</h3>
|
||||
<p>CVS URLs with a <code>rev</code> parameter will redirect to URLs with a <code>revision</code> parameter instead. Subversion URLs with a <code>rev</code> parameter will redirect to URLs with a <code>pathrev</code> parameter, in order to account for the how the Subversion backend used to look up paths before <code>pathrev</code> was introduced.</p>
|
||||
|
||||
<p>CVS URLs with a <code>rev</code> parameter will redirect to URLs
|
||||
with a <code>revision</code> parameter instead. Subversion URLs
|
||||
with a <code>rev</code> parameter will redirect to URLs with a
|
||||
<code>pathrev</code> parameter, in order to account for the how the
|
||||
Subversion backend used to look up paths before
|
||||
<code>pathrev</code> was introduced.</p>
|
||||
|
||||
<h3 id="compat-diff">'<code>.diff</code>' Suffix ⇒ Diff View</h3>
|
||||
<p>When ViewVC encounters a invalid repository path that ends in <code>.diff</code>, and stripping that ending yields a valid file path, it will redirect to a diff view of the file.</p>
|
||||
|
||||
<p>When ViewVC encounters a invalid repository path that ends in
|
||||
<code>.diff</code>, and stripping that ending yields a valid file
|
||||
path, it will redirect to a diff view of the file.</p>
|
||||
|
||||
<h3 id="compat-tgz">'<code>.tar.gz</code>' Suffix ⇒ '<code>view=tar</code>'</h3>
|
||||
<p>When ViewVC encounters a invalid repository path that ends in <code>.tar.gz</code>, <code>/root.tar.gz</code>, or <code>/REPOS-root.tar.gz</code>, and stripping the ending yields a valid directory path, it will redirect to a URL to download a tarball of the directory.</p>
|
||||
|
||||
<p>When ViewVC encounters a invalid repository path that ends in
|
||||
<code>.tar.gz</code>, <code>/root.tar.gz</code>, or
|
||||
<code>/REPOS-root.tar.gz</code>, and stripping the ending yields a
|
||||
valid directory path, it will redirect to a URL to download a
|
||||
tarball of the directory.</p>
|
||||
|
||||
<h3 id="compat-tarball">'<code>tarball=1</code>' Parameter ⇒ '<code>view=tar</code>'</h3>
|
||||
<p>A <code>tarball=1</code> parameter is treated pretty much like a <code>view=tar</code> parameter. There is no redirect when it is encountered, but there could be in the future.</p>
|
||||
|
||||
<p>A <code>tarball=1</code> parameter is treated pretty much like a
|
||||
<code>view=tar</code> parameter. There is no redirect when it is
|
||||
encountered, but there could be in the future.</p>
|
||||
|
||||
<h3 id="compat-graph">'<code>graph=1</code>' Parameter ⇒ '<code>view=graph</code>'</h3>
|
||||
<p>A <code>graph=1</code> parameter is treated like a <code>view=graph</code> parameter. There is currently no redirect when it is encountered, but there could be one in the future.</p>
|
||||
|
||||
<p>A <code>graph=1</code> parameter is treated like a
|
||||
<code>view=graph</code> parameter. There is currently no redirect
|
||||
when it is encountered, but there could be one in the future.</p>
|
||||
|
||||
<h3 id="compat-makeimage">'<code>graph=1&makeimage=1</code>' Parameters ⇒ '<code>view=graphimg</code>'</h3>
|
||||
<p>A <code>graph=1&makeimage=1</code> parameter is treated like a <code>view=graph</code> parameter. There is currently no redirect when it is encountered, but there could be one in the future.</p>
|
||||
|
||||
<p>A <code>graph=1&makeimage=1</code> parameter is treated like a
|
||||
<code>view=graph</code> parameter. There is currently no redirect
|
||||
when it is encountered, but there could be one in the future.</p>
|
||||
|
||||
<h3 id="compat-content_type">'<code>content_type=text/vnd.viewcvs-markup</code>' and '<code>content_type=text/x-cvsweb-markup</code>' Parameters⇒ '<code>view=markup</code>'</h3>
|
||||
<p><code>content_type=text/vnd.viewcvs-markup</code> and <code>content_type=text/x-cvsweb-markup</code> parameters are treated like a <code>view=markup</code> parameter. There is currently no redirect when it is encountered, but there could be one in the future.</p>
|
||||
|
||||
<p><code>content-type=text/vnd.viewcvs-markup</code> and
|
||||
<code>content-type=text/x-cvsweb-markup</code> parameters are
|
||||
treated like a <code>view=markup</code> parameter. There is
|
||||
currently no redirect when it is encountered, but there could be
|
||||
one in the future. Other values of the <code>content-type</code>
|
||||
parameter, which were used to dictate the MIME type of files
|
||||
displayed in the checkout/download view prior to ViewVC 1.0.6, are
|
||||
ignored.</p>
|
||||
|
||||
<h3 id="compat-attic">'<code>Attic/FILE</code>' Paths ⇒ '<code>FILE</code>'</h3>
|
||||
<p>When ViewVC encounters an invalid repository path whose last or second-to-last component is named <code>Attic</code>, and stripping the component yields a valid path, it will redirect to a URL with that path.</p>
|
||||
|
||||
<p>When ViewVC encounters an invalid repository path whose last or
|
||||
second-to-last component is named <code>Attic</code>, and stripping
|
||||
the component yields a valid path, it will redirect to a URL with
|
||||
that path.</p>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
elx-java
|
||||
j_keywords.c
|
||||
j_keywords.h
|
||||
j_scan.c
|
||||
j_scan.h
|
||||
java.c
|
||||
java.h
|
||||
*.output
|
||||
@@ -1,6 +0,0 @@
|
||||
elx-python
|
||||
py_keywords.c
|
||||
py_keywords.h
|
||||
python.c
|
||||
python.h
|
||||
*.output
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -39,7 +39,7 @@ def _parse(hdr, result):
|
||||
while pos < len(hdr):
|
||||
name = _re_token.match(hdr, pos)
|
||||
if not name:
|
||||
raise AcceptParseError()
|
||||
raise AcceptLanguageParseError()
|
||||
a = result.item_class(string.lower(name.group(1)))
|
||||
pos = name.end()
|
||||
while 1:
|
||||
@@ -210,7 +210,7 @@ class _LanguageSelector:
|
||||
def append(self, item):
|
||||
self.requested.append(item)
|
||||
|
||||
class AcceptParseError(Exception):
|
||||
class AcceptLanguageParseError(Exception):
|
||||
pass
|
||||
|
||||
def _test():
|
||||
|
||||
69
lib/blame.py
69
lib/blame.py
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 2000 Curt Hagenlocher <curt@hagenlocher.org>
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
@@ -26,15 +26,14 @@
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import sys
|
||||
import string
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import math
|
||||
import cgi
|
||||
import vclib
|
||||
import vclib.ccvs.blame
|
||||
|
||||
import sapi
|
||||
|
||||
re_includes = re.compile('\\#(\\s*)include(\\s*)"(.*?)"')
|
||||
|
||||
@@ -75,14 +74,15 @@ class HTMLBlameSource:
|
||||
self.path_parts = path_parts
|
||||
self.diff_url = diff_url
|
||||
self.include_url = include_url
|
||||
self.annotation, self.revision = self.repos.annotate(path_parts, opt_rev)
|
||||
self.annotation, self.revision = self.repos.annotate(path_parts, opt_rev,
|
||||
True)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
item = self.annotation.__getitem__(idx)
|
||||
diff_url = None
|
||||
if item.prev_rev:
|
||||
diff_url = '%sr1=%s&r2=%s' % (self.diff_url, item.prev_rev, item.rev)
|
||||
thisline = link_includes(cgi.escape(item.text), self.repos,
|
||||
thisline = link_includes(sapi.escape(item.text), self.repos,
|
||||
self.path_parts, self.include_url)
|
||||
return _item(text=thisline, line_number=item.line_number,
|
||||
rev=item.rev, prev_rev=item.prev_rev,
|
||||
@@ -100,73 +100,44 @@ class _item:
|
||||
|
||||
|
||||
def make_html(root, rcs_path):
|
||||
import vclib.ccvs.blame
|
||||
bs = vclib.ccvs.blame.BlameSource(os.path.join(root, rcs_path))
|
||||
|
||||
count = bs.num_lines
|
||||
if count == 0:
|
||||
count = 1
|
||||
|
||||
line_num_width = int(math.log10(count)) + 1
|
||||
revision_width = 3
|
||||
author_width = 5
|
||||
line = 0
|
||||
old_revision = 0
|
||||
row_color = ''
|
||||
inMark = 0
|
||||
row_color = 'ffffff'
|
||||
rev_count = 0
|
||||
|
||||
open_table_tag = '<table cellpadding="0" cellspacing="0">'
|
||||
startOfRow = '<tr><td colspan="3"%s><pre>'
|
||||
endOfRow = '</td></tr>'
|
||||
|
||||
print open_table_tag + (startOfRow % '')
|
||||
align = ' style="text-align: %s;"'
|
||||
|
||||
sys.stdout.write('<table cellpadding="2" cellspacing="2" style="font-family: monospace; whitespace: pre;">\n')
|
||||
for line_data in bs:
|
||||
revision = line_data.rev
|
||||
thisline = line_data.text
|
||||
line = line_data.line_number
|
||||
author = line_data.author
|
||||
prev_rev = line_data.prev_rev
|
||||
|
||||
output = ''
|
||||
|
||||
if old_revision != revision and line != 1:
|
||||
if row_color == '':
|
||||
row_color = ' style="background-color:#e7e7e7"'
|
||||
if row_color == 'ffffff':
|
||||
row_color = 'e7e7e7'
|
||||
else:
|
||||
row_color = ''
|
||||
row_color = 'ffffff'
|
||||
|
||||
if not inMark:
|
||||
output = output + endOfRow + (startOfRow % row_color)
|
||||
|
||||
output = output + '<a name="%d">%*d</a>' % (line, line_num_width, line)
|
||||
sys.stdout.write('<tr id="l%d" style="background-color: #%s; vertical-align: center;">' % (line, row_color))
|
||||
sys.stdout.write('<td%s>%d</td>' % (align % 'right', line))
|
||||
|
||||
if old_revision != revision or rev_count > 20:
|
||||
revision_width = max(revision_width, len(revision))
|
||||
output = output + ' '
|
||||
author_width = max(author_width, len(author))
|
||||
output = output + ('%-*s ' % (author_width, author))
|
||||
output = output + revision
|
||||
if prev_rev:
|
||||
output = output + '</a>'
|
||||
output = output + (' ' * (revision_width - len(revision) + 1))
|
||||
|
||||
sys.stdout.write('<td%s>%s</td>' % (align % 'right', author or ' '))
|
||||
sys.stdout.write('<td%s>%s</td>' % (align % 'left', revision))
|
||||
old_revision = revision
|
||||
rev_count = 0
|
||||
else:
|
||||
output = output + ' ' + (' ' * (author_width + revision_width))
|
||||
sys.stdout.write('<td> </td><td> </td>')
|
||||
rev_count = rev_count + 1
|
||||
|
||||
output = output + thisline
|
||||
|
||||
# Close the highlighted section
|
||||
#if (defined $mark_cmd and mark_cmd != 'begin'):
|
||||
# chop($output)
|
||||
# output = output + endOfRow + (startOfRow % row_color)
|
||||
# inMark = 0
|
||||
|
||||
print output
|
||||
print endOfRow + '</table>'
|
||||
sys.stdout.write('<td%s>%s</td></tr>\n' % (align % 'left', string.rstrip(thisline) or ' '))
|
||||
sys.stdout.write('</table>\n')
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -125,16 +125,30 @@ except AttributeError:
|
||||
try:
|
||||
mkdtemp = tempfile.mkdtemp
|
||||
except AttributeError:
|
||||
def mkdtemp():
|
||||
for i in range(10):
|
||||
dir = tempfile.mktemp()
|
||||
try:
|
||||
os.mkdir(dir, 0700)
|
||||
return dir
|
||||
except OSError, e:
|
||||
if e.errno == errno.EEXIST:
|
||||
continue # try again
|
||||
raise
|
||||
def mkdtemp(suffix="", prefix="tmp", dir=None):
|
||||
# mktemp() only took a single suffix argument until Python 2.3.
|
||||
# We'll do the best we can.
|
||||
oldtmpdir = os.environ.get('TMPDIR')
|
||||
try:
|
||||
for i in range(10):
|
||||
if dir:
|
||||
os.environ['TMPDIR'] = dir
|
||||
dir = tempfile.mktemp(suffix)
|
||||
if prefix:
|
||||
parent, base = os.path.split(dir)
|
||||
dir = os.path.join(parent, prefix + base)
|
||||
try:
|
||||
os.mkdir(dir, 0700)
|
||||
return dir
|
||||
except OSError, e:
|
||||
if e.errno == errno.EEXIST:
|
||||
continue # try again
|
||||
raise
|
||||
finally:
|
||||
if oldtmpdir:
|
||||
os.environ['TMPDIR'] = oldtmpdir
|
||||
elif os.environ.has_key('TMPDIR'):
|
||||
del(os.environ['TMPDIR'])
|
||||
|
||||
raise IOError, (errno.EEXIST, "No usable temporary directory name found")
|
||||
|
||||
|
||||
492
lib/config.py
492
lib/config.py
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -19,51 +19,154 @@ import os
|
||||
import string
|
||||
import ConfigParser
|
||||
import fnmatch
|
||||
import re
|
||||
import vclib
|
||||
|
||||
|
||||
#########################################################################
|
||||
#
|
||||
# CONFIGURATION
|
||||
# -------------
|
||||
#
|
||||
# There are three forms of configuration:
|
||||
#
|
||||
# 1) edit the viewvc.conf created by the viewvc-install(er)
|
||||
# 2) as (1), but delete all unchanged entries from viewvc.conf
|
||||
# 3) do not use viewvc.conf and just edit the defaults in this file
|
||||
# 1. edit the viewvc.conf created by the viewvc-install(er)
|
||||
# 2. as (1), but delete all unchanged entries from viewvc.conf
|
||||
# 3. do not use viewvc.conf and just edit the defaults in this file
|
||||
#
|
||||
# Most users will want to use (1), but there are slight speed advantages
|
||||
# to the other two options. Note that viewvc.conf values are a bit easier
|
||||
# to work with since it is raw text, rather than python literal values.
|
||||
#
|
||||
#
|
||||
# A WORD ABOUT OPTION LAYERING/OVERRIDES
|
||||
# --------------------------------------
|
||||
#
|
||||
# ViewVC has three "layers" of configuration options:
|
||||
#
|
||||
# 1. base configuration options - very basic configuration bits
|
||||
# found in sections like 'general', 'options', etc.
|
||||
# 2. vhost overrides - these options overlay/override the base
|
||||
# configuration on a per-vhost basis.
|
||||
# 3. root overrides - these options overlay/override the base
|
||||
# configuration and vhost overrides on a per-root basis.
|
||||
#
|
||||
# Here's a diagram of the valid overlays/overrides:
|
||||
#
|
||||
# PER-ROOT PER-VHOST BASE
|
||||
#
|
||||
# ,-----------. ,-----------.
|
||||
# | vhost-*/ | | |
|
||||
# | general | --> | general |
|
||||
# | | | |
|
||||
# `-----------' `-----------'
|
||||
# ,-----------. ,-----------. ,-----------.
|
||||
# | root-*/ | | vhost-*/ | | |
|
||||
# | options | --> | options | --> | options |
|
||||
# | | | | | |
|
||||
# `-----------' `-----------' `-----------'
|
||||
# ,-----------. ,-----------. ,-----------.
|
||||
# | root-*/ | | vhost-*/ | | |
|
||||
# | templates | --> | templates | --> | templates |
|
||||
# | | | | | |
|
||||
# `-----------' `-----------' `-----------'
|
||||
# ,-----------. ,-----------. ,-----------.
|
||||
# | root-*/ | | vhost-*/ | | |
|
||||
# | utilities | --> | utilities | --> | utilities |
|
||||
# | | | | | |
|
||||
# `-----------' `-----------' `-----------'
|
||||
# ,-----------. ,-----------.
|
||||
# | vhost-*/ | | |
|
||||
# | cvsdb | --> | cvsdb |
|
||||
# | | | |
|
||||
# `-----------' `-----------'
|
||||
# ,-----------. ,-----------. ,-----------.
|
||||
# | root-*/ | | vhost-*/ | | |
|
||||
# | authz-* | --> | authz-* | --> | authz-* |
|
||||
# | | | | | |
|
||||
# `-----------' `-----------' `-----------'
|
||||
# ,-----------.
|
||||
# | |
|
||||
# | vhosts |
|
||||
# | |
|
||||
# `-----------'
|
||||
# ,-----------.
|
||||
# | |
|
||||
# | query |
|
||||
# | |
|
||||
# `-----------'
|
||||
#
|
||||
# ### TODO: Figure out what this all means for the 'kv' stuff.
|
||||
#
|
||||
#########################################################################
|
||||
|
||||
class Config:
|
||||
_sections = ('general', 'options', 'cvsdb', 'templates')
|
||||
_force_multi_value = ('cvs_roots', 'forbidden', 'forbiddenre',
|
||||
'svn_roots', 'languages', 'kv_files',
|
||||
'root_parents')
|
||||
_base_sections = (
|
||||
# Base configuration sections.
|
||||
'authz-*',
|
||||
'cvsdb',
|
||||
'general',
|
||||
'options',
|
||||
'query',
|
||||
'templates',
|
||||
'utilities',
|
||||
)
|
||||
_force_multi_value = (
|
||||
# Configuration values with multiple, comma-separated values.
|
||||
'allowed_views',
|
||||
'custom_log_formatting',
|
||||
'cvs_roots',
|
||||
'kv_files',
|
||||
'languages',
|
||||
'mime_types_files',
|
||||
'root_parents',
|
||||
'svn_roots',
|
||||
)
|
||||
_allowed_overrides = {
|
||||
# Mapping of override types to allowed overridable sections.
|
||||
'vhost' : ('authz-*',
|
||||
'cvsdb',
|
||||
'general',
|
||||
'options',
|
||||
'templates',
|
||||
'utilities',
|
||||
),
|
||||
'root' : ('authz-*',
|
||||
'options',
|
||||
'templates',
|
||||
'utilities',
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
for section in self._sections:
|
||||
self.root_options_overlayed = 0
|
||||
for section in self._base_sections:
|
||||
if section[-1] == '*':
|
||||
continue
|
||||
setattr(self, section, _sub_config())
|
||||
|
||||
def load_config(self, pathname, vhost=None):
|
||||
"""Load the configuration file at PATHNAME, applying configuration
|
||||
settings there as overrides to the built-in default values. If
|
||||
VHOST is provided, also process the configuration overrides
|
||||
specific to that virtual host."""
|
||||
|
||||
self.conf_path = os.path.isfile(pathname) and pathname or None
|
||||
self.base = os.path.dirname(pathname)
|
||||
self.parser = ConfigParser.ConfigParser()
|
||||
self.parser.optionxform = lambda x: x # don't case-normalize option names.
|
||||
self.parser.read(self.conf_path or [])
|
||||
|
||||
for section in self.parser.sections():
|
||||
if self._is_allowed_section(section, self._base_sections):
|
||||
self._process_section(self.parser, section, section)
|
||||
|
||||
parser = ConfigParser.ConfigParser()
|
||||
parser.read(pathname)
|
||||
|
||||
for section in self._sections:
|
||||
if parser.has_section(section):
|
||||
self._process_section(parser, section, section)
|
||||
|
||||
if vhost and parser.has_section('vhosts'):
|
||||
self._process_vhost(parser, vhost)
|
||||
if vhost and self.parser.has_section('vhosts'):
|
||||
self._process_vhost(self.parser, vhost)
|
||||
|
||||
def load_kv_files(self, language):
|
||||
"""Process the key/value (kv) files specified in the
|
||||
configuration, merging their values into the configuration as
|
||||
dotted heirarchical items."""
|
||||
|
||||
kv = _sub_config()
|
||||
|
||||
for fname in self.general.kv_files:
|
||||
@@ -76,6 +179,7 @@ class Config:
|
||||
fname = string.replace(fname, '%lang%', language)
|
||||
|
||||
parser = ConfigParser.ConfigParser()
|
||||
parser.optionxform = lambda x: x # don't case-normalize option names.
|
||||
parser.read(os.path.join(self.base, fname))
|
||||
for section in parser.sections():
|
||||
for option in parser.options(section):
|
||||
@@ -92,7 +196,13 @@ class Config:
|
||||
|
||||
return kv
|
||||
|
||||
def path(self, path):
|
||||
"""Return PATH relative to the config file directory."""
|
||||
return os.path.join(self.base, path)
|
||||
|
||||
def _process_section(self, parser, section, subcfg_name):
|
||||
if not hasattr(self, subcfg_name):
|
||||
setattr(self, subcfg_name, _sub_config())
|
||||
sc = getattr(self, subcfg_name)
|
||||
|
||||
for opt in parser.options(section):
|
||||
@@ -105,28 +215,58 @@ class Config:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
### FIXME: This feels like unnecessary depth of knowledge for a
|
||||
### semi-generic configuration object.
|
||||
if opt == 'cvs_roots' or opt == 'svn_roots':
|
||||
value = _parse_roots(opt, value)
|
||||
|
||||
setattr(sc, opt, value)
|
||||
|
||||
def _is_allowed_section(self, section, allowed_sections):
|
||||
"""Return 1 iff SECTION is an allowed section, defined as being
|
||||
explicitly present in the ALLOWED_SECTIONS list or present in the
|
||||
form 'someprefix-*' in that list."""
|
||||
|
||||
for allowed_section in allowed_sections:
|
||||
if allowed_section[-1] == '*':
|
||||
if _startswith(section, allowed_section[:-1]):
|
||||
return 1
|
||||
elif allowed_section == section:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def _is_allowed_override(self, sectype, secspec, section):
|
||||
"""Test if SECTION is an allowed override section for sections of
|
||||
type SECTYPE ('vhosts' or 'root', currently) and type-specifier
|
||||
SECSPEC (a rootname or vhostname, currently). If it is, return
|
||||
the overridden base section name. If it's not an override section
|
||||
at all, return None. And if it's an override section but not an
|
||||
allowed one, raise IllegalOverrideSection."""
|
||||
|
||||
cv = '%s-%s/' % (sectype, secspec)
|
||||
lcv = len(cv)
|
||||
if section[:lcv] != cv:
|
||||
return None
|
||||
base_section = section[lcv:]
|
||||
if self._is_allowed_section(base_section,
|
||||
self._allowed_overrides[sectype]):
|
||||
return base_section
|
||||
raise IllegalOverrideSection(sectype, section)
|
||||
|
||||
def _process_vhost(self, parser, vhost):
|
||||
# Find a vhost name for this VHOST, if any (else, we've nothing to do).
|
||||
canon_vhost = self._find_canon_vhost(parser, vhost)
|
||||
if not canon_vhost:
|
||||
# none of the vhost sections matched
|
||||
return
|
||||
|
||||
cv = canon_vhost + '-'
|
||||
lcv = len(cv)
|
||||
# Overlay any option sections associated with this vhost name.
|
||||
for section in parser.sections():
|
||||
if section[:lcv] == cv:
|
||||
self._process_section(parser, section, section[lcv:])
|
||||
base_section = self._is_allowed_override('vhost', canon_vhost, section)
|
||||
if base_section:
|
||||
self._process_section(parser, section, base_section)
|
||||
|
||||
def _find_canon_vhost(self, parser, vhost):
|
||||
vhost = string.lower(vhost)
|
||||
# Strip (ignore) port number:
|
||||
vhost = string.split(vhost, ':')[0]
|
||||
|
||||
vhost = string.split(string.lower(vhost), ':')[0] # lower-case, no port
|
||||
for canon_vhost in parser.options('vhosts'):
|
||||
value = parser.get('vhosts', canon_vhost)
|
||||
patterns = map(string.lower, map(string.strip,
|
||||
@@ -137,6 +277,102 @@ class Config:
|
||||
|
||||
return None
|
||||
|
||||
def overlay_root_options(self, rootname):
|
||||
"""Overlay per-root options for ROOTNAME atop the existing option
|
||||
set. This is a destructive change to the configuration."""
|
||||
|
||||
did_overlay = 0
|
||||
|
||||
if not self.conf_path:
|
||||
return
|
||||
|
||||
for section in self.parser.sections():
|
||||
base_section = self._is_allowed_override('root', rootname, section)
|
||||
if base_section:
|
||||
# We can currently only deal with root overlays happening
|
||||
# once, so check that we've not yet done any overlaying of
|
||||
# per-root options.
|
||||
assert(self.root_options_overlayed == 0)
|
||||
self._process_section(self.parser, section, base_section)
|
||||
did_overlay = 1
|
||||
|
||||
# If we actually did any overlaying, remember this fact so we
|
||||
# don't do it again later.
|
||||
if did_overlay:
|
||||
self.root_options_overlayed = 1
|
||||
|
||||
def _get_parser_items(self, parser, section):
|
||||
"""Basically implement ConfigParser.items() for pre-Python-2.3 versions."""
|
||||
try:
|
||||
return self.parser.items(section)
|
||||
except AttributeError:
|
||||
d = {}
|
||||
for option in parser.options(section):
|
||||
d[option] = parser.get(section, option)
|
||||
return d.items()
|
||||
|
||||
def get_authorizer_and_params_hack(self, rootname):
|
||||
"""Return a 2-tuple containing the name and parameters of the
|
||||
authorizer configured for use with ROOTNAME.
|
||||
|
||||
### FIXME: This whole thing is a hack caused by our not being able
|
||||
### to non-destructively overlay root options when trying to do
|
||||
### something like a root listing (which might need to get
|
||||
### different authorizer bits for each and every root in the list).
|
||||
### Until we have a good way to do that, we expose this function,
|
||||
### which assumes that base and per-vhost configuration has been
|
||||
### absorbed into this object and that per-root options have *not*
|
||||
### been overlayed. See issue #371."""
|
||||
|
||||
# We assume that per-root options have *not* been overlayed.
|
||||
assert(self.root_options_overlayed == 0)
|
||||
|
||||
if not self.conf_path:
|
||||
return None, {}
|
||||
|
||||
# Figure out the authorizer by searching first for a per-root
|
||||
# override, then falling back to the base/vhost configuration.
|
||||
authorizer = None
|
||||
root_options_section = 'root-%s/options' % (rootname)
|
||||
if self.parser.has_section(root_options_section) \
|
||||
and self.parser.has_option(root_options_section, 'authorizer'):
|
||||
authorizer = self.parser.get(root_options_section, 'authorizer')
|
||||
if not authorizer:
|
||||
authorizer = self.options.authorizer
|
||||
|
||||
# No authorizer? Get outta here.
|
||||
if not authorizer:
|
||||
return None, {}
|
||||
|
||||
# Dig up the parameters for the authorizer, starting with the
|
||||
# base/vhost items, then overlaying any root-specific ones we find.
|
||||
params = {}
|
||||
authz_section = 'authz-%s' % (authorizer)
|
||||
if hasattr(self, authz_section):
|
||||
sub_config = getattr(self, authz_section)
|
||||
for attr in dir(sub_config):
|
||||
params[attr] = getattr(sub_config, attr)
|
||||
root_authz_section = 'root-%s/authz-%s' % (rootname, authorizer)
|
||||
for section in self.parser.sections():
|
||||
if section == root_authz_section:
|
||||
for key, value in self._get_parser_items(self.parser, section):
|
||||
params[key] = value
|
||||
return authorizer, params
|
||||
|
||||
def get_authorizer_params(self, authorizer=None):
|
||||
"""Return a dictionary of parameter names and values which belong
|
||||
to the configured authorizer (or AUTHORIZER, if provided)."""
|
||||
params = {}
|
||||
if authorizer is None:
|
||||
authorizer = self.options.authorizer
|
||||
if authorizer:
|
||||
authz_section = 'authz-%s' % (self.options.authorizer)
|
||||
if hasattr(self, authz_section):
|
||||
sub_config = getattr(self, authz_section)
|
||||
for attr in dir(sub_config):
|
||||
params[attr] = getattr(sub_config, attr)
|
||||
return params
|
||||
|
||||
def set_defaults(self):
|
||||
"Set some default values in the configuration."
|
||||
|
||||
@@ -144,28 +380,72 @@ class Config:
|
||||
self.general.svn_roots = { }
|
||||
self.general.root_parents = []
|
||||
self.general.default_root = ''
|
||||
self.general.rcs_path = ''
|
||||
if sys.platform == "win32":
|
||||
self.general.cvsnt_exe_path = 'cvs'
|
||||
else:
|
||||
self.general.cvsnt_exe_path = None
|
||||
self.general.use_rcsparse = 0
|
||||
self.general.svn_path = ''
|
||||
self.general.mime_types_file = ''
|
||||
self.general.address = '<a href="mailto:user@insert.your.domain.here">No admin address has been configured</a>'
|
||||
self.general.forbidden = []
|
||||
self.general.forbiddenre = []
|
||||
self.general.kv_files = []
|
||||
self.general.mime_types_files = ["mimetypes.conf"]
|
||||
self.general.address = ''
|
||||
self.general.kv_files = [ ]
|
||||
self.general.languages = ['en-us']
|
||||
|
||||
self.utilities.rcs_dir = ''
|
||||
if sys.platform == "win32":
|
||||
self.utilities.cvsnt = 'cvs'
|
||||
else:
|
||||
self.utilities.cvsnt = None
|
||||
self.utilities.svn = ''
|
||||
self.utilities.diff = ''
|
||||
self.utilities.cvsgraph = ''
|
||||
|
||||
self.options.root_as_url_component = 1
|
||||
self.options.checkout_magic = 0
|
||||
self.options.allowed_views = ['annotate', 'diff', 'markup', 'roots']
|
||||
self.options.authorizer = None
|
||||
self.options.mangle_email_addresses = 0
|
||||
self.options.custom_log_formatting = []
|
||||
self.options.default_file_view = "log"
|
||||
self.options.http_expiration_time = 600
|
||||
self.options.generate_etags = 1
|
||||
self.options.svn_ignore_mimetype = 0
|
||||
self.options.svn_config_dir = None
|
||||
self.options.use_rcsparse = 0
|
||||
self.options.sort_by = 'file'
|
||||
self.options.sort_group_dirs = 1
|
||||
self.options.hide_attic = 1
|
||||
self.options.hide_errorful_entries = 0
|
||||
self.options.log_sort = 'date'
|
||||
self.options.diff_format = 'h'
|
||||
self.options.hide_cvsroot = 1
|
||||
self.options.hr_breakable = 1
|
||||
self.options.hr_funout = 1
|
||||
self.options.hr_ignore_white = 0
|
||||
self.options.hr_ignore_keyword_subst = 1
|
||||
self.options.hr_intraline = 0
|
||||
self.options.allow_compress = 0
|
||||
self.options.template_dir = "templates"
|
||||
self.options.docroot = None
|
||||
self.options.show_subdir_lastmod = 0
|
||||
self.options.show_roots_lastmod = 0
|
||||
self.options.show_logs = 1
|
||||
self.options.show_log_in_markup = 1
|
||||
self.options.cross_copies = 1
|
||||
self.options.use_localtime = 0
|
||||
self.options.short_log_len = 80
|
||||
self.options.enable_syntax_coloration = 1
|
||||
self.options.tabsize = 8
|
||||
self.options.detect_encoding = 0
|
||||
self.options.use_cvsgraph = 0
|
||||
self.options.cvsgraph_conf = "cvsgraph.conf"
|
||||
self.options.use_re_search = 0
|
||||
self.options.dir_pagesize = 0
|
||||
self.options.log_pagesize = 0
|
||||
self.options.log_pagesextra = 3
|
||||
self.options.limit_changes = 100
|
||||
|
||||
self.templates.diff = None
|
||||
self.templates.directory = None
|
||||
self.templates.error = None
|
||||
self.templates.file = None
|
||||
self.templates.graph = None
|
||||
self.templates.log = None
|
||||
self.templates.query = None
|
||||
self.templates.diff = None
|
||||
self.templates.graph = None
|
||||
self.templates.annotate = None
|
||||
self.templates.markup = None
|
||||
self.templates.error = None
|
||||
self.templates.query_form = None
|
||||
self.templates.query_results = None
|
||||
self.templates.roots = None
|
||||
@@ -180,110 +460,12 @@ class Config:
|
||||
self.cvsdb.readonly_passwd = ''
|
||||
self.cvsdb.row_limit = 1000
|
||||
self.cvsdb.rss_row_limit = 100
|
||||
self.cvsdb.check_database_for_root = 0
|
||||
|
||||
self.options.root_as_url_component = 0
|
||||
self.options.default_file_view = "log"
|
||||
self.options.checkout_magic = 0
|
||||
self.options.sort_by = 'file'
|
||||
self.options.sort_group_dirs = 1
|
||||
self.options.hide_attic = 1
|
||||
self.options.log_sort = 'date'
|
||||
self.options.diff_format = 'h'
|
||||
self.options.hide_cvsroot = 1
|
||||
self.options.hr_breakable = 1
|
||||
self.options.hr_funout = 1
|
||||
self.options.hr_ignore_white = 1
|
||||
self.options.hr_ignore_keyword_subst = 1
|
||||
self.options.hr_intraline = 0
|
||||
self.options.allow_annotate = 1
|
||||
self.options.allow_markup = 1
|
||||
self.options.allow_compress = 1
|
||||
self.options.template_dir = "templates"
|
||||
self.options.docroot = None
|
||||
self.options.show_subdir_lastmod = 0
|
||||
self.options.show_logs = 1
|
||||
self.options.show_log_in_markup = 1
|
||||
self.options.cross_copies = 0
|
||||
self.options.py2html_path = '.'
|
||||
self.options.short_log_len = 80
|
||||
self.options.use_enscript = 0
|
||||
self.options.enscript_path = ''
|
||||
self.options.use_highlight = 0
|
||||
self.options.highlight_path = ''
|
||||
self.options.highlight_line_numbers = 1
|
||||
self.options.highlight_convert_tabs = 2
|
||||
self.options.use_php = 0
|
||||
self.options.php_exe_path = 'php'
|
||||
self.options.allow_tar = 0
|
||||
self.options.use_cvsgraph = 0
|
||||
self.options.cvsgraph_path = ''
|
||||
self.options.cvsgraph_conf = "cvsgraph.conf"
|
||||
self.options.use_re_search = 0
|
||||
self.options.use_pagesize = 0
|
||||
self.options.limit_changes = 100
|
||||
self.options.use_localtime = 0
|
||||
self.options.http_expiration_time = 600
|
||||
self.options.generate_etags = 1
|
||||
|
||||
def is_forbidden(self, root, path_parts, pathtype):
|
||||
# If we don't have a root and path to check, get outta here.
|
||||
if not (root and path_parts):
|
||||
return 0
|
||||
|
||||
# Give precedence to the new 'forbiddenre' stuff first.
|
||||
if self.general.forbiddenre:
|
||||
|
||||
# Join the root and path-parts together into one path-like thing.
|
||||
root_and_path = string.join([root] + path_parts, "/")
|
||||
if pathtype == vclib.DIR:
|
||||
root_and_path = root_and_path + '/'
|
||||
|
||||
# If we still have a list of strings, replace those suckers with
|
||||
# lists of (compiled_regex, negation_flag)
|
||||
if type(self.general.forbiddenre[0]) == type(""):
|
||||
for i in range(len(self.general.forbiddenre)):
|
||||
pat = self.general.forbiddenre[i]
|
||||
if pat[0] == '!':
|
||||
self.general.forbiddenre[i] = (re.compile(pat[1:]), 1)
|
||||
else:
|
||||
self.general.forbiddenre[i] = (re.compile(pat), 0)
|
||||
|
||||
# Do the forbiddenness test.
|
||||
default = 0
|
||||
for (pat, negated) in self.general.forbiddenre:
|
||||
match = pat.search(root_and_path)
|
||||
if negated:
|
||||
default = 1
|
||||
if match:
|
||||
return 0
|
||||
elif match:
|
||||
return 1
|
||||
return default
|
||||
|
||||
# If no 'forbiddenre' is in use, we check 'forbidden', which only
|
||||
# looks at the top-most directory.
|
||||
elif self.general.forbidden:
|
||||
|
||||
# A root and a single non-directory path component? That's not
|
||||
# a module.
|
||||
if len(path_parts) == 1 and pathtype != vclib.DIR:
|
||||
return 0
|
||||
|
||||
# Do the forbiddenness test.
|
||||
module = path_parts[0]
|
||||
default = 0
|
||||
for pat in self.general.forbidden:
|
||||
if pat[0] == '!':
|
||||
default = 1
|
||||
if fnmatch.fnmatchcase(module, pat[1:]):
|
||||
return 0
|
||||
elif fnmatch.fnmatchcase(module, pat):
|
||||
return 1
|
||||
return default
|
||||
|
||||
# No forbiddenness configuration? Just allow it.
|
||||
else:
|
||||
return 0
|
||||
self.query.viewvc_base_url = None
|
||||
|
||||
def _startswith(somestr, substr):
|
||||
return somestr[:len(substr)] == substr
|
||||
|
||||
def _parse_roots(config_name, config_value):
|
||||
roots = { }
|
||||
@@ -295,8 +477,18 @@ def _parse_roots(config_name, config_value):
|
||||
roots[name] = path
|
||||
return roots
|
||||
|
||||
class ViewVCConfigurationError(Exception):
|
||||
pass
|
||||
|
||||
class MalformedRoot(Exception):
|
||||
class IllegalOverrideSection(ViewVCConfigurationError):
|
||||
def __init__(self, override_type, section_name):
|
||||
self.section_name = section_name
|
||||
self.override_type = override_type
|
||||
def __str__(self):
|
||||
return "malformed configuration: illegal %s override section: %s" \
|
||||
% (self.override_type, self.section_name)
|
||||
|
||||
class MalformedRoot(ViewVCConfigurationError):
|
||||
def __init__(self, config_name, value_given):
|
||||
Exception.__init__(self, config_name, value_given)
|
||||
self.config_name = config_name
|
||||
|
||||
287
lib/cvsdb.py
287
lib/cvsdb.py
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -17,8 +17,17 @@ import time
|
||||
import fnmatch
|
||||
import re
|
||||
|
||||
import vclib
|
||||
import dbi
|
||||
|
||||
## Current commits database schema version number.
|
||||
##
|
||||
## Version 0 was the original Bonsai-compatible version.
|
||||
##
|
||||
## Version 1 added the 'metadata' table (which holds the 'version' key)
|
||||
## and renamed all the 'repository'-related stuff to be 'root'-
|
||||
##
|
||||
CURRENT_SCHEMA_VERSION = 1
|
||||
|
||||
## error
|
||||
error = "cvsdb error"
|
||||
@@ -29,13 +38,13 @@ error = "cvsdb error"
|
||||
## complient database interface
|
||||
|
||||
class CheckinDatabase:
|
||||
def __init__(self, host, port, user, passwd, database, row_limit):
|
||||
def __init__(self, host, port, user, passwd, database):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._user = user
|
||||
self._passwd = passwd
|
||||
self._database = database
|
||||
self._row_limit = row_limit
|
||||
self._version = None
|
||||
|
||||
## database lookup caches
|
||||
self._get_cache = {}
|
||||
@@ -47,6 +56,19 @@ class CheckinDatabase:
|
||||
self._host, self._port, self._user, self._passwd, self._database)
|
||||
cursor = self.db.cursor()
|
||||
cursor.execute("SET AUTOCOMMIT=1")
|
||||
table_list = self.GetTableList()
|
||||
if 'metadata' in table_list:
|
||||
version = self.GetMetadataValue("version")
|
||||
if version is None:
|
||||
self._version = 0
|
||||
else:
|
||||
self._version = int(version)
|
||||
else:
|
||||
self._version = 0
|
||||
if self._version > CURRENT_SCHEMA_VERSION:
|
||||
raise DatabaseVersionError("Database version %d is newer than the "
|
||||
"last version supported by this "
|
||||
"software." % (self._version))
|
||||
|
||||
def sql_get_id(self, table, column, value, auto_set):
|
||||
sql = "SELECT id FROM %s WHERE %s=%%s" % (table, column)
|
||||
@@ -146,6 +168,45 @@ class CheckinDatabase:
|
||||
|
||||
return list
|
||||
|
||||
def GetCommitsTable(self):
|
||||
return self._version >= 1 and 'commits' or 'checkins'
|
||||
|
||||
def GetTableList(self):
|
||||
sql = "SHOW TABLES"
|
||||
cursor = self.db.cursor()
|
||||
cursor.execute(sql)
|
||||
list = []
|
||||
while 1:
|
||||
row = cursor.fetchone()
|
||||
if row == None:
|
||||
break
|
||||
list.append(row[0])
|
||||
return list
|
||||
|
||||
def GetMetadataValue(self, name):
|
||||
sql = "SELECT value FROM metadata WHERE name=%s"
|
||||
sql_args = (name)
|
||||
cursor = self.db.cursor()
|
||||
cursor.execute(sql, sql_args)
|
||||
try:
|
||||
(value,) = cursor.fetchone()
|
||||
except TypeError:
|
||||
return None
|
||||
return value
|
||||
|
||||
def SetMetadataValue(self, name, value):
|
||||
assert(self._version > 0)
|
||||
sql = "REPLACE INTO metadata (name, value) VALUES (%s, %s)"
|
||||
sql_args = (name, value)
|
||||
cursor = self.db.cursor()
|
||||
try:
|
||||
cursor.execute(sql, sql_args)
|
||||
except Exception, e:
|
||||
raise Exception("Error setting metadata: '%s'\n"
|
||||
"\tname = %s\n"
|
||||
"\tvalue = %s\n"
|
||||
% (str(e), name, value))
|
||||
|
||||
def GetBranchID(self, branch, auto_set = 1):
|
||||
return self.get_id("branches", "branch", branch, auto_set)
|
||||
|
||||
@@ -237,7 +298,7 @@ class CheckinDatabase:
|
||||
self.AddCommit(commit)
|
||||
|
||||
def AddCommit(self, commit):
|
||||
ci_when = dbi.DateTimeFromTicks(commit.GetTime())
|
||||
ci_when = dbi.DateTimeFromTicks(commit.GetTime() or 0.0)
|
||||
ci_type = commit.GetTypeString()
|
||||
who_id = self.GetAuthorID(commit.GetAuthor())
|
||||
repository_id = self.GetRepositoryID(commit.GetRepository())
|
||||
@@ -250,7 +311,8 @@ class CheckinDatabase:
|
||||
minus_count = commit.GetMinusCount() or '0'
|
||||
description_id = self.GetDescriptionID(commit.GetDescription())
|
||||
|
||||
sql = "REPLACE INTO checkins"\
|
||||
sql = "REPLACE INTO %s" % (self.GetCommitsTable())
|
||||
sql = sql + \
|
||||
" (type,ci_when,whoid,repositoryid,dirid,fileid,revision,"\
|
||||
" stickytag,branchid,addedlines,removedlines,descid)"\
|
||||
"VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
|
||||
@@ -259,7 +321,24 @@ class CheckinDatabase:
|
||||
plus_count, minus_count, description_id)
|
||||
|
||||
cursor = self.db.cursor()
|
||||
cursor.execute(sql, sql_args)
|
||||
try:
|
||||
cursor.execute(sql, sql_args)
|
||||
except Exception, e:
|
||||
raise Exception("Error adding commit: '%s'\n"
|
||||
"Values were:\n"
|
||||
"\ttype = %s\n"
|
||||
"\tci_when = %s\n"
|
||||
"\twhoid = %s\n"
|
||||
"\trepositoryid = %s\n"
|
||||
"\tdirid = %s\n"
|
||||
"\tfileid = %s\n"
|
||||
"\trevision = %s\n"
|
||||
"\tstickytag = %s\n"
|
||||
"\tbranchid = %s\n"
|
||||
"\taddedlines = %s\n"
|
||||
"\tremovedlines = %s\n"
|
||||
"\tdescid = %s\n"
|
||||
% ((str(e), ) + sql_args))
|
||||
|
||||
def SQLQueryListString(self, field, query_entry_list):
|
||||
sqlList = []
|
||||
@@ -273,9 +352,17 @@ class CheckinDatabase:
|
||||
match = " LIKE "
|
||||
elif query_entry.match == "glob":
|
||||
match = " REGEXP "
|
||||
# use fnmatch to translate the glob into a regexp
|
||||
# Use fnmatch to translate the glob into a regular
|
||||
# expression. Sadly, we have to account for the fact
|
||||
# that in Python 2.6, fnmatch.translate() started
|
||||
# sticking '\Z(?ms)' at the end of the regular
|
||||
# expression instead of just '$', and doesn't prepend
|
||||
# the '^'.
|
||||
data = fnmatch.translate(data)
|
||||
if data[0] != '^': data = '^' + data
|
||||
if data[0] != '^':
|
||||
data = '^' + data
|
||||
if data[-7:] == '\Z(?ms)':
|
||||
data = data[:-7] + '$'
|
||||
elif query_entry.match == "regex":
|
||||
match = " REGEXP "
|
||||
elif query_entry.match == "notregex":
|
||||
@@ -285,53 +372,70 @@ class CheckinDatabase:
|
||||
|
||||
return "(%s)" % (string.join(sqlList, " OR "))
|
||||
|
||||
def CreateSQLQueryString(self, query):
|
||||
tableList = [("checkins", None)]
|
||||
def CreateSQLQueryString(self, query, detect_leftover=0):
|
||||
commits_table = self.GetCommitsTable()
|
||||
tableList = [(commits_table, None)]
|
||||
condList = []
|
||||
|
||||
if len(query.repository_list):
|
||||
tableList.append(("repositories",
|
||||
"(checkins.repositoryid=repositories.id)"))
|
||||
"(%s.repositoryid=repositories.id)"
|
||||
% (commits_table)))
|
||||
temp = self.SQLQueryListString("repositories.repository",
|
||||
query.repository_list)
|
||||
condList.append(temp)
|
||||
|
||||
if len(query.branch_list):
|
||||
tableList.append(("branches", "(checkins.branchid=branches.id)"))
|
||||
tableList.append(("branches",
|
||||
"(%s.branchid=branches.id)" % (commits_table)))
|
||||
temp = self.SQLQueryListString("branches.branch",
|
||||
query.branch_list)
|
||||
condList.append(temp)
|
||||
|
||||
if len(query.directory_list):
|
||||
tableList.append(("dirs", "(checkins.dirid=dirs.id)"))
|
||||
tableList.append(("dirs",
|
||||
"(%s.dirid=dirs.id)" % (commits_table)))
|
||||
temp = self.SQLQueryListString("dirs.dir", query.directory_list)
|
||||
condList.append(temp)
|
||||
|
||||
if len(query.file_list):
|
||||
tableList.append(("files", "(checkins.fileid=files.id)"))
|
||||
tableList.append(("files",
|
||||
"(%s.fileid=files.id)" % (commits_table)))
|
||||
temp = self.SQLQueryListString("files.file", query.file_list)
|
||||
condList.append(temp)
|
||||
|
||||
if len(query.author_list):
|
||||
tableList.append(("people", "(checkins.whoid=people.id)"))
|
||||
tableList.append(("people",
|
||||
"(%s.whoid=people.id)" % (commits_table)))
|
||||
temp = self.SQLQueryListString("people.who", query.author_list)
|
||||
condList.append(temp)
|
||||
|
||||
if len(query.comment_list):
|
||||
tableList.append(("descs",
|
||||
"(%s.descid=descs.id)" % (commits_table)))
|
||||
temp = self.SQLQueryListString("descs.description",
|
||||
query.comment_list)
|
||||
condList.append(temp)
|
||||
|
||||
if query.from_date:
|
||||
temp = "(checkins.ci_when>=\"%s\")" % (str(query.from_date))
|
||||
temp = "(%s.ci_when>=\"%s\")" \
|
||||
% (commits_table, str(query.from_date))
|
||||
condList.append(temp)
|
||||
|
||||
if query.to_date:
|
||||
temp = "(checkins.ci_when<=\"%s\")" % (str(query.to_date))
|
||||
temp = "(%s.ci_when<=\"%s\")" \
|
||||
% (commits_table, str(query.to_date))
|
||||
condList.append(temp)
|
||||
|
||||
if query.sort == "date":
|
||||
order_by = "ORDER BY checkins.ci_when DESC,descid"
|
||||
order_by = "ORDER BY %s.ci_when DESC,descid" % (commits_table)
|
||||
elif query.sort == "author":
|
||||
tableList.append(("people", "(checkins.whoid=people.id)"))
|
||||
tableList.append(("people",
|
||||
"(%s.whoid=people.id)" % (commits_table)))
|
||||
order_by = "ORDER BY people.who,descid"
|
||||
elif query.sort == "file":
|
||||
tableList.append(("files", "(checkins.fileid=files.id)"))
|
||||
tableList.append(("files",
|
||||
"(%s.fileid=files.id)" % (commits_table)))
|
||||
order_by = "ORDER BY files.file,descid"
|
||||
|
||||
## exclude duplicates from the table list, and split out join
|
||||
@@ -349,28 +453,35 @@ class CheckinDatabase:
|
||||
conditions = string.join(joinConds + condList, " AND ")
|
||||
conditions = conditions and "WHERE %s" % conditions
|
||||
|
||||
## limit the number of rows requested or we could really slam
|
||||
## a server with a large database
|
||||
## apply the query's row limit, if any (so we avoid really
|
||||
## slamming a server with a large database)
|
||||
limit = ""
|
||||
if query.limit:
|
||||
limit = "LIMIT %s" % (str(query.limit))
|
||||
elif self._row_limit:
|
||||
limit = "LIMIT %s" % (str(self._row_limit))
|
||||
if detect_leftover:
|
||||
limit = "LIMIT %s" % (str(query.limit + 1))
|
||||
else:
|
||||
limit = "LIMIT %s" % (str(query.limit))
|
||||
|
||||
sql = "SELECT checkins.* FROM %s %s %s %s" % (
|
||||
tables, conditions, order_by, limit)
|
||||
sql = "SELECT %s.* FROM %s %s %s %s" \
|
||||
% (commits_table, tables, conditions, order_by, limit)
|
||||
|
||||
return sql
|
||||
|
||||
def RunQuery(self, query):
|
||||
sql = self.CreateSQLQueryString(query)
|
||||
sql = self.CreateSQLQueryString(query, 1)
|
||||
cursor = self.db.cursor()
|
||||
cursor.execute(sql)
|
||||
query.SetExecuted()
|
||||
row_count = 0
|
||||
|
||||
while 1:
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
break
|
||||
row_count = row_count + 1
|
||||
if query.limit and (row_count > query.limit):
|
||||
query.SetLimitReached()
|
||||
break
|
||||
|
||||
(dbType, dbCI_When, dbAuthorID, dbRepositoryID, dbDirID,
|
||||
dbFileID, dbRevision, dbStickyTag, dbBranchID, dbAddedLines,
|
||||
@@ -409,8 +520,12 @@ class CheckinDatabase:
|
||||
if file_id == None:
|
||||
return None
|
||||
|
||||
sql = "SELECT * FROM checkins WHERE "\
|
||||
" repositoryid=%s AND dirid=%s AND fileid=%s AND revision=%s"
|
||||
sql = "SELECT * FROM %s WHERE "\
|
||||
" repositoryid=%%s "\
|
||||
" AND dirid=%%s"\
|
||||
" AND fileid=%%s"\
|
||||
" AND revision=%%s"\
|
||||
% (self.GetCommitsTable())
|
||||
sql_args = (repository_id, dir_id, file_id, commit.GetRevision())
|
||||
|
||||
cursor = self.db.cursor()
|
||||
@@ -424,6 +539,75 @@ class CheckinDatabase:
|
||||
|
||||
return commit
|
||||
|
||||
def sql_delete(self, table, key, value, keep_fkey = None):
|
||||
sql = "DELETE FROM %s WHERE %s=%%s" % (table, key)
|
||||
sql_args = (value, )
|
||||
if keep_fkey:
|
||||
sql += " AND %s NOT IN (SELECT %s FROM %s WHERE %s = %%s)" \
|
||||
% (key, keep_fkey, self.GetCommitsTable(), keep_fkey)
|
||||
sql_args = (value, value)
|
||||
cursor = self.db.cursor()
|
||||
cursor.execute(sql, sql_args)
|
||||
|
||||
def sql_purge(self, table, key, fkey, ftable):
|
||||
sql = "DELETE FROM %s WHERE %s NOT IN (SELECT %s FROM %s)" \
|
||||
% (table, key, fkey, ftable)
|
||||
cursor = self.db.cursor()
|
||||
cursor.execute(sql)
|
||||
|
||||
def PurgeRepository(self, repository):
|
||||
rep_id = self.GetRepositoryID(repository, auto_set=0)
|
||||
if not rep_id:
|
||||
raise UnknownRepositoryError("Unknown repository '%s'"
|
||||
% (repository))
|
||||
|
||||
if (self._version >= 1):
|
||||
self.sql_delete('repositories', 'id', rep_id)
|
||||
self.sql_purge('commits', 'repositoryid', 'id', 'repositories')
|
||||
self.sql_purge('files', 'id', 'fileid', 'commits')
|
||||
self.sql_purge('dirs', 'id', 'dirid', 'commits')
|
||||
self.sql_purge('branches', 'id', 'branchid', 'commits')
|
||||
self.sql_purge('descs', 'id', 'descid', 'commits')
|
||||
self.sql_purge('people', 'id', 'whoid', 'commits')
|
||||
else:
|
||||
sql = "SELECT * FROM checkins WHERE repositoryid=%s"
|
||||
sql_args = (rep_id, )
|
||||
cursor = self.db.cursor()
|
||||
cursor.execute(sql, sql_args)
|
||||
checkins = []
|
||||
while 1:
|
||||
try:
|
||||
(ci_type, ci_when, who_id, repository_id,
|
||||
dir_id, file_id, revision, sticky_tag, branch_id,
|
||||
plus_count, minus_count, description_id) = \
|
||||
cursor.fetchone()
|
||||
except TypeError:
|
||||
break
|
||||
checkins.append([file_id, dir_id, branch_id,
|
||||
description_id, who_id])
|
||||
|
||||
#self.sql_delete('repositories', 'id', rep_id)
|
||||
self.sql_delete('checkins', 'repositoryid', rep_id)
|
||||
for checkin in checkins:
|
||||
self.sql_delete('files', 'id', checkin[0], 'fileid')
|
||||
self.sql_delete('dirs', 'id', checkin[1], 'dirid')
|
||||
self.sql_delete('branches', 'id', checkin[2], 'branchid')
|
||||
self.sql_delete('descs', 'id', checkin[3], 'descid')
|
||||
self.sql_delete('people', 'id', checkin[4], 'whoid')
|
||||
|
||||
# Reset all internal id caches. We could be choosier here,
|
||||
# but let's just be as safe as possible.
|
||||
self._get_cache = {}
|
||||
self._get_id_cache = {}
|
||||
self._desc_id_cache = {}
|
||||
|
||||
|
||||
class DatabaseVersionError(Exception):
|
||||
pass
|
||||
class UnknownRepositoryError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
## the Commit class holds data on one commit, the representation is as
|
||||
## close as possible to how it should be committed and retrieved to the
|
||||
## database engine
|
||||
@@ -471,10 +655,15 @@ class Commit:
|
||||
return self.__revision
|
||||
|
||||
def SetTime(self, gmt_time):
|
||||
self.__gmt_time = float(gmt_time)
|
||||
if gmt_time is None:
|
||||
### We're just going to assume that a datestamp of The Epoch
|
||||
### ain't real.
|
||||
self.__gmt_time = 0.0
|
||||
else:
|
||||
self.__gmt_time = float(gmt_time)
|
||||
|
||||
def GetTime(self):
|
||||
return self.__gmt_time
|
||||
return self.__gmt_time and self.__gmt_time or None
|
||||
|
||||
def SetAuthor(self, author):
|
||||
self.__author = author
|
||||
@@ -594,8 +783,9 @@ class QueryEntry:
|
||||
self.data = data
|
||||
self.match = match
|
||||
|
||||
## CheckinDatabaseQueryData is a object which contains the search parameters
|
||||
## for a query to the CheckinDatabase
|
||||
## CheckinDatabaseQuery is an object which contains the search
|
||||
## parameters for a query to the Checkin Database and -- after the
|
||||
## query is executed -- the data returned by the query.
|
||||
class CheckinDatabaseQuery:
|
||||
def __init__(self):
|
||||
## sorting
|
||||
@@ -607,6 +797,7 @@ class CheckinDatabaseQuery:
|
||||
self.directory_list = []
|
||||
self.file_list = []
|
||||
self.author_list = []
|
||||
self.comment_list = []
|
||||
|
||||
## date range in DBI 2.0 timedate objects
|
||||
self.from_date = None
|
||||
@@ -614,7 +805,8 @@ class CheckinDatabaseQuery:
|
||||
|
||||
## limit on number of rows to return
|
||||
self.limit = None
|
||||
|
||||
self.limit_reached = 0
|
||||
|
||||
## list of commits -- filled in by CVS query
|
||||
self.commit_list = []
|
||||
|
||||
@@ -622,6 +814,9 @@ class CheckinDatabaseQuery:
|
||||
## are added
|
||||
self.commit_cb = None
|
||||
|
||||
## has this query been run?
|
||||
self.executed = 0
|
||||
|
||||
def SetRepository(self, repository, match = "exact"):
|
||||
self.repository_list.append(QueryEntry(repository, match))
|
||||
|
||||
@@ -637,6 +832,9 @@ class CheckinDatabaseQuery:
|
||||
def SetAuthor(self, author, match = "exact"):
|
||||
self.author_list.append(QueryEntry(author, match))
|
||||
|
||||
def SetComment(self, comment, match = "exact"):
|
||||
self.comment_list.append(QueryEntry(comment, match))
|
||||
|
||||
def SetSortMethod(self, sort):
|
||||
self.sort = sort
|
||||
|
||||
@@ -664,6 +862,20 @@ class CheckinDatabaseQuery:
|
||||
def AddCommit(self, commit):
|
||||
self.commit_list.append(commit)
|
||||
|
||||
def SetExecuted(self):
|
||||
self.executed = 1
|
||||
|
||||
def SetLimitReached(self):
|
||||
self.limit_reached = 1
|
||||
|
||||
def GetLimitReached(self):
|
||||
assert self.executed
|
||||
return self.limit_reached
|
||||
|
||||
def GetCommitList(self):
|
||||
assert self.executed
|
||||
return self.commit_list
|
||||
|
||||
|
||||
##
|
||||
## entrypoints
|
||||
@@ -682,7 +894,7 @@ def ConnectDatabase(cfg, readonly=0):
|
||||
user = cfg.cvsdb.user
|
||||
passwd = cfg.cvsdb.passwd
|
||||
db = CheckinDatabase(cfg.cvsdb.host, cfg.cvsdb.port, user, passwd,
|
||||
cfg.cvsdb.database_name, cfg.cvsdb.row_limit)
|
||||
cfg.cvsdb.database_name)
|
||||
db.Connect()
|
||||
return db
|
||||
|
||||
@@ -695,7 +907,8 @@ def GetCommitListFromRCSFile(repository, path_parts, revision=None):
|
||||
directory = string.join(path_parts[:-1], "/")
|
||||
file = path_parts[-1]
|
||||
|
||||
revs = repository.itemlog(path_parts, revision, {"cvs_pass_rev": 1})
|
||||
revs = repository.itemlog(path_parts, revision, vclib.SORTBY_DEFAULT,
|
||||
0, 0, {"cvs_pass_rev": 1})
|
||||
for rev in revs:
|
||||
commit = CreateCommit()
|
||||
commit.SetRepository(repository.rootpath)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
|
||||
26
lib/debug.py
26
lib/debug.py
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -17,10 +17,20 @@
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
# Set to non-zero to track and print processing times
|
||||
SHOW_TIMES = 0
|
||||
|
||||
# Set to non-zero to display child process info
|
||||
SHOW_CHILD_PROCESSES = 0
|
||||
|
||||
# Set to a server-side path to force the tarball view to generate the
|
||||
# tarball as a file on the server, instead of transmitting the data
|
||||
# back to the browser. This enables easy display of error
|
||||
# considitions in the browser, as well as tarball inspection on the
|
||||
# server. NOTE: The file will be a TAR archive, *not* gzip-compressed.
|
||||
TARFILE_PATH = ''
|
||||
|
||||
|
||||
if SHOW_TIMES:
|
||||
|
||||
import time
|
||||
@@ -38,13 +48,17 @@ if SHOW_TIMES:
|
||||
else:
|
||||
_times[which] = t
|
||||
|
||||
def dump():
|
||||
for name, value in _times.items():
|
||||
print '%s: %.6f<br />' % (name, value)
|
||||
def t_dump(out):
|
||||
out.write('<div>')
|
||||
names = _times.keys()
|
||||
names.sort()
|
||||
for name in names:
|
||||
out.write('%s: %.6fs<br/>\n' % (name, _times[name]))
|
||||
out.write('</div>')
|
||||
|
||||
else:
|
||||
|
||||
t_start = t_end = dump = lambda *args: None
|
||||
t_start = t_end = t_dump = lambda *args: None
|
||||
|
||||
|
||||
class ViewVCException:
|
||||
|
||||
127
lib/ezt.py
127
lib/ezt.py
@@ -189,9 +189,10 @@ Directives
|
||||
templates are escaped before they are put into the output stream. It
|
||||
has no effect on the literal text of the templates, only the output
|
||||
from [QUAL_NAME ...] directives. STRING can be one of "raw" "html"
|
||||
or "xml". The "raw" mode leaves the output unaltered. The "html" and
|
||||
"xml" modes escape special characters using entity escapes (like
|
||||
" and >)
|
||||
"xml" or "uri". The "raw" mode leaves the output unaltered; the "html"
|
||||
and "xml" modes escape special characters using entity escapes (like
|
||||
" and >); the "uri" mode escapes characters using hexadecimal
|
||||
escape sequences (like %20 and %7e).
|
||||
|
||||
[format CALLBACK]
|
||||
|
||||
@@ -235,6 +236,7 @@ import re
|
||||
from types import StringType, IntType, FloatType, LongType, TupleType
|
||||
import os
|
||||
import cgi
|
||||
import urllib
|
||||
try:
|
||||
import cStringIO
|
||||
except ImportError:
|
||||
@@ -247,6 +249,7 @@ except ImportError:
|
||||
FORMAT_RAW = 'raw'
|
||||
FORMAT_HTML = 'html'
|
||||
FORMAT_XML = 'xml'
|
||||
FORMAT_URI = 'uri'
|
||||
|
||||
#
|
||||
# This regular expression matches three alternatives:
|
||||
@@ -344,7 +347,7 @@ class Template:
|
||||
for_names = [ ]
|
||||
|
||||
if base_format:
|
||||
program.append((self._cmd_format, _printers[base_format]))
|
||||
program.append((self._cmd_format, _formatters[base_format]))
|
||||
|
||||
for i in range(len(parts)):
|
||||
piece = parts[i]
|
||||
@@ -402,13 +405,13 @@ class Template:
|
||||
elif cmd == 'format':
|
||||
if args[1][0]:
|
||||
# argument is a variable reference
|
||||
printer = args[1]
|
||||
formatter = args[1]
|
||||
else:
|
||||
# argument is a string constant referring to built-in printer
|
||||
printer = _printers.get(args[1][1])
|
||||
if not printer:
|
||||
# argument is a string constant referring to built-in formatter
|
||||
formatter = _formatters.get(args[1][1])
|
||||
if not formatter:
|
||||
raise UnknownFormatConstantError(str(args[1:]))
|
||||
program.append((self._cmd_format, printer))
|
||||
program.append((self._cmd_format, formatter))
|
||||
|
||||
# remember the cmd, current pos, args, and a section placeholder
|
||||
stack.append([cmd, len(program), args[1:], None])
|
||||
@@ -457,15 +460,18 @@ class Template:
|
||||
def _cmd_print(self, valrefs, ctx):
|
||||
value = _get_value(valrefs[0], ctx)
|
||||
args = map(lambda valref, ctx=ctx: _get_value(valref, ctx), valrefs[1:])
|
||||
_write_value(value, args, ctx)
|
||||
try:
|
||||
_write_value(value, args, ctx)
|
||||
except TypeError:
|
||||
raise Exception("Unprintable value type for '%s'" % (str(valrefs[0][0])))
|
||||
|
||||
def _cmd_format(self, printer, ctx):
|
||||
if type(printer) is TupleType:
|
||||
printer = _get_value(printer, ctx)
|
||||
ctx.printers.append(printer)
|
||||
def _cmd_format(self, formatter, ctx):
|
||||
if type(formatter) is TupleType:
|
||||
formatter = _get_value(formatter, ctx)
|
||||
ctx.formatters.append(formatter)
|
||||
|
||||
def _cmd_end_format(self, valref, ctx):
|
||||
ctx.printers.pop()
|
||||
ctx.formatters.pop()
|
||||
|
||||
def _cmd_include(self, (valref, reader), ctx):
|
||||
fname = _get_value(valref, ctx)
|
||||
@@ -519,7 +525,8 @@ class Template:
|
||||
((valref,), unused, section) = args
|
||||
list = _get_value(valref, ctx)
|
||||
if isinstance(list, StringType):
|
||||
raise NeedSequenceError()
|
||||
raise NeedSequenceError("The value of '%s' is not a sequence"
|
||||
% (valref[0]))
|
||||
refname = valref[0]
|
||||
ctx.for_iterators[refname] = iterator = _iter(list)
|
||||
for unused in iterator:
|
||||
@@ -630,14 +637,23 @@ def _get_value((refname, start, rest), ctx):
|
||||
# string or a sequence
|
||||
return ob
|
||||
|
||||
def _print_formatted(formatters, ctx, chunk):
|
||||
# print chunk to ctx.fp after running it sequentially through formatters
|
||||
for formatter in formatters:
|
||||
chunk = formatter(chunk)
|
||||
ctx.fp.write(chunk)
|
||||
|
||||
def _write_value(value, args, ctx):
|
||||
# value is a callback function, generates its own output
|
||||
if callable(value):
|
||||
apply(value, [ctx] + list(args))
|
||||
return
|
||||
|
||||
# pop printer in case it recursively calls _write_value
|
||||
printer = ctx.printers.pop()
|
||||
# squirrel away formatters in case one of them recursively calls
|
||||
# _write_value() -- we'll use them (in reverse order) to format our
|
||||
# output.
|
||||
formatters = ctx.formatters[:]
|
||||
formatters.reverse()
|
||||
|
||||
try:
|
||||
# if the value has a 'read' attribute, then it is a stream: copy it
|
||||
@@ -646,7 +662,7 @@ def _write_value(value, args, ctx):
|
||||
chunk = value.read(16384)
|
||||
if not chunk:
|
||||
break
|
||||
printer(ctx, chunk)
|
||||
_print_formatted(formatters, ctx, chunk)
|
||||
|
||||
# value is a substitution pattern
|
||||
elif args:
|
||||
@@ -659,21 +675,58 @@ def _write_value(value, args, ctx):
|
||||
piece = args[idx]
|
||||
else:
|
||||
piece = '<undef>'
|
||||
printer(ctx, piece)
|
||||
_print_formatted(formatters, ctx, piece)
|
||||
|
||||
# plain old value, write to output
|
||||
else:
|
||||
printer(ctx, value)
|
||||
_print_formatted(formatters, ctx, value)
|
||||
|
||||
finally:
|
||||
ctx.printers.append(printer)
|
||||
# restore our formatters
|
||||
formatters.reverse()
|
||||
ctx.formatters = formatters
|
||||
|
||||
|
||||
class TemplateData:
|
||||
"""A custom dictionary-like object that allows one-time definition
|
||||
of keys, and only value fetches and changes, and key deletions,
|
||||
thereafter.
|
||||
|
||||
EZT doesn't require the use of this special class -- a normal
|
||||
dict-type data dictionary works fine. But use of this class will
|
||||
assist those who want the data sent to their templates to have a
|
||||
consistent set of keys."""
|
||||
|
||||
def __init__(self, initial_data={}):
|
||||
self._items = initial_data
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._items.__getitem__(key)
|
||||
|
||||
def __setitem__(self, key, item):
|
||||
assert self._items.has_key(key)
|
||||
return self._items.__setitem__(key, item)
|
||||
|
||||
def __delitem__(self, key):
|
||||
return self._items.__delitem__(key)
|
||||
|
||||
def keys(self):
|
||||
return self._items.keys()
|
||||
|
||||
def merge(self, template_data):
|
||||
"""Merge the data in TemplataData instance TEMPLATA_DATA into this
|
||||
instance. Avoid the temptation to use this conditionally in your
|
||||
code -- it rather defeats the purpose of this class."""
|
||||
|
||||
assert isinstance(template_data, TemplateData)
|
||||
self._items.update(template_data._items)
|
||||
|
||||
|
||||
class Context:
|
||||
"""A container for the execution context"""
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
self.printers = []
|
||||
self.formatters = []
|
||||
def write(self, value, args=()):
|
||||
_write_value(value, args, self)
|
||||
|
||||
@@ -786,16 +839,26 @@ class BaseUnavailableError(EZTException):
|
||||
class UnknownFormatConstantError(EZTException):
|
||||
"""The format specifier is an unknown value."""
|
||||
|
||||
def _raw_printer(ctx, s):
|
||||
ctx.fp.write(s)
|
||||
|
||||
def _html_printer(ctx, s):
|
||||
ctx.fp.write(cgi.escape(s))
|
||||
def _raw_formatter(s):
|
||||
return s
|
||||
|
||||
_printers = {
|
||||
FORMAT_RAW : _raw_printer,
|
||||
FORMAT_HTML : _html_printer,
|
||||
FORMAT_XML : _html_printer,
|
||||
def _html_formatter(s):
|
||||
return cgi.escape(s)
|
||||
|
||||
def _xml_formatter(s):
|
||||
s = s.replace('&', '&')
|
||||
s = s.replace('<', '<')
|
||||
s = s.replace('>', '>')
|
||||
return s
|
||||
|
||||
def _uri_formatter(s):
|
||||
return urllib.quote(s)
|
||||
|
||||
_formatters = {
|
||||
FORMAT_RAW : _raw_formatter,
|
||||
FORMAT_HTML : _html_formatter,
|
||||
FORMAT_XML : _xml_formatter,
|
||||
FORMAT_URI : _uri_formatter,
|
||||
}
|
||||
|
||||
# --- standard test environment ---
|
||||
|
||||
20
lib/idiff.py
20
lib/idiff.py
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -20,7 +20,7 @@ import difflib
|
||||
import sys
|
||||
import re
|
||||
import ezt
|
||||
import cgi
|
||||
import sapi
|
||||
|
||||
def sidebyside(fromlines, tolines, context):
|
||||
"""Generate side by side diff"""
|
||||
@@ -49,18 +49,18 @@ def _mdiff_split(flag, (line_number, text)):
|
||||
while True:
|
||||
m = _re_mdiff.search(text, pos)
|
||||
if not m:
|
||||
segments.append(_item(text=cgi.escape(text[pos:]), type=None))
|
||||
segments.append(_item(text=sapi.escape(text[pos:]), type=None))
|
||||
break
|
||||
|
||||
if m.start() > pos:
|
||||
segments.append(_item(text=cgi.escape(text[pos:m.start()]), type=None))
|
||||
segments.append(_item(text=sapi.escape(text[pos:m.start()]), type=None))
|
||||
|
||||
if m.group(1) == "+":
|
||||
segments.append(_item(text=cgi.escape(m.group(2)), type="add"))
|
||||
segments.append(_item(text=sapi.escape(m.group(2)), type="add"))
|
||||
elif m.group(1) == "-":
|
||||
segments.append(_item(text=cgi.escape(m.group(2)), type="remove"))
|
||||
segments.append(_item(text=sapi.escape(m.group(2)), type="remove"))
|
||||
elif m.group(1) == "^":
|
||||
segments.append(_item(text=cgi.escape(m.group(2)), type="change"))
|
||||
segments.append(_item(text=sapi.escape(m.group(2)), type="change"))
|
||||
|
||||
pos = m.end()
|
||||
|
||||
@@ -166,12 +166,12 @@ def _differ_split(row, guide):
|
||||
|
||||
for m in _re_differ.finditer(guide, pos):
|
||||
if m.start() > pos:
|
||||
segments.append(_item(text=cgi.escape(line[pos:m.start()]), type=None))
|
||||
segments.append(_item(text=cgi.escape(line[m.start():m.end()]),
|
||||
segments.append(_item(text=sapi.escape(line[pos:m.start()]), type=None))
|
||||
segments.append(_item(text=sapi.escape(line[m.start():m.end()]),
|
||||
type="change"))
|
||||
pos = m.end()
|
||||
|
||||
segments.append(_item(text=cgi.escape(line[pos:]), type=None))
|
||||
segments.append(_item(text=sapi.escape(line[pos:]), type=None))
|
||||
|
||||
return _item(gap=ezt.boolean(gap), type=type, segments=segments,
|
||||
left_number=left_number, right_number=right_number)
|
||||
|
||||
196
lib/popen.py
196
lib/popen.py
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -131,194 +131,6 @@ def popen(cmd, args, mode, capture_err=1):
|
||||
# crap. shouldn't be here.
|
||||
sys.exit(127)
|
||||
|
||||
def pipe_cmds(cmds, out=None):
|
||||
"""Executes a sequence of commands. The output of each command is directed to
|
||||
the input of the next command. A _pipe object is returned for writing to the
|
||||
first command's input. The output of the last command is directed to the
|
||||
"out" file object or the standard output if "out" is None. If "out" is not an
|
||||
OS file descriptor, a separate thread will be spawned to send data to its
|
||||
write() method."""
|
||||
|
||||
if out is None:
|
||||
out = sys.stdout
|
||||
|
||||
if sys.platform == "win32":
|
||||
### FIXME: windows implementation ignores "out" argument, always
|
||||
### writing last command's output to standard out
|
||||
|
||||
if debug.SHOW_CHILD_PROCESSES:
|
||||
dbgIn = StringIO.StringIO()
|
||||
hStdIn, handle = win32popen.MakeSpyPipe(1, 0, (dbgIn,))
|
||||
|
||||
i = 0
|
||||
for cmd in cmds:
|
||||
i = i + 1
|
||||
|
||||
dbgOut, dbgErr = StringIO.StringIO(), StringIO.StringIO()
|
||||
|
||||
if i < len(cmds):
|
||||
nextStdIn, hStdOut = win32popen.MakeSpyPipe(1, 1, (dbgOut,))
|
||||
x, hStdErr = win32popen.MakeSpyPipe(None, 1, (dbgErr,))
|
||||
else:
|
||||
ehandle = win32event.CreateEvent(None, 1, 0, None)
|
||||
nextStdIn, hStdOut = win32popen.MakeSpyPipe(None, 1, (dbgOut, sapi.server.file()), ehandle)
|
||||
x, hStdErr = win32popen.MakeSpyPipe(None, 1, (dbgErr,))
|
||||
|
||||
command = win32popen.CommandLine(cmd[0], cmd[1:])
|
||||
phandle, pid, thandle, tid = win32popen.CreateProcess(command, hStdIn, hStdOut, hStdErr)
|
||||
if debug.SHOW_CHILD_PROCESSES:
|
||||
debug.Process(command, dbgIn, dbgOut, dbgErr)
|
||||
|
||||
dbgIn = dbgOut
|
||||
hStdIn = nextStdIn
|
||||
|
||||
|
||||
else:
|
||||
|
||||
hStdIn, handle = win32popen.CreatePipe(1, 0)
|
||||
spool = None
|
||||
|
||||
i = 0
|
||||
for cmd in cmds:
|
||||
i = i + 1
|
||||
if i < len(cmds):
|
||||
nextStdIn, hStdOut = win32popen.CreatePipe(1, 1)
|
||||
else:
|
||||
# very last process
|
||||
nextStdIn = None
|
||||
|
||||
if sapi.server.inheritableOut:
|
||||
# send child output to standard out
|
||||
hStdOut = win32popen.MakeInheritedHandle(win32popen.FileObject2File(sys.stdout),0)
|
||||
ehandle = None
|
||||
else:
|
||||
ehandle = win32event.CreateEvent(None, 1, 0, None)
|
||||
x, hStdOut = win32popen.MakeSpyPipe(None, 1, (sapi.server.file(),), ehandle)
|
||||
|
||||
command = win32popen.CommandLine(cmd[0], cmd[1:])
|
||||
phandle, pid, thandle, tid = win32popen.CreateProcess(command, hStdIn, hStdOut, None)
|
||||
hStdIn = nextStdIn
|
||||
|
||||
return _pipe(win32popen.File2FileObject(handle, 'wb'), phandle, ehandle)
|
||||
|
||||
# flush the stdio buffers since we are about to change the FD under them
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
prev_r, parent_w = os.pipe()
|
||||
|
||||
null = os.open('/dev/null', os.O_RDWR)
|
||||
|
||||
child_pids = []
|
||||
|
||||
for cmd in cmds[:-1]:
|
||||
r, w = os.pipe()
|
||||
pid = os.fork()
|
||||
if not pid:
|
||||
# in the child
|
||||
|
||||
# hook up stdin to the "read" channel
|
||||
os.dup2(prev_r, 0)
|
||||
|
||||
# hook up stdout to the output channel
|
||||
os.dup2(w, 1)
|
||||
|
||||
# toss errors
|
||||
os.dup2(null, 2)
|
||||
|
||||
# close these extra descriptors
|
||||
os.close(prev_r)
|
||||
os.close(parent_w)
|
||||
os.close(null)
|
||||
os.close(r)
|
||||
os.close(w)
|
||||
|
||||
# time to run the command
|
||||
try:
|
||||
os.execvp(cmd[0], cmd)
|
||||
except:
|
||||
pass
|
||||
|
||||
sys.exit(127)
|
||||
|
||||
# in the parent
|
||||
child_pids.append(pid)
|
||||
|
||||
# we don't need these any more
|
||||
os.close(prev_r)
|
||||
os.close(w)
|
||||
|
||||
# the read channel of this pipe will feed into to the next command
|
||||
prev_r = r
|
||||
|
||||
# no longer needed
|
||||
os.close(null)
|
||||
|
||||
# done with most of the commands. set up the last command to write to "out"
|
||||
if not hasattr(out, 'fileno'):
|
||||
r, w = os.pipe()
|
||||
|
||||
pid = os.fork()
|
||||
if not pid:
|
||||
# in the child (the last command)
|
||||
|
||||
# hook up stdin to the "read" channel
|
||||
os.dup2(prev_r, 0)
|
||||
|
||||
# hook up stdout to "out"
|
||||
if hasattr(out, 'fileno'):
|
||||
if out.fileno() != 1:
|
||||
os.dup2(out.fileno(), 1)
|
||||
out.close()
|
||||
|
||||
else:
|
||||
# "out" can't be hooked up directly, so use a pipe and a thread
|
||||
os.dup2(w, 1)
|
||||
os.close(r)
|
||||
os.close(w)
|
||||
|
||||
# close these extra descriptors
|
||||
os.close(prev_r)
|
||||
os.close(parent_w)
|
||||
|
||||
# run the last command
|
||||
try:
|
||||
os.execvp(cmds[-1][0], cmds[-1])
|
||||
except:
|
||||
pass
|
||||
|
||||
sys.exit(127)
|
||||
|
||||
child_pids.append(pid)
|
||||
# not needed any more
|
||||
os.close(prev_r)
|
||||
|
||||
if not hasattr(out, 'fileno'):
|
||||
os.close(w)
|
||||
thread = _copy(r, out)
|
||||
thread.start()
|
||||
else:
|
||||
thread = None
|
||||
|
||||
# write into the first pipe, wait on the final process
|
||||
return _pipe(os.fdopen(parent_w, 'w'), child_pids, thread=thread)
|
||||
|
||||
class _copy(threading.Thread):
|
||||
def __init__(self, srcfd, destfile):
|
||||
self.srcfd = srcfd
|
||||
self.destfile = destfile
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while 1:
|
||||
s = os.read(self.srcfd, 1024)
|
||||
if not s:
|
||||
break
|
||||
self.destfile.write(s)
|
||||
finally:
|
||||
os.close(self.srcfd)
|
||||
|
||||
class _pipe:
|
||||
"Wrapper for a file which can wait() on a child process at close time."
|
||||
|
||||
@@ -364,12 +176,12 @@ class _pipe:
|
||||
else:
|
||||
if self.thread:
|
||||
self.thread.join()
|
||||
if type(self.child_pid) == type([]):
|
||||
if type(self.child_pid) == type([]):
|
||||
for pid in self.child_pid:
|
||||
exit = os.waitpid(pid, 0)[1]
|
||||
return exit
|
||||
else:
|
||||
return os.waitpid(self.child_pid, 0)[1]
|
||||
else:
|
||||
return os.waitpid(self.child_pid, 0)[1]
|
||||
return None
|
||||
|
||||
def __getattr__(self, name):
|
||||
|
||||
144
lib/query.py
144
lib/query.py
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -28,6 +28,7 @@ import viewvc
|
||||
import ezt
|
||||
import debug
|
||||
import urllib
|
||||
import fnmatch
|
||||
|
||||
class FormData:
|
||||
def __init__(self, form):
|
||||
@@ -216,8 +217,9 @@ def decode_command(cmd):
|
||||
else:
|
||||
return "exact"
|
||||
|
||||
def form_to_cvsdb_query(form_data):
|
||||
def form_to_cvsdb_query(cfg, form_data):
|
||||
query = cvsdb.CreateCheckinQuery()
|
||||
query.SetLimit(cfg.cvsdb.row_limit)
|
||||
|
||||
if form_data.repository:
|
||||
for cmd, str in listparse_string(form_data.repository):
|
||||
@@ -273,17 +275,60 @@ def prev_rev(rev):
|
||||
r = r[:-2]
|
||||
return string.join(r, '.')
|
||||
|
||||
def is_forbidden(cfg, cvsroot_name, module):
|
||||
'''Return 1 if MODULE in CVSROOT_NAME is forbidden; return 0 otherwise.'''
|
||||
|
||||
# CVSROOT_NAME might be None here if the data comes from an
|
||||
# unconfigured root. This interfaces doesn't care that the root
|
||||
# isn't configured, but if that's the case, it will consult only
|
||||
# the base and per-vhost configuration for authorizer and
|
||||
# authorizer parameters.
|
||||
if cvsroot_name:
|
||||
authorizer, params = cfg.get_authorizer_and_params_hack(cvsroot_name)
|
||||
else:
|
||||
authorizer = cfg.options.authorizer
|
||||
params = cfg.get_authorizer_params()
|
||||
|
||||
# If CVSROOT_NAME isn't configured to use an authorizer, nothing
|
||||
# is forbidden. If it's configured to use something other than
|
||||
# the 'forbidden' authorizer, complain. Otherwise, check for
|
||||
# forbiddenness per the PARAMS as expected.
|
||||
if not authorizer:
|
||||
return 0
|
||||
if authorizer != 'forbidden':
|
||||
raise Exception("The 'forbidden' authorizer is the only one supported "
|
||||
"by this interface. The '%s' root is configured to "
|
||||
"use a different one." % (cvsroot_name))
|
||||
forbidden = params.get('forbidden', '')
|
||||
forbidden = map(string.strip, filter(None, string.split(forbidden, ',')))
|
||||
default = 0
|
||||
for pat in forbidden:
|
||||
if pat[0] == '!':
|
||||
default = 1
|
||||
if fnmatch.fnmatchcase(module, pat[1:]):
|
||||
return 0
|
||||
elif fnmatch.fnmatchcase(module, pat):
|
||||
return 1
|
||||
return default
|
||||
|
||||
def build_commit(server, cfg, desc, files, cvsroots, viewvc_link):
|
||||
ob = _item(num_files=len(files), files=[])
|
||||
|
||||
if desc:
|
||||
ob.log = string.replace(server.escape(desc), '\n', '<br />')
|
||||
else:
|
||||
ob.log = ' '
|
||||
ob.log = desc and string.replace(server.escape(desc), '\n', '<br />') or ''
|
||||
|
||||
for commit in files:
|
||||
parts = filter(None, string.split(commit.GetDirectory(), '/'))
|
||||
if parts and cfg.options.hide_cvsroot and parts[0] == 'CVSROOT':
|
||||
repository = commit.GetRepository()
|
||||
directory = commit.GetDirectory()
|
||||
cvsroot_name = cvsroots.get(repository)
|
||||
|
||||
## find the module name (if any)
|
||||
try:
|
||||
module = filter(None, string.split(directory, '/'))[0]
|
||||
except IndexError:
|
||||
module = None
|
||||
|
||||
## skip commits we aren't supposed to show
|
||||
if module and ((module == 'CVSROOT' and cfg.options.hide_cvsroot) \
|
||||
or is_forbidden(cfg, cvsroot_name, module)):
|
||||
continue
|
||||
|
||||
ctime = commit.GetTime()
|
||||
@@ -294,22 +339,18 @@ def build_commit(server, cfg, desc, files, cvsroots, viewvc_link):
|
||||
ctime = time.strftime("%y/%m/%d %H:%M %Z", time.localtime(ctime))
|
||||
else:
|
||||
ctime = time.strftime("%y/%m/%d %H:%M", time.gmtime(ctime)) \
|
||||
+ ' UTC'
|
||||
+ ' UTC'
|
||||
|
||||
## make the file link
|
||||
repository = commit.GetRepository()
|
||||
directory = commit.GetDirectory()
|
||||
file = (directory and directory + "/") + commit.GetFile()
|
||||
cvsroot_name = cvsroots.get(repository)
|
||||
try:
|
||||
file = (directory and directory + "/") + commit.GetFile()
|
||||
except:
|
||||
raise Exception, str([directory, commit.GetFile()])
|
||||
|
||||
## skip forbidden files
|
||||
if cfg.is_forbidden(cvsroot_name,
|
||||
filter(None, string.split(file, "/")), vclib.FILE):
|
||||
continue
|
||||
|
||||
## if we couldn't find the cvsroot path configured in the
|
||||
## viewvc.conf file, then don't make the link
|
||||
if cvsroot_name:
|
||||
## If we couldn't find the cvsroot path configured in the
|
||||
## viewvc.conf file, or we don't have a VIEWVC_LINK, then
|
||||
## don't make the link.
|
||||
if cvsroot_name and viewvc_link:
|
||||
flink = '[%s] <a href="%s/%s?root=%s">%s</a>' % (
|
||||
cvsroot_name, viewvc_link, urllib.quote(file),
|
||||
cvsroot_name, file)
|
||||
@@ -337,23 +378,27 @@ def build_commit(server, cfg, desc, files, cvsroots, viewvc_link):
|
||||
return ob
|
||||
|
||||
def run_query(server, cfg, form_data, viewvc_link):
|
||||
query = form_to_cvsdb_query(form_data)
|
||||
query = form_to_cvsdb_query(cfg, form_data)
|
||||
db = cvsdb.ConnectDatabaseReadOnly(cfg)
|
||||
db.RunQuery(query)
|
||||
|
||||
if not query.commit_list:
|
||||
return [ ]
|
||||
commit_list = query.GetCommitList()
|
||||
if not commit_list:
|
||||
return [ ], 0
|
||||
|
||||
row_limit_reached = query.GetLimitReached()
|
||||
|
||||
commits = [ ]
|
||||
files = [ ]
|
||||
|
||||
cvsroots = {}
|
||||
viewvc.expand_root_parents(cfg)
|
||||
rootitems = cfg.general.svn_roots.items() + cfg.general.cvs_roots.items()
|
||||
for key, value in rootitems:
|
||||
cvsroots[cvsdb.CleanRepository(value)] = key
|
||||
|
||||
current_desc = query.commit_list[0].GetDescription()
|
||||
for commit in query.commit_list:
|
||||
current_desc = commit_list[0].GetDescription()
|
||||
for commit in commit_list:
|
||||
desc = commit.GetDescription()
|
||||
if current_desc == desc:
|
||||
files.append(commit)
|
||||
@@ -376,7 +421,7 @@ def run_query(server, cfg, form_data, viewvc_link):
|
||||
return len(commit.files) > 0
|
||||
commits = filter(_only_with_files, commits)
|
||||
|
||||
return commits
|
||||
return commits, row_limit_reached
|
||||
|
||||
def main(server, cfg, viewvc_link):
|
||||
try:
|
||||
@@ -385,47 +430,42 @@ def main(server, cfg, viewvc_link):
|
||||
form_data = FormData(form)
|
||||
|
||||
if form_data.valid:
|
||||
commits = run_query(server, cfg, form_data, viewvc_link)
|
||||
commits, row_limit_reached = run_query(server, cfg,
|
||||
form_data, viewvc_link)
|
||||
query = None
|
||||
else:
|
||||
commits = [ ]
|
||||
row_limit_reached = 0
|
||||
query = 'skipped'
|
||||
|
||||
script_name = server.getenv('SCRIPT_NAME', '')
|
||||
|
||||
data = {
|
||||
docroot = cfg.options.docroot
|
||||
if docroot is None and viewvc_link:
|
||||
docroot = viewvc_link + '/' + viewvc.docroot_magic_path
|
||||
|
||||
data = ezt.TemplateData({
|
||||
'cfg' : cfg,
|
||||
'address' : cfg.general.address,
|
||||
'vsn' : viewvc.__version__,
|
||||
|
||||
'repository' : server.escape(form_data.repository, 1),
|
||||
'branch' : server.escape(form_data.branch, 1),
|
||||
'directory' : server.escape(form_data.directory, 1),
|
||||
'file' : server.escape(form_data.file, 1),
|
||||
'who' : server.escape(form_data.who, 1),
|
||||
'docroot' : cfg.options.docroot is None \
|
||||
and viewvc_link + '/' + viewvc.docroot_magic_path \
|
||||
or cfg.options.docroot,
|
||||
|
||||
'repository' : server.escape(form_data.repository),
|
||||
'branch' : server.escape(form_data.branch),
|
||||
'directory' : server.escape(form_data.directory),
|
||||
'file' : server.escape(form_data.file),
|
||||
'who' : server.escape(form_data.who),
|
||||
'docroot' : docroot,
|
||||
'sortby' : form_data.sortby,
|
||||
'date' : form_data.date,
|
||||
|
||||
'query' : query,
|
||||
'row_limit_reached' : ezt.boolean(row_limit_reached),
|
||||
'commits' : commits,
|
||||
'num_commits' : len(commits),
|
||||
'rss_href' : None,
|
||||
}
|
||||
|
||||
if form_data.hours:
|
||||
data['hours'] = form_data.hours
|
||||
else:
|
||||
data['hours'] = 2
|
||||
|
||||
server.header()
|
||||
'hours' : form_data.hours and form_data.hours or 2,
|
||||
})
|
||||
|
||||
# generate the page
|
||||
server.header()
|
||||
template = viewvc.get_view_template(cfg, "query")
|
||||
template.generate(sys.stdout, data)
|
||||
template.generate(server.file(), data)
|
||||
|
||||
except SystemExit, e:
|
||||
pass
|
||||
|
||||
104
lib/sapi.py
104
lib/sapi.py
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -20,13 +20,27 @@ import string
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import cgi
|
||||
|
||||
|
||||
# global server object. It will be either a CgiServer or a proxy to
|
||||
# an AspServer or ModPythonServer object.
|
||||
# global server object. It will be either a CgiServer, a WsgiServer,
|
||||
# or a proxy to an AspServer or ModPythonServer object.
|
||||
server = None
|
||||
|
||||
|
||||
# Simple HTML string escaping. Note that we always escape the
|
||||
# double-quote character -- ViewVC shouldn't ever need to preserve
|
||||
# that character as-is, and sometimes needs to embed escaped values
|
||||
# into HTML attributes.
|
||||
def escape(s):
|
||||
s = str(s)
|
||||
s = string.replace(s, '&', '&')
|
||||
s = string.replace(s, '>', '>')
|
||||
s = string.replace(s, '<', '<')
|
||||
s = string.replace(s, '"', """)
|
||||
return s
|
||||
|
||||
|
||||
class Server:
|
||||
def __init__(self):
|
||||
self.pageGlobals = {}
|
||||
@@ -34,6 +48,9 @@ class Server:
|
||||
def self(self):
|
||||
return self
|
||||
|
||||
def escape(self, s):
|
||||
return escape(s)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
@@ -129,9 +146,6 @@ class CgiServer(Server):
|
||||
global server
|
||||
server = self
|
||||
|
||||
global cgi
|
||||
import cgi
|
||||
|
||||
def addheader(self, name, value):
|
||||
self.headers.append((name, value))
|
||||
|
||||
@@ -158,11 +172,7 @@ class CgiServer(Server):
|
||||
if self.iis: url = fix_iis_url(self, url)
|
||||
self.addheader('Location', url)
|
||||
self.header(status='301 Moved')
|
||||
print 'This document is located <a href="%s">here</a>.' % url
|
||||
sys.exit(0)
|
||||
|
||||
def escape(self, s, quote = None):
|
||||
return cgi.escape(s, quote)
|
||||
sys.stdout.write('This document is located <a href="%s">here</a>.\n' % url)
|
||||
|
||||
def getenv(self, name, value=None):
|
||||
ret = os.environ.get(name, value)
|
||||
@@ -176,7 +186,7 @@ class CgiServer(Server):
|
||||
def FieldStorage(fp=None, headers=None, outerboundary="",
|
||||
environ=os.environ, keep_blank_values=0, strict_parsing=0):
|
||||
return cgi.FieldStorage(fp, headers, outerboundary, environ,
|
||||
keep_blank_values, strict_parsing)
|
||||
keep_blank_values, strict_parsing)
|
||||
|
||||
def write(self, s):
|
||||
sys.stdout.write(s)
|
||||
@@ -188,6 +198,63 @@ class CgiServer(Server):
|
||||
return sys.stdout
|
||||
|
||||
|
||||
class WsgiServer(Server):
|
||||
def __init__(self, environ, start_response):
|
||||
Server.__init__(self)
|
||||
|
||||
self._environ = environ
|
||||
self._start_response = start_response;
|
||||
self._headers = []
|
||||
self._wsgi_write = None
|
||||
self.headerSent = False
|
||||
|
||||
global server
|
||||
server = self
|
||||
|
||||
global cgi
|
||||
import cgi
|
||||
|
||||
def addheader(self, name, value):
|
||||
self._headers.append((name, value))
|
||||
|
||||
def header(self, content_type='text/html; charset=UTF-8', status=None):
|
||||
if not status:
|
||||
status = "200 OK"
|
||||
if not self.headerSent:
|
||||
self.headerSent = True
|
||||
self._headers.insert(0, ("Content-Type", content_type),)
|
||||
self._wsgi_write = self._start_response("%s" % status, self._headers)
|
||||
|
||||
def redirect(self, url):
|
||||
"""Redirect client to url. This discards any data that has been queued
|
||||
to be sent to the user. But there should never by any anyway.
|
||||
"""
|
||||
self.addheader('Location', url)
|
||||
self.header(status='301 Moved')
|
||||
self._wsgi_write('This document is located <a href="%s">here</a>.' % url)
|
||||
|
||||
def getenv(self, name, value=None):
|
||||
return self._environ.get(name, value)
|
||||
|
||||
def params(self):
|
||||
return cgi.parse(environ=self._environ, fp=self._environ["wsgi.input"])
|
||||
|
||||
def FieldStorage(self, fp=None, headers=None, outerboundary="",
|
||||
environ=os.environ, keep_blank_values=0, strict_parsing=0):
|
||||
return cgi.FieldStorage(self._environ["wsgi.input"], headers,
|
||||
outerboundary, self._environ, keep_blank_values,
|
||||
strict_parsing)
|
||||
|
||||
def write(self, s):
|
||||
self._wsgi_write(s)
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
def file(self):
|
||||
return File(self)
|
||||
|
||||
|
||||
class AspServer(ThreadedServer):
|
||||
def __init__(self, Server, Request, Response, Application):
|
||||
ThreadedServer.__init__(self)
|
||||
@@ -219,10 +286,6 @@ class AspServer(ThreadedServer):
|
||||
|
||||
def redirect(self, url):
|
||||
self.response.Redirect(url)
|
||||
sys.exit()
|
||||
|
||||
def escape(self, s, quote = None):
|
||||
return self.server.HTMLEncode(str(s))
|
||||
|
||||
def getenv(self, name, value = None):
|
||||
ret = self.request.ServerVariables(name)()
|
||||
@@ -285,9 +348,6 @@ class ModPythonServer(ThreadedServer):
|
||||
self.request = request
|
||||
self.headerSent = 0
|
||||
|
||||
global cgi
|
||||
import cgi
|
||||
|
||||
def addheader(self, name, value):
|
||||
self.request.headers_out.add(name, value)
|
||||
|
||||
@@ -308,11 +368,7 @@ class ModPythonServer(ThreadedServer):
|
||||
self.request.headers_out['Location'] = url
|
||||
self.request.status = mod_python.apache.HTTP_MOVED_TEMPORARILY
|
||||
self.request.write("You are being redirected to <a href=\"%s\">%s</a>"
|
||||
% (url, url))
|
||||
sys.exit()
|
||||
|
||||
def escape(self, s, quote = None):
|
||||
return cgi.escape(s, quote)
|
||||
% (url, url))
|
||||
|
||||
def getenv(self, name, value = None):
|
||||
try:
|
||||
|
||||
60
lib/vcauth/__init__.py
Normal file
60
lib/vcauth/__init__.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 2006-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
"""Generic API for implementing authorization checks employed by ViewVC."""
|
||||
|
||||
import string
|
||||
import vclib
|
||||
|
||||
|
||||
class GenericViewVCAuthorizer:
|
||||
"""Abstract class encapsulating version control authorization routines."""
|
||||
|
||||
def __init__(self, username=None, params={}):
|
||||
"""Create a GenericViewVCAuthorizer object which will be used to
|
||||
validate that USERNAME has the permissions needed to view version
|
||||
control repositories (in whole or in part). PARAMS is a
|
||||
dictionary of custom parameters for the authorizer."""
|
||||
pass
|
||||
|
||||
def check_root_access(self, rootname):
|
||||
"""Return 1 iff the associated username is permitted to read ROOTNAME."""
|
||||
pass
|
||||
|
||||
def check_universal_access(self, rootname):
|
||||
"""Return 1 if the associated username is permitted to read every
|
||||
path in the repository at every revision, 0 if the associated
|
||||
username is prohibited from reading any path in the repository, or
|
||||
None if no such determination can be made (perhaps because the
|
||||
cost of making it is too great)."""
|
||||
pass
|
||||
|
||||
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
|
||||
"""Return 1 iff the associated username is permitted to read
|
||||
revision REV of the path PATH_PARTS (of type PATHTYPE) in
|
||||
repository ROOTNAME."""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
##############################################################################
|
||||
|
||||
class ViewVCAuthorizer(GenericViewVCAuthorizer):
|
||||
"""The uber-permissive authorizer."""
|
||||
def check_root_access(self, rootname):
|
||||
return 1
|
||||
|
||||
def check_universal_access(self, rootname):
|
||||
return 1
|
||||
|
||||
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
|
||||
return 1
|
||||
53
lib/vcauth/forbidden/__init__.py
Normal file
53
lib/vcauth/forbidden/__init__.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 2006-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
import vcauth
|
||||
import vclib
|
||||
import fnmatch
|
||||
import string
|
||||
|
||||
class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
|
||||
"""A simple top-level module authorizer."""
|
||||
def __init__(self, username, params={}):
|
||||
forbidden = params.get('forbidden', '')
|
||||
self.forbidden = map(string.strip,
|
||||
filter(None, string.split(forbidden, ',')))
|
||||
|
||||
def check_root_access(self, rootname):
|
||||
return 1
|
||||
|
||||
def check_universal_access(self, rootname):
|
||||
# If there aren't any forbidden paths, we can grant universal read
|
||||
# access. Otherwise, we make no claim.
|
||||
if not self.forbidden:
|
||||
return 1
|
||||
return None
|
||||
|
||||
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
|
||||
# No path? No problem.
|
||||
if not path_parts:
|
||||
return 1
|
||||
|
||||
# Not a directory? We aren't interested.
|
||||
if pathtype != vclib.DIR:
|
||||
return 1
|
||||
|
||||
# At this point we're looking at a directory path.
|
||||
module = path_parts[0]
|
||||
default = 1
|
||||
for pat in self.forbidden:
|
||||
if pat[0] == '!':
|
||||
default = 0
|
||||
if fnmatch.fnmatchcase(module, pat[1:]):
|
||||
return 1
|
||||
elif fnmatch.fnmatchcase(module, pat):
|
||||
return 0
|
||||
return default
|
||||
65
lib/vcauth/forbiddenre/__init__.py
Normal file
65
lib/vcauth/forbiddenre/__init__.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 2008-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
import vcauth
|
||||
import vclib
|
||||
import fnmatch
|
||||
import string
|
||||
import re
|
||||
|
||||
|
||||
def _split_regexp(restr):
|
||||
"""Return a 2-tuple consisting of a compiled regular expression
|
||||
object and a boolean flag indicating if that object should be
|
||||
interpreted inversely."""
|
||||
if restr[0] == '!':
|
||||
return re.compile(restr[1:]), 1
|
||||
return re.compile(restr), 0
|
||||
|
||||
|
||||
class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
|
||||
"""A simple regular-expression-based authorizer."""
|
||||
def __init__(self, username, params={}):
|
||||
forbidden = params.get('forbiddenre', '')
|
||||
self.forbidden = map(lambda x: _split_regexp(string.strip(x)),
|
||||
filter(None, string.split(forbidden, ',')))
|
||||
|
||||
def _check_root_path_access(self, root_path):
|
||||
default = 1
|
||||
for forbidden, negated in self.forbidden:
|
||||
if negated:
|
||||
default = 0
|
||||
if forbidden.search(root_path):
|
||||
return 1
|
||||
elif forbidden.search(root_path):
|
||||
return 0
|
||||
return default
|
||||
|
||||
def check_root_access(self, rootname):
|
||||
return self._check_root_path_access(rootname)
|
||||
|
||||
def check_universal_access(self, rootname):
|
||||
# If there aren't any forbidden regexps, we can grant universal
|
||||
# read access. Otherwise, we make no claim.
|
||||
if not self.forbidden:
|
||||
return 1
|
||||
return None
|
||||
|
||||
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
|
||||
root_path = rootname
|
||||
if path_parts:
|
||||
root_path = root_path + '/' + string.join(path_parts, '/')
|
||||
if pathtype == vclib.DIR:
|
||||
root_path = root_path + '/'
|
||||
else:
|
||||
root_path = root_path + '/'
|
||||
return self._check_root_path_access(root_path)
|
||||
|
||||
270
lib/vcauth/svnauthz/__init__.py
Normal file
270
lib/vcauth/svnauthz/__init__.py
Normal file
@@ -0,0 +1,270 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 2006-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
# (c) 2006 Sergey Lapin <slapin@dataart.com>
|
||||
|
||||
import vcauth
|
||||
import string
|
||||
import os.path
|
||||
import debug
|
||||
|
||||
from ConfigParser import ConfigParser
|
||||
|
||||
class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
|
||||
"""Subversion authz authorizer module"""
|
||||
|
||||
def __init__(self, username, params={}):
|
||||
self.rootpaths = { } # {root -> { paths -> access boolean for USERNAME }}
|
||||
|
||||
# Get the authz file location from a passed-in parameter.
|
||||
self.authz_file = params.get('authzfile')
|
||||
if not self.authz_file:
|
||||
raise debug.ViewVCException("No authzfile configured")
|
||||
if not os.path.exists(self.authz_file):
|
||||
raise debug.ViewVCException("Configured authzfile file not found")
|
||||
|
||||
# See if the admin wants us to do case normalization of usernames.
|
||||
self.force_username_case = params.get('force_username_case')
|
||||
if self.force_username_case == "upper":
|
||||
self.username = username and string.upper(username) or username
|
||||
elif self.force_username_case == "lower":
|
||||
self.username = username and string.lower(username) or username
|
||||
elif not self.force_username_case:
|
||||
self.username = username
|
||||
else:
|
||||
raise debug.ViewVCException("Invalid value for force_username_case "
|
||||
"option")
|
||||
|
||||
def _get_paths_for_root(self, rootname):
|
||||
if self.rootpaths.has_key(rootname):
|
||||
return self.rootpaths[rootname]
|
||||
|
||||
paths_for_root = { }
|
||||
|
||||
# Parse the authz file, replacing ConfigParser's optionxform()
|
||||
# method with something that won't futz with the case of the
|
||||
# option names.
|
||||
cp = ConfigParser()
|
||||
cp.optionxform = lambda x: x
|
||||
try:
|
||||
cp.read(self.authz_file)
|
||||
except:
|
||||
raise debug.ViewVCException("Unable to parse configured authzfile file")
|
||||
|
||||
# Figure out if there are any aliases for the current username
|
||||
aliases = []
|
||||
if cp.has_section('aliases'):
|
||||
for alias in cp.options('aliases'):
|
||||
entry = cp.get('aliases', alias)
|
||||
if entry == self.username:
|
||||
aliases.append(alias)
|
||||
|
||||
# Figure out which groups USERNAME has a part of.
|
||||
groups = []
|
||||
if cp.has_section('groups'):
|
||||
all_groups = []
|
||||
|
||||
def _process_group(groupname):
|
||||
"""Inline function to handle groups within groups.
|
||||
|
||||
For a group to be within another group in SVN, the group
|
||||
definitions must be in the correct order in the config file.
|
||||
ie. If group A is a member of group B then group A must be
|
||||
defined before group B in the [groups] section.
|
||||
|
||||
Unfortunately, the ConfigParser class provides no way of
|
||||
finding the order in which groups were defined so, for reasons
|
||||
of practicality, this function lets you get away with them
|
||||
being defined in the wrong order. Recursion is guarded
|
||||
against though."""
|
||||
|
||||
# If we already know the user is part of this already-
|
||||
# processed group, return that fact.
|
||||
if groupname in groups:
|
||||
return 1
|
||||
# Otherwise, ensure we don't process a group twice.
|
||||
if groupname in all_groups:
|
||||
return 0
|
||||
# Store the group name in a global list so it won't be processed again
|
||||
all_groups.append(groupname)
|
||||
group_member = 0
|
||||
groupname = groupname.strip()
|
||||
entries = string.split(cp.get('groups', groupname), ',')
|
||||
for entry in entries:
|
||||
entry = string.strip(entry)
|
||||
if entry == self.username:
|
||||
group_member = 1
|
||||
break
|
||||
elif entry[0:1] == "@" and _process_group(entry[1:]):
|
||||
group_member = 1
|
||||
break
|
||||
elif entry[0:1] == "&" and entry[1:] in aliases:
|
||||
group_member = 1
|
||||
break
|
||||
if group_member:
|
||||
groups.append(groupname)
|
||||
return group_member
|
||||
|
||||
# Process the groups
|
||||
for group in cp.options('groups'):
|
||||
_process_group(group)
|
||||
|
||||
def _userspec_matches_user(userspec):
|
||||
# If there is an inversion character, recurse and return the
|
||||
# opposite result.
|
||||
if userspec[0:1] == '~':
|
||||
return not _userspec_matches_user(userspec[1:])
|
||||
|
||||
# See if the userspec applies to our current user.
|
||||
return userspec == '*' \
|
||||
or userspec == self.username \
|
||||
or (self.username is not None and userspec == "$authenticated") \
|
||||
or (self.username is None and userspec == "$anonymous") \
|
||||
or (userspec[0:1] == "@" and userspec[1:] in groups) \
|
||||
or (userspec[0:1] == "&" and userspec[1:] in aliases)
|
||||
|
||||
def _process_access_section(section):
|
||||
"""Inline function for determining user access in a single
|
||||
config secction. Return a two-tuple (ALLOW, DENY) containing
|
||||
the access determination for USERNAME in a given authz file
|
||||
SECTION (if any)."""
|
||||
|
||||
# Figure if this path is explicitly allowed or denied to USERNAME.
|
||||
allow = deny = 0
|
||||
for user in cp.options(section):
|
||||
user = string.strip(user)
|
||||
if _userspec_matches_user(user):
|
||||
# See if the 'r' permission is among the ones granted to
|
||||
# USER. If so, we can stop looking. (Entry order is not
|
||||
# relevant -- we'll use the most permissive entry, meaning
|
||||
# one 'allow' is all we need.)
|
||||
allow = string.find(cp.get(section, user), 'r') != -1
|
||||
deny = not allow
|
||||
if allow:
|
||||
break
|
||||
return allow, deny
|
||||
|
||||
# Read the other (non-"groups") sections, and figure out in which
|
||||
# repositories USERNAME or his groups have read rights. We'll
|
||||
# first check groups that have no specific repository designation,
|
||||
# then superimpose those that have a repository designation which
|
||||
# matches the one we're asking about.
|
||||
root_sections = []
|
||||
for section in cp.sections():
|
||||
|
||||
# Skip the "groups" section -- we handled that already.
|
||||
if section == 'groups':
|
||||
continue
|
||||
|
||||
if section == 'aliases':
|
||||
continue
|
||||
|
||||
# Process root-agnostic access sections; skip (but remember)
|
||||
# root-specific ones that match our root; ignore altogether
|
||||
# root-specific ones that don't match our root. While we're at
|
||||
# it, go ahead and figure out the repository path we're talking
|
||||
# about.
|
||||
if section.find(':') == -1:
|
||||
path = section
|
||||
else:
|
||||
name, path = string.split(section, ':', 1)
|
||||
if name == rootname:
|
||||
root_sections.append(section)
|
||||
continue
|
||||
|
||||
# Check for a specific access determination.
|
||||
allow, deny = _process_access_section(section)
|
||||
|
||||
# If we got an explicit access determination for this path and this
|
||||
# USERNAME, record it.
|
||||
if allow or deny:
|
||||
if path != '/':
|
||||
path = '/' + string.join(filter(None, string.split(path, '/')), '/')
|
||||
paths_for_root[path] = allow
|
||||
|
||||
# Okay. Superimpose those root-specific values now.
|
||||
for section in root_sections:
|
||||
|
||||
# Get the path again.
|
||||
name, path = string.split(section, ':', 1)
|
||||
|
||||
# Check for a specific access determination.
|
||||
allow, deny = _process_access_section(section)
|
||||
|
||||
# If we got an explicit access determination for this path and this
|
||||
# USERNAME, record it.
|
||||
if allow or deny:
|
||||
if path != '/':
|
||||
path = '/' + string.join(filter(None, string.split(path, '/')), '/')
|
||||
paths_for_root[path] = allow
|
||||
|
||||
# If the root isn't readable, there's no point in caring about all
|
||||
# the specific paths the user can't see. Just point the rootname
|
||||
# to a None paths dictionary.
|
||||
root_is_readable = 0
|
||||
for path in paths_for_root.keys():
|
||||
if paths_for_root[path]:
|
||||
root_is_readable = 1
|
||||
break
|
||||
if not root_is_readable:
|
||||
paths_for_root = None
|
||||
|
||||
self.rootpaths[rootname] = paths_for_root
|
||||
return paths_for_root
|
||||
|
||||
def check_root_access(self, rootname):
|
||||
paths = self._get_paths_for_root(rootname)
|
||||
return (paths is not None) and 1 or 0
|
||||
|
||||
def check_universal_access(self, rootname):
|
||||
paths = self._get_paths_for_root(rootname)
|
||||
if not paths: # None or empty.
|
||||
return 0
|
||||
|
||||
# Search the access determinations. If there's a mix, we can't
|
||||
# claim a universal access determination.
|
||||
found_allow = 0
|
||||
found_deny = 0
|
||||
for access in paths.values():
|
||||
if access:
|
||||
found_allow = 1
|
||||
else:
|
||||
found_deny = 1
|
||||
if found_allow and found_deny:
|
||||
return None
|
||||
|
||||
# We didn't find both allowances and denials, so we must have
|
||||
# found one or the other. Denials only is a universal denial.
|
||||
if found_deny:
|
||||
return 0
|
||||
|
||||
# ... but allowances only is only a universal allowance if read
|
||||
# access is granted to the root directory.
|
||||
if found_allow and paths.has_key('/'):
|
||||
return 1
|
||||
|
||||
# Anything else is indeterminable.
|
||||
return None
|
||||
|
||||
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
|
||||
# Crawl upward from the path represented by PATH_PARTS toward to
|
||||
# the root of the repository, looking for an explicitly grant or
|
||||
# denial of access.
|
||||
paths = self._get_paths_for_root(rootname)
|
||||
if paths is None:
|
||||
return 0
|
||||
parts = path_parts[:]
|
||||
while parts:
|
||||
path = '/' + string.join(parts, '/')
|
||||
if paths.has_key(path):
|
||||
return paths[path]
|
||||
del parts[-1]
|
||||
return paths.get('/', 0)
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -27,11 +27,43 @@ UNIFIED = 1
|
||||
CONTEXT = 2
|
||||
SIDE_BY_SIDE = 3
|
||||
|
||||
# root types returned by Repository.roottype().
|
||||
CVS = 'cvs'
|
||||
SVN = 'svn'
|
||||
|
||||
# action kinds found in ChangedPath.action
|
||||
ADDED = 'added'
|
||||
DELETED = 'deleted'
|
||||
REPLACED = 'replaced'
|
||||
MODIFIED = 'modified'
|
||||
|
||||
# log sort keys
|
||||
SORTBY_DEFAULT = 0 # default/no sorting
|
||||
SORTBY_DATE = 1 # sorted by date, youngest first
|
||||
SORTBY_REV = 2 # sorted by revision, youngest first
|
||||
|
||||
|
||||
# ======================================================================
|
||||
#
|
||||
class Repository:
|
||||
"""Abstract class representing a repository."""
|
||||
|
||||
def rootname(self):
|
||||
"""Return the name of this repository."""
|
||||
|
||||
def roottype(self):
|
||||
"""Return the type of this repository (vclib.CVS, vclib.SVN, ...)."""
|
||||
|
||||
def rootpath(self):
|
||||
"""Return the location of this repository."""
|
||||
|
||||
def authorizer(self):
|
||||
"""Return the vcauth.Authorizer object associated with this
|
||||
repository, or None if no such association has been made."""
|
||||
|
||||
def open(self):
|
||||
"""Open a connection to the repository."""
|
||||
|
||||
def itemtype(self, path_parts, rev):
|
||||
"""Return the type of the item (file or dir) at the given path and revision
|
||||
|
||||
@@ -44,7 +76,7 @@ class Repository:
|
||||
"""
|
||||
pass
|
||||
|
||||
def openfile(self, path_parts, rev):
|
||||
def openfile(self, path_parts, rev, options):
|
||||
"""Open a file object to read file contents at a given path and revision.
|
||||
|
||||
The return value is a 2-tuple of containg the file object and revision
|
||||
@@ -54,6 +86,8 @@ class Repository:
|
||||
of the repository. e.g. ["subdir1", "subdir2", "filename"]
|
||||
|
||||
rev is the revision of the file to check out
|
||||
|
||||
options is a dictionary of implementation specific options
|
||||
"""
|
||||
|
||||
def listdir(self, path_parts, rev, options):
|
||||
@@ -75,7 +109,7 @@ class Repository:
|
||||
New properties will be set on all of the DirEntry objects in the entries
|
||||
list. At the very least, a "rev" property will be set to a revision
|
||||
number or None if the entry doesn't have a number. Other properties that
|
||||
may be set include "date", "author", and "log".
|
||||
may be set include "date", "author", "log", "size", and "lockinfo".
|
||||
|
||||
The path is specified as a list of components, relative to the root
|
||||
of the repository. e.g. ["subdir1", "subdir2", "filename"]
|
||||
@@ -89,7 +123,7 @@ class Repository:
|
||||
options is a dictionary of implementation specific options
|
||||
"""
|
||||
|
||||
def itemlog(self, path_parts, rev, options):
|
||||
def itemlog(self, path_parts, rev, sortby, first, limit, options):
|
||||
"""Retrieve an item's log information
|
||||
|
||||
The result is a list of Revision objects
|
||||
@@ -99,9 +133,28 @@ class Repository:
|
||||
|
||||
rev is the revision of the item to return information about
|
||||
|
||||
sortby indicates the way in which the returned list should be
|
||||
sorted (SORTBY_DEFAULT, SORTBY_DATE, SORTBY_REV)
|
||||
|
||||
first is the 0-based index of the first Revision returned (after
|
||||
sorting, if any, has occured)
|
||||
|
||||
limit is the maximum number of returned Revisions, or 0 to return
|
||||
all available data
|
||||
|
||||
options is a dictionary of implementation specific options
|
||||
"""
|
||||
|
||||
def itemprops(self, path_parts, rev):
|
||||
"""Return a dictionary mapping property names to property values
|
||||
for properties stored on an item.
|
||||
|
||||
The path is specified as a list of components, relative to the root
|
||||
of the repository. e.g. ["subdir1", "subdir2", "filename"]
|
||||
|
||||
rev is the revision of the item to return information about.
|
||||
"""
|
||||
|
||||
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
|
||||
"""Return a diff (in GNU diff format) of two file revisions
|
||||
|
||||
@@ -117,29 +170,56 @@ class Repository:
|
||||
Return value is a python file object
|
||||
"""
|
||||
|
||||
def annotate(self, path_parts, rev):
|
||||
"""Return a list of annotate file content lines and a revision.
|
||||
def annotate(self, path_parts, rev, include_text=False):
|
||||
"""Return a list of Annotation object, sorted by their
|
||||
"line_number" components, which describe the lines of given
|
||||
version of a file.
|
||||
|
||||
The annotated lines are an collection of objects with the
|
||||
following addressable members:
|
||||
The file path is specified as a list of components, relative to
|
||||
the root of the repository. e.g. ["subdir1", "subdir2", "filename"]
|
||||
|
||||
text - raw text of a line of file contents
|
||||
line_number - line number on which the line is found
|
||||
rev - revision in which the line was last modified
|
||||
prev_rev - revision prior to 'rev'
|
||||
author - author who last modified the line
|
||||
date - date on which the line was last modified, in seconds
|
||||
since the epoch, GMT
|
||||
rev is the revision of the item to return information about.
|
||||
|
||||
If include_text is true, populate the Annotation objects' "text"
|
||||
members with the corresponding line of file content; otherwise,
|
||||
leave that member set to None."""
|
||||
|
||||
These object are sort by their line_number components.
|
||||
def revinfo(self, rev):
|
||||
"""Return information about a global revision
|
||||
|
||||
rev is the revision of the item to return information about
|
||||
|
||||
Return value is a 5-tuple containing: the date, author, log
|
||||
message, a list of ChangedPath items representing paths changed,
|
||||
and a dictionary mapping property names to property values for
|
||||
properties stored on an item.
|
||||
|
||||
Raise vclib.UnsupportedFeature if the version control system
|
||||
doesn't support a global revision concept.
|
||||
"""
|
||||
|
||||
def isexecutable(self, path_parts, rev):
|
||||
"""Return true iff a given revision of a versioned file is to be
|
||||
considered an executable program or script.
|
||||
|
||||
The path is specified as a list of components, relative to the root
|
||||
of the repository. e.g. ["subdir1", "subdir2", "filename"]
|
||||
|
||||
rev is the revision of the item to return information about
|
||||
"""
|
||||
|
||||
|
||||
|
||||
# ======================================================================
|
||||
class DirEntry:
|
||||
"Instances represent items in a directory listing"
|
||||
"""Instances represent items in a directory listing"""
|
||||
|
||||
def __init__(self, name, kind, errors=[]):
|
||||
"""Create a new DirEntry() item:
|
||||
NAME: The name of the directory entry
|
||||
KIND: The path kind of the entry (vclib.DIR, vclib.FILE)
|
||||
ERRORS: A list of error strings representing problems encountered
|
||||
while determining the other info about this entry
|
||||
"""
|
||||
self.name = name
|
||||
self.kind = kind
|
||||
self.errors = errors
|
||||
@@ -147,16 +227,17 @@ class DirEntry:
|
||||
class Revision:
|
||||
"""Instances holds information about revisions of versioned resources"""
|
||||
|
||||
"""Create a new Revision() item:
|
||||
NUMBER: Revision in an integer-based, sortable format
|
||||
STRING: Revision as a string
|
||||
DATE: Seconds since Epoch (GMT) that this revision was created
|
||||
AUTHOR: Author of the revision
|
||||
CHANGED: Lines-changed (contextual diff) information
|
||||
LOG: Log message associated with the creation of this revision
|
||||
SIZE: Size (in bytes) of this revision's fulltext (files only)
|
||||
"""
|
||||
def __init__(self, number, string, date, author, changed, log, size):
|
||||
def __init__(self, number, string, date, author, changed, log, size, lockinfo):
|
||||
"""Create a new Revision() item:
|
||||
NUMBER: Revision in an integer-based, sortable format
|
||||
STRING: Revision as a string
|
||||
DATE: Seconds since Epoch (GMT) that this revision was created
|
||||
AUTHOR: Author of the revision
|
||||
CHANGED: Lines-changed (contextual diff) information
|
||||
LOG: Log message associated with the creation of this revision
|
||||
SIZE: Size (in bytes) of this revision's fulltext (files only)
|
||||
LOCKINFO: Information about locks held on this revision
|
||||
"""
|
||||
self.number = number
|
||||
self.string = string
|
||||
self.date = date
|
||||
@@ -164,16 +245,69 @@ class Revision:
|
||||
self.changed = changed
|
||||
self.log = log
|
||||
self.size = size
|
||||
self.lockinfo = lockinfo
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.number, other.number)
|
||||
|
||||
class Annotation:
|
||||
"""Instances represent per-line file annotation information"""
|
||||
|
||||
def __init__(self, text, line_number, rev, prev_rev, author, date):
|
||||
"""Create a new Annotation() item:
|
||||
TEXT: Raw text of a line of file contents
|
||||
LINE_NUMBER: Line number on which the line is found
|
||||
REV: Revision in which the line was last modified
|
||||
PREV_REV: Revision prior to 'rev'
|
||||
AUTHOR: Author who last modified the line
|
||||
DATE: Date on which the line was last modified, in seconds since
|
||||
the epoch, GMT
|
||||
"""
|
||||
self.text = text
|
||||
self.line_number = line_number
|
||||
self.rev = rev
|
||||
self.prev_rev = prev_rev
|
||||
self.author = author
|
||||
self.date = date
|
||||
|
||||
class ChangedPath:
|
||||
"""Instances represent changes to paths"""
|
||||
|
||||
def __init__(self, path_parts, rev, pathtype, base_path_parts,
|
||||
base_rev, action, copied, text_changed, props_changed):
|
||||
"""Create a new ChangedPath() item:
|
||||
PATH_PARTS: Path that was changed
|
||||
REV: Revision represented by this change
|
||||
PATHTYPE: Type of this path (vclib.DIR, vclib.FILE, ...)
|
||||
BASE_PATH_PARTS: Previous path for this changed item
|
||||
BASE_REV: Previous revision for this changed item
|
||||
ACTION: Kind of change (vclib.ADDED, vclib.DELETED, ...)
|
||||
COPIED: Boolean -- was this path copied from elsewhere?
|
||||
TEXT_CHANGED: Boolean -- did the file's text change?
|
||||
PROPS_CHANGED: Boolean -- did the item's metadata change?
|
||||
"""
|
||||
self.path_parts = path_parts
|
||||
self.rev = rev
|
||||
self.pathtype = pathtype
|
||||
self.base_path_parts = base_path_parts
|
||||
self.base_rev = base_rev
|
||||
self.action = action
|
||||
self.copied = copied
|
||||
self.text_changed = text_changed
|
||||
self.props_changed = props_changed
|
||||
|
||||
|
||||
# ======================================================================
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
class ReposNotFound(Error):
|
||||
pass
|
||||
|
||||
class UnsupportedFeature(Error):
|
||||
pass
|
||||
|
||||
class ItemNotFound(Error):
|
||||
def __init__(self, path):
|
||||
# use '/' rather than os.sep because this is for user consumption, and
|
||||
@@ -181,6 +315,7 @@ class ItemNotFound(Error):
|
||||
if type(path) in (types.TupleType, types.ListType):
|
||||
path = string.join(path, '/')
|
||||
Error.__init__(self, path)
|
||||
|
||||
class InvalidRevision(Error):
|
||||
def __init__(self, revision=None):
|
||||
if revision is None:
|
||||
@@ -188,6 +323,9 @@ class InvalidRevision(Error):
|
||||
else:
|
||||
Error.__init__(self, "Invalid revision " + str(revision))
|
||||
|
||||
class NonTextualFileContents(Error):
|
||||
pass
|
||||
|
||||
# ======================================================================
|
||||
# Implementation code used by multiple vclib modules
|
||||
|
||||
@@ -200,12 +338,18 @@ def _diff_args(type, options):
|
||||
args = []
|
||||
if type == CONTEXT:
|
||||
if options.has_key('context'):
|
||||
args.append('--context=%i' % options['context'])
|
||||
if options['context'] is None:
|
||||
args.append('--context=-1')
|
||||
else:
|
||||
args.append('--context=%i' % options['context'])
|
||||
else:
|
||||
args.append('-c')
|
||||
elif type == UNIFIED:
|
||||
if options.has_key('context'):
|
||||
args.append('--unified=%i' % options['context'])
|
||||
if options['context'] is None:
|
||||
args.append('--unified=-1')
|
||||
else:
|
||||
args.append('--unified=%i' % options['context'])
|
||||
else:
|
||||
args.append('-u')
|
||||
elif type == SIDE_BY_SIDE:
|
||||
@@ -226,14 +370,14 @@ class _diff_fp:
|
||||
"""File object reading a diff between temporary files, cleaning up
|
||||
on close"""
|
||||
|
||||
def __init__(self, temp1, temp2, info1=None, info2=None, diff_opts=[]):
|
||||
def __init__(self, temp1, temp2, info1=None, info2=None, diff_cmd='diff', diff_opts=[]):
|
||||
self.temp1 = temp1
|
||||
self.temp2 = temp2
|
||||
args = diff_opts[:]
|
||||
if info1 and info2:
|
||||
args.extend(["-L", self._label(info1), "-L", self._label(info2)])
|
||||
args.extend([temp1, temp2])
|
||||
self.fp = popen.popen("diff", args, "r")
|
||||
self.fp = popen.popen(diff_cmd, args, "r")
|
||||
|
||||
def read(self, bytes):
|
||||
return self.fp.read(bytes)
|
||||
@@ -262,3 +406,26 @@ class _diff_fp:
|
||||
def _label(self, (path, date, rev)):
|
||||
date = date and time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date))
|
||||
return "%s\t%s\t%s" % (path, date, rev)
|
||||
|
||||
|
||||
def check_root_access(repos):
|
||||
"""Return 1 iff the associated username is permitted to read REPOS,
|
||||
as determined by consulting REPOS's Authorizer object (if any)."""
|
||||
|
||||
auth = repos.authorizer()
|
||||
if not auth:
|
||||
return 1
|
||||
return auth.check_root_access(repos.rootname())
|
||||
|
||||
def check_path_access(repos, path_parts, pathtype=None, rev=None):
|
||||
"""Return 1 iff the associated username is permitted to read
|
||||
revision REV of the path PATH_PARTS (of type PATHTYPE) in repository
|
||||
REPOS, as determined by consulting REPOS's Authorizer object (if any)."""
|
||||
|
||||
auth = repos.authorizer()
|
||||
if not auth:
|
||||
return 1
|
||||
if not pathtype:
|
||||
pathtype = repos.itemtype(path_parts, rev)
|
||||
return auth.check_path_access(repos.rootname(), path_parts, pathtype, rev)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -9,343 +9,37 @@
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
This is a Version Control library driver for locally accessible cvs-repositories.
|
||||
"""
|
||||
|
||||
import os
|
||||
import string
|
||||
import re
|
||||
import cStringIO
|
||||
import tempfile
|
||||
|
||||
import vclib
|
||||
import rcsparse
|
||||
import blame
|
||||
|
||||
### The functionality shared with bincvs should probably be moved to a
|
||||
### separate module
|
||||
from vclib.bincvs import CVSRepository, Revision, Tag, \
|
||||
_file_log, _log_path
|
||||
|
||||
class CCVSRepository(CVSRepository):
|
||||
def dirlogs(self, path_parts, rev, entries, options):
|
||||
"""see vclib.Repository.dirlogs docstring
|
||||
|
||||
rev can be a tag name or None. if set only information from revisions
|
||||
matching the tag will be retrieved
|
||||
|
||||
Option values recognized by this implementation:
|
||||
|
||||
cvs_subdirs
|
||||
boolean. true to fetch logs of the most recently modified file in each
|
||||
subdirectory
|
||||
|
||||
Option values returned by this implementation:
|
||||
|
||||
cvs_tags, cvs_branches
|
||||
lists of tag and branch names encountered in the directory
|
||||
"""
|
||||
subdirs = options.get('cvs_subdirs', 0)
|
||||
|
||||
dirpath = self._getpath(path_parts)
|
||||
alltags = { # all the tags seen in the files of this dir
|
||||
'MAIN' : '',
|
||||
'HEAD' : '1.1'
|
||||
}
|
||||
|
||||
for entry in entries:
|
||||
entry.rev = entry.date = entry.author = entry.dead = entry.log = None
|
||||
path = _log_path(entry, dirpath, subdirs)
|
||||
if path:
|
||||
entry.path = path
|
||||
try:
|
||||
rcsparse.Parser().parse(open(path, 'rb'), InfoSink(entry, rev, alltags))
|
||||
except IOError, e:
|
||||
entry.errors.append("rcsparse error: %s" % e)
|
||||
except RuntimeError, e:
|
||||
entry.errors.append("rcsparse error: %s" % e)
|
||||
except rcsparse.RCSStopParser:
|
||||
pass
|
||||
|
||||
branches = options['cvs_branches'] = []
|
||||
tags = options['cvs_tags'] = []
|
||||
for name, rev in alltags.items():
|
||||
if Tag(None, rev).is_branch:
|
||||
branches.append(name)
|
||||
else:
|
||||
tags.append(name)
|
||||
|
||||
def itemlog(self, path_parts, rev, options):
|
||||
"""see vclib.Repository.itemlog docstring
|
||||
|
||||
rev parameter can be a revision number, a branch number, a tag name,
|
||||
or None. If None, will return information about all revisions, otherwise,
|
||||
will only return information about the specified revision or branch.
|
||||
|
||||
Option values returned by this implementation:
|
||||
|
||||
cvs_tags
|
||||
dictionary of Tag objects for all tags encountered
|
||||
"""
|
||||
path = self.rcsfile(path_parts, 1)
|
||||
sink = TreeSink()
|
||||
rcsparse.Parser().parse(open(path, 'rb'), sink)
|
||||
filtered_revs = _file_log(sink.revs.values(), sink.tags,
|
||||
sink.default_branch, rev)
|
||||
for rev in filtered_revs:
|
||||
if rev.prev and len(rev.number) == 2:
|
||||
rev.changed = rev.prev.next_changed
|
||||
options['cvs_tags'] = sink.tags
|
||||
|
||||
return filtered_revs
|
||||
|
||||
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
|
||||
temp1 = tempfile.mktemp()
|
||||
open(temp1, 'wb').write(self.openfile(path_parts1, rev1)[0].getvalue())
|
||||
temp2 = tempfile.mktemp()
|
||||
open(temp2, 'wb').write(self.openfile(path_parts2, rev2)[0].getvalue())
|
||||
|
||||
r1 = self.itemlog(path_parts1, rev1, {})[-1]
|
||||
r2 = self.itemlog(path_parts2, rev2, {})[-1]
|
||||
|
||||
info1 = (self.rcsfile(path_parts1, root=1, v=0), r1.date, r1.string)
|
||||
info2 = (self.rcsfile(path_parts2, root=1, v=0), r2.date, r2.string)
|
||||
|
||||
diff_args = vclib._diff_args(type, options)
|
||||
|
||||
return vclib._diff_fp(temp1, temp2, info1, info2, diff_args)
|
||||
|
||||
def annotate(self, path_parts, rev=None):
|
||||
source = blame.BlameSource(self.rcsfile(path_parts, 1), rev)
|
||||
return source, source.revision
|
||||
|
||||
def openfile(self, path_parts, rev=None):
|
||||
path = self.rcsfile(path_parts, 1)
|
||||
sink = COSink(rev)
|
||||
rcsparse.Parser().parse(open(path, 'rb'), sink)
|
||||
revision = sink.last and sink.last.string
|
||||
return cStringIO.StringIO(string.join(sink.sstext.text, "\n")), revision
|
||||
|
||||
class MatchingSink(rcsparse.Sink):
|
||||
"""Superclass for sinks that search for revisions based on tag or number"""
|
||||
|
||||
def __init__(self, find):
|
||||
"""Initialize with tag name or revision number string to match against"""
|
||||
if not find or find == 'MAIN' or find == 'HEAD':
|
||||
self.find = None
|
||||
else:
|
||||
self.find = find
|
||||
|
||||
self.find_tag = None
|
||||
|
||||
def set_principal_branch(self, branch_number):
|
||||
if self.find is None:
|
||||
self.find_tag = Tag(None, branch_number)
|
||||
|
||||
def define_tag(self, name, revision):
|
||||
if name == self.find:
|
||||
self.find_tag = Tag(None, revision)
|
||||
|
||||
def admin_completed(self):
|
||||
if self.find_tag is None:
|
||||
if self.find is None:
|
||||
self.find_tag = Tag(None, '')
|
||||
else:
|
||||
try:
|
||||
self.find_tag = Tag(None, self.find)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
class InfoSink(MatchingSink):
|
||||
def __init__(self, entry, tag, alltags):
|
||||
MatchingSink.__init__(self, tag)
|
||||
self.entry = entry
|
||||
self.alltags = alltags
|
||||
self.matching_rev = None
|
||||
self.perfect_match = 0
|
||||
|
||||
def define_tag(self, name, revision):
|
||||
MatchingSink.define_tag(self, name, revision)
|
||||
self.alltags[name] = revision
|
||||
|
||||
def admin_completed(self):
|
||||
MatchingSink.admin_completed(self)
|
||||
if self.find_tag is None:
|
||||
# tag we're looking for doesn't exist
|
||||
raise rcsparse.RCSStopParser
|
||||
|
||||
def define_revision(self, revision, date, author, state, branches, next):
|
||||
if self.perfect_match:
|
||||
return
|
||||
|
||||
tag = self.find_tag
|
||||
rev = Revision(revision, date, author, state == "dead")
|
||||
|
||||
# perfect match if revision number matches tag number or if revision is on
|
||||
# trunk and tag points to trunk. imperfect match if tag refers to a branch
|
||||
# and this revision is the highest revision so far found on that branch
|
||||
perfect = ((rev.number == tag.number) or
|
||||
(not tag.number and len(rev.number) == 2))
|
||||
if perfect or (tag.is_branch and tag.number == rev.number[:-1] and
|
||||
(not self.matching_rev or
|
||||
rev.number > self.matching_rev.number)):
|
||||
self.matching_rev = rev
|
||||
self.perfect_match = perfect
|
||||
|
||||
def set_revision_info(self, revision, log, text):
|
||||
if self.matching_rev:
|
||||
if revision == self.matching_rev.string:
|
||||
self.entry.rev = self.matching_rev.string
|
||||
self.entry.date = self.matching_rev.date
|
||||
self.entry.author = self.matching_rev.author
|
||||
self.entry.dead = self.matching_rev.dead
|
||||
self.entry.log = log
|
||||
raise rcsparse.RCSStopParser
|
||||
else:
|
||||
raise rcsparse.RCSStopParser
|
||||
|
||||
class TreeSink(rcsparse.Sink):
|
||||
d_command = re.compile('^d(\d+)\\s(\\d+)')
|
||||
a_command = re.compile('^a(\d+)\\s(\\d+)')
|
||||
|
||||
def __init__(self):
|
||||
self.revs = { }
|
||||
self.tags = { }
|
||||
self.head = None
|
||||
self.default_branch = None
|
||||
|
||||
def set_head_revision(self, revision):
|
||||
self.head = revision
|
||||
|
||||
def set_principal_branch(self, branch_number):
|
||||
self.default_branch = branch_number
|
||||
|
||||
def define_tag(self, name, revision):
|
||||
# check !tags.has_key(tag_name)
|
||||
self.tags[name] = revision
|
||||
|
||||
def define_revision(self, revision, date, author, state, branches, next):
|
||||
# check !revs.has_key(revision)
|
||||
self.revs[revision] = Revision(revision, date, author, state == "dead")
|
||||
|
||||
def set_revision_info(self, revision, log, text):
|
||||
# check revs.has_key(revision)
|
||||
rev = self.revs[revision]
|
||||
rev.log = log
|
||||
|
||||
changed = None
|
||||
added = 0
|
||||
deled = 0
|
||||
if self.head != revision:
|
||||
changed = 1
|
||||
lines = string.split(text, '\n')
|
||||
idx = 0
|
||||
while idx < len(lines):
|
||||
command = lines[idx]
|
||||
dmatch = self.d_command.match(command)
|
||||
idx = idx + 1
|
||||
if dmatch:
|
||||
deled = deled + string.atoi(dmatch.group(2))
|
||||
else:
|
||||
amatch = self.a_command.match(command)
|
||||
if amatch:
|
||||
count = string.atoi(amatch.group(2))
|
||||
added = added + count
|
||||
idx = idx + count
|
||||
elif command:
|
||||
raise "error while parsing deltatext: %s" % command
|
||||
|
||||
if len(rev.number) == 2:
|
||||
rev.next_changed = changed and "+%i -%i" % (deled, added)
|
||||
else:
|
||||
rev.changed = changed and "+%i -%i" % (added, deled)
|
||||
|
||||
class StreamText:
|
||||
d_command = re.compile('^d(\d+)\\s(\\d+)')
|
||||
a_command = re.compile('^a(\d+)\\s(\\d+)')
|
||||
|
||||
def __init__(self, text):
|
||||
self.text = string.split(text, "\n")
|
||||
|
||||
def command(self, cmd):
|
||||
adjust = 0
|
||||
add_lines_remaining = 0
|
||||
diffs = string.split(cmd, "\n")
|
||||
if diffs[-1] == "":
|
||||
del diffs[-1]
|
||||
if len(diffs) == 0:
|
||||
return
|
||||
if diffs[0] == "":
|
||||
del diffs[0]
|
||||
for command in diffs:
|
||||
if add_lines_remaining > 0:
|
||||
# Insertion lines from a prior "a" command
|
||||
self.text.insert(start_line + adjust, command)
|
||||
add_lines_remaining = add_lines_remaining - 1
|
||||
adjust = adjust + 1
|
||||
continue
|
||||
dmatch = self.d_command.match(command)
|
||||
amatch = self.a_command.match(command)
|
||||
if dmatch:
|
||||
# "d" - Delete command
|
||||
start_line = string.atoi(dmatch.group(1))
|
||||
count = string.atoi(dmatch.group(2))
|
||||
begin = start_line + adjust - 1
|
||||
del self.text[begin:begin + count]
|
||||
adjust = adjust - count
|
||||
elif amatch:
|
||||
# "a" - Add command
|
||||
start_line = string.atoi(amatch.group(1))
|
||||
count = string.atoi(amatch.group(2))
|
||||
add_lines_remaining = count
|
||||
else:
|
||||
raise RuntimeError, 'Error parsing diff commands'
|
||||
|
||||
def secondnextdot(s, start):
|
||||
# find the position the second dot after the start index.
|
||||
return string.find(s, '.', string.find(s, '.', start) + 1)
|
||||
import os.path
|
||||
|
||||
|
||||
class COSink(MatchingSink):
|
||||
def __init__(self, rev):
|
||||
MatchingSink.__init__(self, rev)
|
||||
def canonicalize_rootpath(rootpath):
|
||||
assert os.path.isabs(rootpath)
|
||||
return os.path.normpath(rootpath)
|
||||
|
||||
def set_head_revision(self, revision):
|
||||
self.head = Revision(revision)
|
||||
self.last = None
|
||||
self.sstext = None
|
||||
|
||||
def admin_completed(self):
|
||||
MatchingSink.admin_completed(self)
|
||||
if self.find_tag is None:
|
||||
raise vclib.InvalidRevision(self.find)
|
||||
def expand_root_parent(parent_path):
|
||||
# Each subdirectory of PARENT_PATH that contains a child
|
||||
# "CVSROOT/config" is added the set of returned roots. Or, if the
|
||||
# PARENT_PATH itself contains a child "CVSROOT/config", then all its
|
||||
# subdirectories are returned as roots.
|
||||
assert os.path.isabs(parent_path)
|
||||
roots = {}
|
||||
subpaths = os.listdir(parent_path)
|
||||
cvsroot = os.path.exists(os.path.join(parent_path, "CVSROOT", "config"))
|
||||
for rootname in subpaths:
|
||||
rootpath = os.path.join(parent_path, rootname)
|
||||
if cvsroot \
|
||||
or (os.path.exists(os.path.join(rootpath, "CVSROOT", "config"))):
|
||||
roots[rootname] = canonicalize_rootpath(rootpath)
|
||||
return roots
|
||||
|
||||
def set_revision_info(self, revision, log, text):
|
||||
tag = self.find_tag
|
||||
rev = Revision(revision)
|
||||
|
||||
if rev.number == tag.number:
|
||||
self.log = log
|
||||
|
||||
depth = len(rev.number)
|
||||
|
||||
if rev.number == self.head.number:
|
||||
assert self.sstext is None
|
||||
self.sstext = StreamText(text)
|
||||
elif (depth == 2 and tag.number and rev.number >= tag.number[:depth]):
|
||||
assert len(self.last.number) == 2
|
||||
assert rev.number < self.last.number
|
||||
self.sstext.command(text)
|
||||
elif (depth > 2 and rev.number[:depth-1] == tag.number[:depth-1] and
|
||||
(rev.number <= tag.number or len(tag.number) == depth-1)):
|
||||
assert len(rev.number) - len(self.last.number) in (0, 2)
|
||||
assert rev.number > self.last.number
|
||||
self.sstext.command(text)
|
||||
else:
|
||||
rev = None
|
||||
|
||||
if rev:
|
||||
#print "tag =", tag.number, "rev =", rev.number, "<br>"
|
||||
self.last = rev
|
||||
def CVSRepository(name, rootpath, authorizer, utilities, use_rcsparse):
|
||||
rootpath = canonicalize_rootpath(rootpath)
|
||||
if use_rcsparse:
|
||||
import ccvs
|
||||
return ccvs.CCVSRepository(name, rootpath, authorizer, utilities)
|
||||
else:
|
||||
import bincvs
|
||||
return bincvs.BinCVSRepository(name, rootpath, authorizer, utilities)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -13,6 +13,7 @@
|
||||
"Version Control lib driver for locally accessible cvs-repositories."
|
||||
|
||||
import vclib
|
||||
import vcauth
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
@@ -25,50 +26,96 @@ import time
|
||||
import compat
|
||||
import popen
|
||||
|
||||
class CVSRepository(vclib.Repository):
|
||||
def __init__(self, name, rootpath):
|
||||
class BaseCVSRepository(vclib.Repository):
|
||||
def __init__(self, name, rootpath, authorizer, utilities):
|
||||
if not os.path.isdir(rootpath):
|
||||
raise vclib.ReposNotFound(name)
|
||||
|
||||
raise vclib.ReposNotFound(name)
|
||||
|
||||
self.name = name
|
||||
self.rootpath = rootpath
|
||||
self.auth = authorizer
|
||||
self.utilities = utilities
|
||||
|
||||
# See if this repository is even viewable, authz-wise.
|
||||
if not vclib.check_root_access(self):
|
||||
raise vclib.ReposNotFound(name)
|
||||
|
||||
def open(self):
|
||||
# See if a universal read access determination can be made.
|
||||
if self.auth and self.auth.check_universal_access(self.name) == 1:
|
||||
self.auth = None
|
||||
|
||||
def rootname(self):
|
||||
return self.name
|
||||
|
||||
def rootpath(self):
|
||||
return self.rootpath
|
||||
|
||||
def roottype(self):
|
||||
return vclib.CVS
|
||||
|
||||
def authorizer(self):
|
||||
return self.auth
|
||||
|
||||
def itemtype(self, path_parts, rev):
|
||||
basepath = self._getpath(path_parts)
|
||||
kind = None
|
||||
if os.path.isdir(basepath):
|
||||
return vclib.DIR
|
||||
if os.path.isfile(basepath + ',v'):
|
||||
return vclib.FILE
|
||||
atticpath = self._getpath(self._atticpath(path_parts))
|
||||
if os.path.isfile(atticpath + ',v'):
|
||||
return vclib.FILE
|
||||
raise vclib.ItemNotFound(path_parts)
|
||||
kind = vclib.DIR
|
||||
elif os.path.isfile(basepath + ',v'):
|
||||
kind = vclib.FILE
|
||||
else:
|
||||
atticpath = self._getpath(self._atticpath(path_parts))
|
||||
if os.path.isfile(atticpath + ',v'):
|
||||
kind = vclib.FILE
|
||||
if not kind:
|
||||
raise vclib.ItemNotFound(path_parts)
|
||||
if not vclib.check_path_access(self, path_parts, kind, rev):
|
||||
raise vclib.ItemNotFound(path_parts)
|
||||
return kind
|
||||
|
||||
def itemprops(self, path_parts, rev):
|
||||
self.itemtype(path_parts, rev) # does auth-check
|
||||
return {} # CVS doesn't support properties
|
||||
|
||||
def listdir(self, path_parts, rev, options):
|
||||
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a directory."
|
||||
% (string.join(path_parts, "/")))
|
||||
|
||||
# Only RCS files (*,v) and subdirs are returned.
|
||||
data = [ ]
|
||||
|
||||
full_name = self._getpath(path_parts)
|
||||
for file in os.listdir(full_name):
|
||||
name = None
|
||||
kind, errors = _check_path(os.path.join(full_name, file))
|
||||
if kind == vclib.FILE:
|
||||
if file[-2:] == ',v':
|
||||
data.append(CVSDirEntry(file[:-2], kind, errors, 0))
|
||||
name = file[:-2]
|
||||
elif kind == vclib.DIR:
|
||||
if file != 'Attic' and file != 'CVS': # CVS directory is for fileattr
|
||||
data.append(CVSDirEntry(file, kind, errors, 0))
|
||||
name = file
|
||||
else:
|
||||
data.append(CVSDirEntry(file, kind, errors, 0))
|
||||
name = file
|
||||
if not name:
|
||||
continue
|
||||
if vclib.check_path_access(self, path_parts + [name], kind, rev):
|
||||
data.append(CVSDirEntry(name, kind, errors, 0))
|
||||
|
||||
full_name = os.path.join(full_name, 'Attic')
|
||||
if os.path.isdir(full_name):
|
||||
for file in os.listdir(full_name):
|
||||
name = None
|
||||
kind, errors = _check_path(os.path.join(full_name, file))
|
||||
if kind == vclib.FILE:
|
||||
if file[-2:] == ',v':
|
||||
data.append(CVSDirEntry(file[:-2], kind, errors, 1))
|
||||
name = file[:-2]
|
||||
elif kind != vclib.DIR:
|
||||
data.append(CVSDirEntry(file, kind, errors, 1))
|
||||
name = file
|
||||
if not name:
|
||||
continue
|
||||
if vclib.check_path_access(self, path_parts + [name], kind, rev):
|
||||
data.append(CVSDirEntry(name, kind, errors, 1))
|
||||
|
||||
return data
|
||||
|
||||
@@ -88,48 +135,61 @@ class CVSRepository(vclib.Repository):
|
||||
ret_file = self._getpath(ret_parts)
|
||||
if not os.path.isfile(ret_file + ',v'):
|
||||
raise vclib.ItemNotFound(path_parts)
|
||||
|
||||
if root:
|
||||
ret = ret_file
|
||||
else:
|
||||
ret = string.join(ret_parts, "/")
|
||||
|
||||
if v:
|
||||
ret = ret + ",v"
|
||||
|
||||
return ret
|
||||
|
||||
class BinCVSRepository(CVSRepository):
|
||||
def __init__(self, name, rootpath, rcs_paths):
|
||||
CVSRepository.__init__(self, name, rootpath)
|
||||
self.rcs_paths = rcs_paths
|
||||
def isexecutable(self, path_parts, rev):
|
||||
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file."
|
||||
% (string.join(path_parts, "/")))
|
||||
rcsfile = self.rcsfile(path_parts, 1)
|
||||
return os.access(rcsfile, os.X_OK)
|
||||
|
||||
|
||||
class BinCVSRepository(BaseCVSRepository):
|
||||
def _get_tip_revision(self, rcs_file, rev=None):
|
||||
"""Get the (basically) youngest revision (filtered by REV)."""
|
||||
args = rcs_file,
|
||||
fp = self.rcs_popen('rlog', args, 'rt', 0)
|
||||
filename, default_branch, tags, msg, eof = _parse_log_header(fp)
|
||||
filename, default_branch, tags, lockinfo, msg, eof = _parse_log_header(fp)
|
||||
revs = []
|
||||
while not eof:
|
||||
revision, eof = _parse_log_entry(fp)
|
||||
if revision:
|
||||
revs.append(revision)
|
||||
revs = _file_log(revs, tags, default_branch, rev)
|
||||
revs = _file_log(revs, tags, lockinfo, default_branch, rev)
|
||||
if revs:
|
||||
return revs[-1]
|
||||
return None
|
||||
|
||||
def openfile(self, path_parts, rev):
|
||||
def openfile(self, path_parts, rev, options):
|
||||
"""see vclib.Repository.openfile docstring
|
||||
|
||||
Option values recognized by this implementation:
|
||||
|
||||
cvs_oldkeywords
|
||||
boolean. true to use the original keyword substitution values.
|
||||
"""
|
||||
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file."
|
||||
% (string.join(path_parts, "/")))
|
||||
if not rev or rev == 'HEAD' or rev == 'MAIN':
|
||||
rev_flag = '-p'
|
||||
else:
|
||||
rev_flag = '-p' + rev
|
||||
if options.get('cvs_oldkeywords', 0):
|
||||
kv_flag = '-ko'
|
||||
else:
|
||||
kv_flag = '-kkv'
|
||||
full_name = self.rcsfile(path_parts, root=1, v=0)
|
||||
|
||||
used_rlog = 0
|
||||
tip_rev = None # used only if we have to fallback to using rlog
|
||||
|
||||
fp = self.rcs_popen('co', (rev_flag, full_name), 'rb')
|
||||
fp = self.rcs_popen('co', (kv_flag, rev_flag, full_name), 'rb')
|
||||
try:
|
||||
filename, revision = _parse_co_header(fp)
|
||||
except COMissingRevision:
|
||||
@@ -189,11 +249,16 @@ class BinCVSRepository(CVSRepository):
|
||||
cvs_tags, cvs_branches
|
||||
lists of tag and branch names encountered in the directory
|
||||
"""
|
||||
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a directory."
|
||||
% (string.join(path_parts, "/")))
|
||||
|
||||
subdirs = options.get('cvs_subdirs', 0)
|
||||
|
||||
dirpath = self._getpath(path_parts)
|
||||
alltags = _get_logs(self, dirpath, entries, rev, subdirs)
|
||||
|
||||
entries_to_fetch = []
|
||||
for entry in entries:
|
||||
if vclib.check_path_access(self, path_parts + [entry.name], None, rev):
|
||||
entries_to_fetch.append(entry)
|
||||
alltags = _get_logs(self, path_parts, entries_to_fetch, rev, subdirs)
|
||||
branches = options['cvs_branches'] = []
|
||||
tags = options['cvs_tags'] = []
|
||||
for name, rev in alltags.items():
|
||||
@@ -202,7 +267,7 @@ class BinCVSRepository(CVSRepository):
|
||||
else:
|
||||
tags.append(name)
|
||||
|
||||
def itemlog(self, path_parts, rev, options):
|
||||
def itemlog(self, path_parts, rev, sortby, first, limit, options):
|
||||
"""see vclib.Repository.itemlog docstring
|
||||
|
||||
rev parameter can be a revision number, a branch number, a tag name,
|
||||
@@ -222,6 +287,10 @@ class BinCVSRepository(CVSRepository):
|
||||
dictionary of Tag objects for all tags encountered
|
||||
"""
|
||||
|
||||
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file."
|
||||
% (string.join(path_parts, "/")))
|
||||
|
||||
# Invoke rlog
|
||||
rcsfile = self.rcsfile(path_parts, 1)
|
||||
if rev and options.get('cvs_pass_rev', 0):
|
||||
@@ -230,7 +299,7 @@ class BinCVSRepository(CVSRepository):
|
||||
args = rcsfile,
|
||||
|
||||
fp = self.rcs_popen('rlog', args, 'rt', 0)
|
||||
filename, default_branch, tags, msg, eof = _parse_log_header(fp)
|
||||
filename, default_branch, tags, lockinfo, msg, eof = _parse_log_header(fp)
|
||||
|
||||
# Retrieve revision objects
|
||||
revs = []
|
||||
@@ -239,26 +308,42 @@ class BinCVSRepository(CVSRepository):
|
||||
if revision:
|
||||
revs.append(revision)
|
||||
|
||||
filtered_revs = _file_log(revs, tags, default_branch, rev)
|
||||
filtered_revs = _file_log(revs, tags, lockinfo, default_branch, rev)
|
||||
|
||||
options['cvs_tags'] = tags
|
||||
if sortby == vclib.SORTBY_DATE:
|
||||
filtered_revs.sort(_logsort_date_cmp)
|
||||
elif sortby == vclib.SORTBY_REV:
|
||||
filtered_revs.sort(_logsort_rev_cmp)
|
||||
|
||||
if len(filtered_revs) < first:
|
||||
return []
|
||||
if limit:
|
||||
return filtered_revs[first:first+limit]
|
||||
return filtered_revs
|
||||
|
||||
def rcs_popen(self, rcs_cmd, rcs_args, mode, capture_err=1):
|
||||
if self.rcs_paths.cvsnt_exe_path:
|
||||
cmd = self.rcs_paths.cvsnt_exe_path
|
||||
if self.utilities.cvsnt:
|
||||
cmd = self.utilities.cvsnt
|
||||
args = ['rcsfile', rcs_cmd]
|
||||
args.extend(list(rcs_args))
|
||||
else:
|
||||
cmd = os.path.join(self.rcs_paths.rcs_path, rcs_cmd)
|
||||
cmd = os.path.join(self.utilities.rcs_dir, rcs_cmd)
|
||||
args = rcs_args
|
||||
return popen.popen(cmd, args, mode, capture_err)
|
||||
|
||||
def annotate(self, path_parts, rev=None):
|
||||
def annotate(self, path_parts, rev=None, include_text=False):
|
||||
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file."
|
||||
% (string.join(path_parts, "/")))
|
||||
|
||||
from vclib.ccvs import blame
|
||||
source = blame.BlameSource(self.rcsfile(path_parts, 1), rev)
|
||||
source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, include_text)
|
||||
return source, source.revision
|
||||
|
||||
def revinfo(self, rev):
|
||||
raise vclib.UnsupportedFeature
|
||||
|
||||
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
|
||||
"""see vclib.Repository.rawdiff docstring
|
||||
|
||||
@@ -266,6 +351,13 @@ class BinCVSRepository(CVSRepository):
|
||||
|
||||
ignore_keyword_subst - boolean, ignore keyword substitution
|
||||
"""
|
||||
if self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file."
|
||||
% (string.join(path_parts1, "/")))
|
||||
if self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file."
|
||||
% (string.join(path_parts2, "/")))
|
||||
|
||||
args = vclib._diff_args(type, options)
|
||||
if options.get('ignore_keyword_subst', 0):
|
||||
args.append('-kk')
|
||||
@@ -286,15 +378,16 @@ class BinCVSRepository(CVSRepository):
|
||||
|
||||
|
||||
class CVSDirEntry(vclib.DirEntry):
|
||||
def __init__(self, name, kind, errors, in_attic):
|
||||
def __init__(self, name, kind, errors, in_attic, absent=0):
|
||||
vclib.DirEntry.__init__(self, name, kind, errors)
|
||||
self.in_attic = in_attic
|
||||
self.absent = absent # meaning, no revisions found on requested tag
|
||||
|
||||
class Revision(vclib.Revision):
|
||||
def __init__(self, revstr, date=None, author=None, dead=None,
|
||||
changed=None, log=None):
|
||||
vclib.Revision.__init__(self, _revision_tuple(revstr), revstr,
|
||||
date, author, changed, log, None)
|
||||
date, author, changed, log, None, None)
|
||||
self.dead = dead
|
||||
|
||||
class Tag:
|
||||
@@ -307,6 +400,14 @@ class Tag:
|
||||
# ======================================================================
|
||||
# Functions for dealing with Revision and Tag objects
|
||||
|
||||
def _logsort_date_cmp(rev1, rev2):
|
||||
# sort on date; secondary on revision number
|
||||
return -cmp(rev1.date, rev2.date) or -cmp(rev1.number, rev2.number)
|
||||
|
||||
def _logsort_rev_cmp(rev1, rev2):
|
||||
# sort highest revision first
|
||||
return -cmp(rev1.number, rev2.number)
|
||||
|
||||
def _match_revs_tags(revlist, taglist):
|
||||
"""Match up a list of Revision objects with a list of Tag objects
|
||||
|
||||
@@ -534,31 +635,25 @@ def _parse_co_header(fp):
|
||||
raise COMalformedOutput, "Unable to find filename in co output stream"
|
||||
filename = match.group(1)
|
||||
|
||||
# look for a revision in the second line.
|
||||
line = fp.readline()
|
||||
if not line:
|
||||
raise COMalformedOutput, "Missing second line from co output stream"
|
||||
match = _re_co_revision.match(line)
|
||||
if match:
|
||||
return filename, match.group(1)
|
||||
elif _re_co_missing_rev.match(line) or _re_co_side_branches.match(line):
|
||||
raise COMissingRevision, "Got missing revision error from co output stream"
|
||||
elif _re_co_warning.match(line):
|
||||
pass
|
||||
else:
|
||||
raise COMalformedOutput, "Unable to find revision in co output stream"
|
||||
# look through subsequent lines for a revision. we might encounter
|
||||
# some ignorable or problematic lines along the way.
|
||||
while 1:
|
||||
line = fp.readline()
|
||||
if not line:
|
||||
break
|
||||
# look for a revision.
|
||||
match = _re_co_revision.match(line)
|
||||
if match:
|
||||
return filename, match.group(1)
|
||||
elif _re_co_missing_rev.match(line) or _re_co_side_branches.match(line):
|
||||
raise COMissingRevision, "Got missing revision error from co output stream"
|
||||
elif _re_co_warning.match(line):
|
||||
pass
|
||||
else:
|
||||
break
|
||||
|
||||
# if we get here, the second line wasn't a revision, but it was a
|
||||
# warning we can ignore. look for a revision in the third line.
|
||||
line = fp.readline()
|
||||
if not line:
|
||||
raise COMalformedOutput, "Missing third line from co output stream"
|
||||
match = _re_co_revision.match(line)
|
||||
if match:
|
||||
return filename, match.group(1)
|
||||
raise COMalformedOutput, "Unable to find revision in co output stream"
|
||||
|
||||
|
||||
# if your rlog doesn't use 77 '=' characters, then this must change
|
||||
LOG_END_MARKER = '=' * 77 + '\n'
|
||||
ENTRY_END_MARKER = '-' * 28 + '\n'
|
||||
@@ -603,13 +698,14 @@ def _parse_log_header(fp):
|
||||
If there is no revision information (e.g. the "-h" switch was passed to
|
||||
rlog), then fp will consumed the file separator line on exit.
|
||||
|
||||
Returns: filename, default branch, tag dictionary, rlog error message,
|
||||
and eof flag
|
||||
Returns: filename, default branch, tag dictionary, lock dictionary,
|
||||
rlog error message, and eof flag
|
||||
"""
|
||||
|
||||
filename = head = branch = msg = ""
|
||||
taginfo = { } # tag name => number
|
||||
|
||||
parsing_tags = 0
|
||||
taginfo = { } # tag name => number
|
||||
lockinfo = { } # revision => locker
|
||||
state = 0 # 0 = base, 1 = parsing symbols, 2 = parsing locks
|
||||
eof = None
|
||||
|
||||
while 1:
|
||||
@@ -619,24 +715,35 @@ def _parse_log_header(fp):
|
||||
eof = _EOF_LOG
|
||||
break
|
||||
|
||||
if parsing_tags:
|
||||
if state == 1:
|
||||
if line[0] == '\t':
|
||||
[ tag, rev ] = map(string.strip, string.split(line, ':'))
|
||||
taginfo[tag] = rev
|
||||
else:
|
||||
# oops. this line isn't tag info. stop parsing tags.
|
||||
parsing_tags = 0
|
||||
state = 0
|
||||
|
||||
if not parsing_tags:
|
||||
if state == 2:
|
||||
if line[0] == '\t':
|
||||
[ locker, rev ] = map(string.strip, string.split(line, ':'))
|
||||
lockinfo[rev] = locker
|
||||
else:
|
||||
# oops. this line isn't lock info. stop parsing tags.
|
||||
state = 0
|
||||
|
||||
if state == 0:
|
||||
if line[:9] == 'RCS file:':
|
||||
filename = line[10:-1]
|
||||
elif line[:5] == 'head:':
|
||||
head = line[6:-1]
|
||||
elif line[:7] == 'branch:':
|
||||
branch = line[8:-1]
|
||||
elif line[:6] == 'locks:':
|
||||
# start parsing the lock information
|
||||
state = 2
|
||||
elif line[:14] == 'symbolic names':
|
||||
# start parsing the tag information
|
||||
parsing_tags = 1
|
||||
state = 1
|
||||
elif line == ENTRY_END_MARKER:
|
||||
# end of the headers
|
||||
break
|
||||
@@ -665,7 +772,7 @@ def _parse_log_header(fp):
|
||||
eof = _EOF_ERROR
|
||||
break
|
||||
|
||||
return filename, branch, taginfo, msg, eof
|
||||
return filename, branch, taginfo, lockinfo, msg, eof
|
||||
|
||||
_re_log_info = re.compile(r'^date:\s+([^;]+);'
|
||||
r'\s+author:\s+([^;]+);'
|
||||
@@ -765,7 +872,7 @@ def _paths_eq(path1, path2):
|
||||
# ======================================================================
|
||||
# Functions for interpreting and manipulating log information
|
||||
|
||||
def _file_log(revs, taginfo, cur_branch, filter):
|
||||
def _file_log(revs, taginfo, lockinfo, cur_branch, filter):
|
||||
"""Augment list of Revisions and a dictionary of Tags"""
|
||||
|
||||
# Add artificial ViewVC tag MAIN. If the file has a default branch, then
|
||||
@@ -796,6 +903,10 @@ def _file_log(revs, taginfo, cur_branch, filter):
|
||||
# Match up tags and revisions
|
||||
_match_revs_tags(revs, tags)
|
||||
|
||||
# Match up lockinfo and revision
|
||||
for rev in revs:
|
||||
rev.lockinfo = lockinfo.get(rev.string)
|
||||
|
||||
# Add artificial ViewVC tag HEAD, which acts like a non-branch tag pointing
|
||||
# at the latest revision on the MAIN branch. The HEAD revision doesn't have
|
||||
# anything to do with the "head" revision number specified in the RCS file
|
||||
@@ -842,7 +953,7 @@ def _file_log(revs, taginfo, cur_branch, filter):
|
||||
|
||||
return filtered_revs
|
||||
|
||||
def _get_logs(repos, dirpath, entries, view_tag, get_dirs):
|
||||
def _get_logs(repos, dir_path_parts, entries, view_tag, get_dirs):
|
||||
alltags = { # all the tags seen in the files of this dir
|
||||
'MAIN' : '',
|
||||
'HEAD' : '1.1'
|
||||
@@ -857,14 +968,15 @@ def _get_logs(repos, dirpath, entries, view_tag, get_dirs):
|
||||
|
||||
while len(chunk) < max_args and entries_idx < entries_len:
|
||||
entry = entries[entries_idx]
|
||||
path = _log_path(entry, dirpath, get_dirs)
|
||||
path = _log_path(entry, repos._getpath(dir_path_parts), get_dirs)
|
||||
if path:
|
||||
entry.path = path
|
||||
entry.idx = entries_idx
|
||||
chunk.append(entry)
|
||||
|
||||
# set properties even if we don't retrieve logs
|
||||
entry.rev = entry.date = entry.author = entry.dead = entry.log = None
|
||||
entry.rev = entry.date = entry.author = None
|
||||
entry.dead = entry.log = entry.lockinfo = None
|
||||
|
||||
entries_idx = entries_idx + 1
|
||||
|
||||
@@ -884,7 +996,8 @@ def _get_logs(repos, dirpath, entries, view_tag, get_dirs):
|
||||
chunk_idx = 0
|
||||
while chunk_idx < len(chunk):
|
||||
file = chunk[chunk_idx]
|
||||
filename, default_branch, taginfo, msg, eof = _parse_log_header(rlog)
|
||||
filename, default_branch, taginfo, lockinfo, msg, eof \
|
||||
= _parse_log_header(rlog)
|
||||
|
||||
if eof == _EOF_LOG:
|
||||
# the rlog output ended early. this can happen on errors that rlog
|
||||
@@ -923,16 +1036,16 @@ def _get_logs(repos, dirpath, entries, view_tag, get_dirs):
|
||||
file.errors.append("rlog error: %s" % msg)
|
||||
continue
|
||||
|
||||
tag = None
|
||||
if view_tag == 'MAIN' or view_tag == 'HEAD':
|
||||
tag = Tag(None, default_branch)
|
||||
elif taginfo.has_key(view_tag):
|
||||
tag = Tag(None, taginfo[view_tag])
|
||||
elif view_tag:
|
||||
# the tag wasn't found, so skip this file
|
||||
elif view_tag and (eof != _EOF_FILE):
|
||||
# the tag wasn't found, so skip this file (unless we already
|
||||
# know there's nothing left of it to read)
|
||||
_skip_file(rlog)
|
||||
eof = 1
|
||||
else:
|
||||
tag = None
|
||||
eof = _EOF_FILE
|
||||
|
||||
# we don't care about the specific values -- just the keys and whether
|
||||
# the values point to branches or revisions. this the fastest way to
|
||||
@@ -972,13 +1085,16 @@ def _get_logs(repos, dirpath, entries, view_tag, get_dirs):
|
||||
file.date = wanted_entry.date
|
||||
file.author = wanted_entry.author
|
||||
file.dead = file.kind == vclib.FILE and wanted_entry.dead
|
||||
file.absent = 0
|
||||
file.log = wanted_entry.log
|
||||
file.lockinfo = lockinfo.get(file.rev)
|
||||
# suppress rlog errors if we find a usable revision in the end
|
||||
del file.errors[:]
|
||||
elif file.kind == vclib.FILE:
|
||||
file.dead = 1
|
||||
file.errors.append("No revisions exist on %s" % (view_tag or "MAIN"))
|
||||
|
||||
file.dead = 0
|
||||
#file.errors.append("No revisions exist on %s" % (view_tag or "MAIN"))
|
||||
file.absent = 1
|
||||
|
||||
# done with this file now, skip the rest of this file's revisions
|
||||
if not eof:
|
||||
_skip_file(rlog)
|
||||
@@ -1090,6 +1206,8 @@ def _newest_file(dirpath):
|
||||
newest_file = None
|
||||
newest_time = 0
|
||||
|
||||
### FIXME: This sucker is leaking unauthorized paths! ###
|
||||
|
||||
for subfile in os.listdir(dirpath):
|
||||
### filter CVS locks? stale NFS handles?
|
||||
if subfile[-2:] != ',v':
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 2000 Curt Hagenlocher <curt@hagenlocher.org>
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
@@ -31,6 +31,7 @@ import re
|
||||
import time
|
||||
import math
|
||||
import rcsparse
|
||||
import vclib
|
||||
|
||||
class CVSParser(rcsparse.Sink):
|
||||
# Precompiled regular expressions
|
||||
@@ -267,7 +268,7 @@ class CVSParser(rcsparse.Sink):
|
||||
raise RuntimeError, ('error: %s appeared to be under CVS control, ' +
|
||||
'but the RCS file is inaccessible.') % rcs_pathname
|
||||
|
||||
rcsparse.Parser().parse(rcsfile, self)
|
||||
rcsparse.parse(rcsfile, self)
|
||||
rcsfile.close()
|
||||
|
||||
if opt_rev in [None, '', 'HEAD']:
|
||||
@@ -346,7 +347,7 @@ class CVSParser(rcsparse.Sink):
|
||||
is_trunk_revision = self.trunk_rev.match(revision) is not None
|
||||
|
||||
if is_trunk_revision:
|
||||
diffs = self.deltatext_split(last_revision)
|
||||
diffs = self.deltatext_split(last_revision)
|
||||
|
||||
# Revisions on the trunk specify deltas that transform a
|
||||
# revision into an earlier revision, so invert the translation
|
||||
@@ -379,7 +380,7 @@ class CVSParser(rcsparse.Sink):
|
||||
# the trunk. They specify deltas that transform a revision
|
||||
# into a later revision.
|
||||
adjust = 0
|
||||
diffs = self.deltatext_split(revision)
|
||||
diffs = self.deltatext_split(revision)
|
||||
for command in diffs:
|
||||
if skip > 0:
|
||||
skip = skip - 1
|
||||
@@ -413,7 +414,7 @@ class CVSParser(rcsparse.Sink):
|
||||
|
||||
|
||||
class BlameSource:
|
||||
def __init__(self, rcs_file, opt_rev=None):
|
||||
def __init__(self, rcs_file, opt_rev=None, include_text=False):
|
||||
# Parse the CVS file
|
||||
parser = CVSParser()
|
||||
revision = parser.parse_cvs_file(rcs_file, opt_rev)
|
||||
@@ -427,6 +428,7 @@ class BlameSource:
|
||||
self.lines = lines
|
||||
self.num_lines = count
|
||||
self.parser = parser
|
||||
self.include_text = include_text
|
||||
|
||||
# keep track of where we are during an iteration
|
||||
self.idx = -1
|
||||
@@ -446,9 +448,10 @@ class BlameSource:
|
||||
line_number = idx + 1
|
||||
author = self.parser.revision_author[rev]
|
||||
thisline = self.lines[idx]
|
||||
if not self.include_text:
|
||||
thisline = None
|
||||
### TODO: Put a real date in here.
|
||||
item = _item(text=thisline, line_number=line_number, rev=rev,
|
||||
prev_rev=prev_rev, author=author, date=None)
|
||||
item = vclib.Annotation(thisline, line_number, rev, prev_rev, author, None)
|
||||
self.last = item
|
||||
self.idx = idx
|
||||
return item
|
||||
@@ -456,9 +459,3 @@ class BlameSource:
|
||||
|
||||
class BlameSequencingError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class _item:
|
||||
def __init__(self, **kw):
|
||||
vars(self).update(kw)
|
||||
|
||||
|
||||
398
lib/vclib/ccvs/ccvs.py
Normal file
398
lib/vclib/ccvs/ccvs.py
Normal file
@@ -0,0 +1,398 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import string
|
||||
import re
|
||||
import cStringIO
|
||||
import tempfile
|
||||
|
||||
import vclib
|
||||
import rcsparse
|
||||
import blame
|
||||
|
||||
### The functionality shared with bincvs should probably be moved to a
|
||||
### separate module
|
||||
from bincvs import BaseCVSRepository, Revision, Tag, _file_log, _log_path, _logsort_date_cmp, _logsort_rev_cmp
|
||||
|
||||
class CCVSRepository(BaseCVSRepository):
|
||||
def dirlogs(self, path_parts, rev, entries, options):
|
||||
"""see vclib.Repository.dirlogs docstring
|
||||
|
||||
rev can be a tag name or None. if set only information from revisions
|
||||
matching the tag will be retrieved
|
||||
|
||||
Option values recognized by this implementation:
|
||||
|
||||
cvs_subdirs
|
||||
boolean. true to fetch logs of the most recently modified file in each
|
||||
subdirectory
|
||||
|
||||
Option values returned by this implementation:
|
||||
|
||||
cvs_tags, cvs_branches
|
||||
lists of tag and branch names encountered in the directory
|
||||
"""
|
||||
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a directory."
|
||||
% (string.join(path_parts, "/")))
|
||||
entries_to_fetch = []
|
||||
for entry in entries:
|
||||
if vclib.check_path_access(self, path_parts + [entry.name], None, rev):
|
||||
entries_to_fetch.append(entry)
|
||||
|
||||
subdirs = options.get('cvs_subdirs', 0)
|
||||
|
||||
dirpath = self._getpath(path_parts)
|
||||
alltags = { # all the tags seen in the files of this dir
|
||||
'MAIN' : '',
|
||||
'HEAD' : '1.1'
|
||||
}
|
||||
|
||||
for entry in entries_to_fetch:
|
||||
entry.rev = entry.date = entry.author = None
|
||||
entry.dead = entry.absent = entry.log = entry.lockinfo = None
|
||||
path = _log_path(entry, dirpath, subdirs)
|
||||
if path:
|
||||
entry.path = path
|
||||
try:
|
||||
rcsparse.parse(open(path, 'rb'), InfoSink(entry, rev, alltags))
|
||||
except IOError, e:
|
||||
entry.errors.append("rcsparse error: %s" % e)
|
||||
except RuntimeError, e:
|
||||
entry.errors.append("rcsparse error: %s" % e)
|
||||
except rcsparse.RCSStopParser:
|
||||
pass
|
||||
|
||||
branches = options['cvs_branches'] = []
|
||||
tags = options['cvs_tags'] = []
|
||||
for name, rev in alltags.items():
|
||||
if Tag(None, rev).is_branch:
|
||||
branches.append(name)
|
||||
else:
|
||||
tags.append(name)
|
||||
|
||||
def itemlog(self, path_parts, rev, sortby, first, limit, options):
|
||||
"""see vclib.Repository.itemlog docstring
|
||||
|
||||
rev parameter can be a revision number, a branch number, a tag name,
|
||||
or None. If None, will return information about all revisions, otherwise,
|
||||
will only return information about the specified revision or branch.
|
||||
|
||||
Option values returned by this implementation:
|
||||
|
||||
cvs_tags
|
||||
dictionary of Tag objects for all tags encountered
|
||||
"""
|
||||
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file."
|
||||
% (string.join(path_parts, "/")))
|
||||
|
||||
path = self.rcsfile(path_parts, 1)
|
||||
sink = TreeSink()
|
||||
rcsparse.parse(open(path, 'rb'), sink)
|
||||
filtered_revs = _file_log(sink.revs.values(), sink.tags, sink.lockinfo,
|
||||
sink.default_branch, rev)
|
||||
for rev in filtered_revs:
|
||||
if rev.prev and len(rev.number) == 2:
|
||||
rev.changed = rev.prev.next_changed
|
||||
options['cvs_tags'] = sink.tags
|
||||
|
||||
if sortby == vclib.SORTBY_DATE:
|
||||
filtered_revs.sort(_logsort_date_cmp)
|
||||
elif sortby == vclib.SORTBY_REV:
|
||||
filtered_revs.sort(_logsort_rev_cmp)
|
||||
|
||||
if len(filtered_revs) < first:
|
||||
return []
|
||||
if limit:
|
||||
return filtered_revs[first:first+limit]
|
||||
return filtered_revs
|
||||
|
||||
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
|
||||
if self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file."
|
||||
% (string.join(path_parts1, "/")))
|
||||
if self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file."
|
||||
% (string.join(path_parts2, "/")))
|
||||
|
||||
temp1 = tempfile.mktemp()
|
||||
open(temp1, 'wb').write(self.openfile(path_parts1, rev1, {})[0].getvalue())
|
||||
temp2 = tempfile.mktemp()
|
||||
open(temp2, 'wb').write(self.openfile(path_parts2, rev2, {})[0].getvalue())
|
||||
|
||||
r1 = self.itemlog(path_parts1, rev1, vclib.SORTBY_DEFAULT, 0, 0, {})[-1]
|
||||
r2 = self.itemlog(path_parts2, rev2, vclib.SORTBY_DEFAULT, 0, 0, {})[-1]
|
||||
|
||||
info1 = (self.rcsfile(path_parts1, root=1, v=0), r1.date, r1.string)
|
||||
info2 = (self.rcsfile(path_parts2, root=1, v=0), r2.date, r2.string)
|
||||
|
||||
diff_args = vclib._diff_args(type, options)
|
||||
|
||||
return vclib._diff_fp(temp1, temp2, info1, info2,
|
||||
self.utilities.diff or 'diff', diff_args)
|
||||
|
||||
def annotate(self, path_parts, rev=None, include_text=False):
|
||||
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file."
|
||||
% (string.join(path_parts, "/")))
|
||||
source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, include_text)
|
||||
return source, source.revision
|
||||
|
||||
def revinfo(self, rev):
|
||||
raise vclib.UnsupportedFeature
|
||||
|
||||
def openfile(self, path_parts, rev, options):
|
||||
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file."
|
||||
% (string.join(path_parts, "/")))
|
||||
path = self.rcsfile(path_parts, 1)
|
||||
sink = COSink(rev)
|
||||
rcsparse.parse(open(path, 'rb'), sink)
|
||||
revision = sink.last and sink.last.string
|
||||
return cStringIO.StringIO(string.join(sink.sstext.text, "\n")), revision
|
||||
|
||||
class MatchingSink(rcsparse.Sink):
|
||||
"""Superclass for sinks that search for revisions based on tag or number"""
|
||||
|
||||
def __init__(self, find):
|
||||
"""Initialize with tag name or revision number string to match against"""
|
||||
if not find or find == 'MAIN' or find == 'HEAD':
|
||||
self.find = None
|
||||
else:
|
||||
self.find = find
|
||||
|
||||
self.find_tag = None
|
||||
|
||||
def set_principal_branch(self, branch_number):
|
||||
if self.find is None:
|
||||
self.find_tag = Tag(None, branch_number)
|
||||
|
||||
def define_tag(self, name, revision):
|
||||
if name == self.find:
|
||||
self.find_tag = Tag(None, revision)
|
||||
|
||||
def admin_completed(self):
|
||||
if self.find_tag is None:
|
||||
if self.find is None:
|
||||
self.find_tag = Tag(None, '')
|
||||
else:
|
||||
try:
|
||||
self.find_tag = Tag(None, self.find)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
class InfoSink(MatchingSink):
|
||||
def __init__(self, entry, tag, alltags):
|
||||
MatchingSink.__init__(self, tag)
|
||||
self.entry = entry
|
||||
self.alltags = alltags
|
||||
self.matching_rev = None
|
||||
self.perfect_match = 0
|
||||
self.lockinfo = { }
|
||||
|
||||
def define_tag(self, name, revision):
|
||||
MatchingSink.define_tag(self, name, revision)
|
||||
self.alltags[name] = revision
|
||||
|
||||
def admin_completed(self):
|
||||
MatchingSink.admin_completed(self)
|
||||
if self.find_tag is None:
|
||||
# tag we're looking for doesn't exist
|
||||
if self.entry.kind == vclib.FILE:
|
||||
self.entry.absent = 1
|
||||
raise rcsparse.RCSStopParser
|
||||
|
||||
def set_locker(self, rev, locker):
|
||||
self.lockinfo[rev] = locker
|
||||
|
||||
def define_revision(self, revision, date, author, state, branches, next):
|
||||
if self.perfect_match:
|
||||
return
|
||||
|
||||
tag = self.find_tag
|
||||
rev = Revision(revision, date, author, state == "dead")
|
||||
rev.lockinfo = self.lockinfo.get(revision)
|
||||
|
||||
# perfect match if revision number matches tag number or if revision is on
|
||||
# trunk and tag points to trunk. imperfect match if tag refers to a branch
|
||||
# and this revision is the highest revision so far found on that branch
|
||||
perfect = ((rev.number == tag.number) or
|
||||
(not tag.number and len(rev.number) == 2))
|
||||
if perfect or (tag.is_branch and tag.number == rev.number[:-1] and
|
||||
(not self.matching_rev or
|
||||
rev.number > self.matching_rev.number)):
|
||||
self.matching_rev = rev
|
||||
self.perfect_match = perfect
|
||||
|
||||
def set_revision_info(self, revision, log, text):
|
||||
if self.matching_rev:
|
||||
if revision == self.matching_rev.string:
|
||||
self.entry.rev = self.matching_rev.string
|
||||
self.entry.date = self.matching_rev.date
|
||||
self.entry.author = self.matching_rev.author
|
||||
self.entry.dead = self.matching_rev.dead
|
||||
self.entry.lockinfo = self.matching_rev.lockinfo
|
||||
self.entry.absent = 0
|
||||
self.entry.log = log
|
||||
raise rcsparse.RCSStopParser
|
||||
else:
|
||||
raise rcsparse.RCSStopParser
|
||||
|
||||
class TreeSink(rcsparse.Sink):
|
||||
d_command = re.compile('^d(\d+)\\s(\\d+)')
|
||||
a_command = re.compile('^a(\d+)\\s(\\d+)')
|
||||
|
||||
def __init__(self):
|
||||
self.revs = { }
|
||||
self.tags = { }
|
||||
self.head = None
|
||||
self.default_branch = None
|
||||
self.lockinfo = { }
|
||||
|
||||
def set_head_revision(self, revision):
|
||||
self.head = revision
|
||||
|
||||
def set_principal_branch(self, branch_number):
|
||||
self.default_branch = branch_number
|
||||
|
||||
def set_locker(self, rev, locker):
|
||||
self.lockinfo[rev] = locker
|
||||
|
||||
def define_tag(self, name, revision):
|
||||
# check !tags.has_key(tag_name)
|
||||
self.tags[name] = revision
|
||||
|
||||
def define_revision(self, revision, date, author, state, branches, next):
|
||||
# check !revs.has_key(revision)
|
||||
self.revs[revision] = Revision(revision, date, author, state == "dead")
|
||||
|
||||
def set_revision_info(self, revision, log, text):
|
||||
# check revs.has_key(revision)
|
||||
rev = self.revs[revision]
|
||||
rev.log = log
|
||||
|
||||
changed = None
|
||||
added = 0
|
||||
deled = 0
|
||||
if self.head != revision:
|
||||
changed = 1
|
||||
lines = string.split(text, '\n')
|
||||
idx = 0
|
||||
while idx < len(lines):
|
||||
command = lines[idx]
|
||||
dmatch = self.d_command.match(command)
|
||||
idx = idx + 1
|
||||
if dmatch:
|
||||
deled = deled + string.atoi(dmatch.group(2))
|
||||
else:
|
||||
amatch = self.a_command.match(command)
|
||||
if amatch:
|
||||
count = string.atoi(amatch.group(2))
|
||||
added = added + count
|
||||
idx = idx + count
|
||||
elif command:
|
||||
raise "error while parsing deltatext: %s" % command
|
||||
|
||||
if len(rev.number) == 2:
|
||||
rev.next_changed = changed and "+%i -%i" % (deled, added)
|
||||
else:
|
||||
rev.changed = changed and "+%i -%i" % (added, deled)
|
||||
|
||||
class StreamText:
|
||||
d_command = re.compile('^d(\d+)\\s(\\d+)')
|
||||
a_command = re.compile('^a(\d+)\\s(\\d+)')
|
||||
|
||||
def __init__(self, text):
|
||||
self.text = string.split(text, "\n")
|
||||
|
||||
def command(self, cmd):
|
||||
adjust = 0
|
||||
add_lines_remaining = 0
|
||||
diffs = string.split(cmd, "\n")
|
||||
if diffs[-1] == "":
|
||||
del diffs[-1]
|
||||
if len(diffs) == 0:
|
||||
return
|
||||
if diffs[0] == "":
|
||||
del diffs[0]
|
||||
for command in diffs:
|
||||
if add_lines_remaining > 0:
|
||||
# Insertion lines from a prior "a" command
|
||||
self.text.insert(start_line + adjust, command)
|
||||
add_lines_remaining = add_lines_remaining - 1
|
||||
adjust = adjust + 1
|
||||
continue
|
||||
dmatch = self.d_command.match(command)
|
||||
amatch = self.a_command.match(command)
|
||||
if dmatch:
|
||||
# "d" - Delete command
|
||||
start_line = string.atoi(dmatch.group(1))
|
||||
count = string.atoi(dmatch.group(2))
|
||||
begin = start_line + adjust - 1
|
||||
del self.text[begin:begin + count]
|
||||
adjust = adjust - count
|
||||
elif amatch:
|
||||
# "a" - Add command
|
||||
start_line = string.atoi(amatch.group(1))
|
||||
count = string.atoi(amatch.group(2))
|
||||
add_lines_remaining = count
|
||||
else:
|
||||
raise RuntimeError, 'Error parsing diff commands'
|
||||
|
||||
def secondnextdot(s, start):
|
||||
# find the position the second dot after the start index.
|
||||
return string.find(s, '.', string.find(s, '.', start) + 1)
|
||||
|
||||
|
||||
class COSink(MatchingSink):
|
||||
def __init__(self, rev):
|
||||
MatchingSink.__init__(self, rev)
|
||||
|
||||
def set_head_revision(self, revision):
|
||||
self.head = Revision(revision)
|
||||
self.last = None
|
||||
self.sstext = None
|
||||
|
||||
def admin_completed(self):
|
||||
MatchingSink.admin_completed(self)
|
||||
if self.find_tag is None:
|
||||
raise vclib.InvalidRevision(self.find)
|
||||
|
||||
def set_revision_info(self, revision, log, text):
|
||||
tag = self.find_tag
|
||||
rev = Revision(revision)
|
||||
|
||||
if rev.number == tag.number:
|
||||
self.log = log
|
||||
|
||||
depth = len(rev.number)
|
||||
|
||||
if rev.number == self.head.number:
|
||||
assert self.sstext is None
|
||||
self.sstext = StreamText(text)
|
||||
elif (depth == 2 and tag.number and rev.number >= tag.number[:depth]):
|
||||
assert len(self.last.number) == 2
|
||||
assert rev.number < self.last.number
|
||||
self.sstext.command(text)
|
||||
elif (depth > 2 and rev.number[:depth-1] == tag.number[:depth-1] and
|
||||
(rev.number <= tag.number or len(tag.number) == depth-1)):
|
||||
assert len(rev.number) - len(self.last.number) in (0, 2)
|
||||
assert rev.number > self.last.number
|
||||
self.sstext.command(text)
|
||||
else:
|
||||
rev = None
|
||||
|
||||
if rev:
|
||||
#print "tag =", tag.number, "rev =", rev.number, "<br>"
|
||||
self.last = rev
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -12,45 +12,55 @@
|
||||
|
||||
"""common.py: common classes and functions for the RCS parsing tools."""
|
||||
|
||||
import time
|
||||
import calendar
|
||||
import string
|
||||
|
||||
### compat isn't in vclib right now. need to work up a solution
|
||||
import compat
|
||||
|
||||
|
||||
class Sink:
|
||||
def set_head_revision(self, revision):
|
||||
pass
|
||||
|
||||
def set_principal_branch(self, branch_name):
|
||||
pass
|
||||
def define_tag(self, name, revision):
|
||||
pass
|
||||
|
||||
def set_access(self, accessors):
|
||||
pass
|
||||
def set_expansion(self, mode):
|
||||
|
||||
def define_tag(self, name, revision):
|
||||
pass
|
||||
|
||||
def set_locker(self, revision, locker):
|
||||
pass
|
||||
|
||||
def set_locking(self, mode):
|
||||
"""Used to signal locking mode.
|
||||
|
||||
Called with mode argument 'strict' if strict locking
|
||||
Not called when no locking used."""
|
||||
|
||||
pass
|
||||
def set_locker(self, revision, locker):
|
||||
pass
|
||||
|
||||
def set_comment(self, comment):
|
||||
pass
|
||||
def set_description(self, description):
|
||||
|
||||
def set_expansion(self, mode):
|
||||
pass
|
||||
|
||||
def admin_completed(self):
|
||||
pass
|
||||
|
||||
def define_revision(self, revision, timestamp, author, state,
|
||||
branches, next):
|
||||
pass
|
||||
def set_revision_info(self, revision, log, text):
|
||||
pass
|
||||
def admin_completed(self):
|
||||
pass
|
||||
|
||||
def tree_completed(self):
|
||||
pass
|
||||
|
||||
def set_description(self, description):
|
||||
pass
|
||||
|
||||
def set_revision_info(self, revision, log, text):
|
||||
pass
|
||||
|
||||
def parse_completed(self):
|
||||
pass
|
||||
|
||||
@@ -62,97 +72,201 @@ class Sink:
|
||||
|
||||
class RCSParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RCSIllegalCharacter(RCSParseError):
|
||||
pass
|
||||
### need more work on this one
|
||||
|
||||
|
||||
class RCSExpected(RCSParseError):
|
||||
def __init__(self, got, wanted):
|
||||
RCSParseError.__init__(self, got, wanted)
|
||||
RCSParseError.__init__(
|
||||
self,
|
||||
'Unexpected parsing error in RCS file.\n'
|
||||
'Expected token: %s, but saw: %s'
|
||||
% (wanted, got)
|
||||
)
|
||||
|
||||
|
||||
class RCSStopParser(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
#
|
||||
# STANDARD TOKEN STREAM-BASED PARSER
|
||||
#
|
||||
|
||||
class _Parser:
|
||||
stream_class = None # subclasses need to define this
|
||||
stream_class = None # subclasses need to define this
|
||||
|
||||
def _read_until_semicolon(self):
|
||||
"""Read all tokens up to and including the next semicolon token.
|
||||
|
||||
Return the tokens (not including the semicolon) as a list."""
|
||||
|
||||
tokens = []
|
||||
|
||||
while 1:
|
||||
token = self.ts.get()
|
||||
if token == ';':
|
||||
break
|
||||
tokens.append(token)
|
||||
|
||||
return tokens
|
||||
|
||||
def _parse_admin_head(self, token):
|
||||
rev = self.ts.get()
|
||||
if rev == ';':
|
||||
# The head revision is not specified. Just drop the semicolon
|
||||
# on the floor.
|
||||
pass
|
||||
else:
|
||||
self.sink.set_head_revision(rev)
|
||||
self.ts.match(';')
|
||||
|
||||
def _parse_admin_branch(self, token):
|
||||
branch = self.ts.get()
|
||||
if branch != ';':
|
||||
self.sink.set_principal_branch(branch)
|
||||
self.ts.match(';')
|
||||
|
||||
def _parse_admin_access(self, token):
|
||||
accessors = self._read_until_semicolon()
|
||||
if accessors:
|
||||
self.sink.set_access(accessors)
|
||||
|
||||
def _parse_admin_symbols(self, token):
|
||||
while 1:
|
||||
tag_name = self.ts.get()
|
||||
if tag_name == ';':
|
||||
break
|
||||
self.ts.match(':')
|
||||
tag_rev = self.ts.get()
|
||||
self.sink.define_tag(tag_name, tag_rev)
|
||||
|
||||
def _parse_admin_locks(self, token):
|
||||
while 1:
|
||||
locker = self.ts.get()
|
||||
if locker == ';':
|
||||
break
|
||||
self.ts.match(':')
|
||||
rev = self.ts.get()
|
||||
self.sink.set_locker(rev, locker)
|
||||
|
||||
def _parse_admin_strict(self, token):
|
||||
self.sink.set_locking("strict")
|
||||
self.ts.match(';')
|
||||
|
||||
def _parse_admin_comment(self, token):
|
||||
self.sink.set_comment(self.ts.get())
|
||||
self.ts.match(';')
|
||||
|
||||
def _parse_admin_expand(self, token):
|
||||
expand_mode = self.ts.get()
|
||||
self.sink.set_expansion(expand_mode)
|
||||
self.ts.match(';')
|
||||
|
||||
admin_token_map = {
|
||||
'head' : _parse_admin_head,
|
||||
'branch' : _parse_admin_branch,
|
||||
'access' : _parse_admin_access,
|
||||
'symbols' : _parse_admin_symbols,
|
||||
'locks' : _parse_admin_locks,
|
||||
'strict' : _parse_admin_strict,
|
||||
'comment' : _parse_admin_comment,
|
||||
'expand' : _parse_admin_expand,
|
||||
'desc' : None,
|
||||
}
|
||||
|
||||
def parse_rcs_admin(self):
|
||||
while 1:
|
||||
# Read initial token at beginning of line
|
||||
token = self.ts.get()
|
||||
|
||||
# We're done once we reach the description of the RCS tree
|
||||
if token[0] in string.digits:
|
||||
self.ts.unget(token)
|
||||
return
|
||||
|
||||
if token == "head":
|
||||
semi, rev = self.ts.mget(2)
|
||||
self.sink.set_head_revision(rev)
|
||||
if semi != ';':
|
||||
raise RCSExpected(semi, ';')
|
||||
elif token == "branch":
|
||||
semi, branch = self.ts.mget(2)
|
||||
if semi == ';':
|
||||
self.sink.set_principal_branch(branch)
|
||||
try:
|
||||
f = self.admin_token_map[token]
|
||||
except KeyError:
|
||||
# We're done once we reach the description of the RCS tree
|
||||
if token[0] in string.digits:
|
||||
self.ts.unget(token)
|
||||
return
|
||||
else:
|
||||
if branch == ';':
|
||||
self.ts.unget(semi);
|
||||
else:
|
||||
raise RCSExpected(semi, ';')
|
||||
elif token == "symbols":
|
||||
while 1:
|
||||
tag = self.ts.get()
|
||||
if tag == ';':
|
||||
break
|
||||
self.ts.match(':')
|
||||
tag_name = tag
|
||||
tag_rev = self.ts.get()
|
||||
self.sink.define_tag(tag_name, tag_rev)
|
||||
elif token == "comment":
|
||||
semi, comment = self.ts.mget(2)
|
||||
self.sink.set_comment(comment)
|
||||
if semi != ';':
|
||||
raise RCSExpected(semi, ';')
|
||||
elif token == "expand":
|
||||
semi, expand_mode = self.ts.mget(2)
|
||||
self.sink.set_expansion(expand_mode)
|
||||
if semi != ';':
|
||||
raise RCSExpected(semi, ';')
|
||||
elif token == "locks":
|
||||
while 1:
|
||||
tag = self.ts.get()
|
||||
if tag == ';':
|
||||
break
|
||||
(locker, rev) = string.split(tag,':')
|
||||
self.sink.set_locker(rev, locker)
|
||||
|
||||
tag = self.ts.get()
|
||||
if tag == "strict":
|
||||
self.sink.set_locking("strict")
|
||||
self.ts.match(';')
|
||||
else:
|
||||
self.ts.unget(tag)
|
||||
elif token == "access":
|
||||
accessors = []
|
||||
while 1:
|
||||
tag = self.ts.get()
|
||||
if tag == ';':
|
||||
if accessors != []:
|
||||
self.sink.set_access(accessors)
|
||||
break
|
||||
accessors = accessors + [ tag ]
|
||||
|
||||
# Chew up "newphrase"
|
||||
# Chew up "newphrase"
|
||||
# warn("Unexpected RCS token: $token\n")
|
||||
while self.ts.get() != ';':
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
# warn("Unexpected RCS token: $token\n")
|
||||
if f is None:
|
||||
self.ts.unget(token)
|
||||
return
|
||||
else:
|
||||
f(self, token)
|
||||
|
||||
raise RuntimeError, "Unexpected EOF"
|
||||
def _parse_rcs_tree_entry(self, revision):
|
||||
# Parse date
|
||||
self.ts.match('date')
|
||||
date = self.ts.get()
|
||||
self.ts.match(';')
|
||||
|
||||
# Convert date into timestamp
|
||||
date_fields = string.split(date, '.')
|
||||
# According to rcsfile(5): the year "contains just the last two
|
||||
# digits of the year for years from 1900 through 1999, and all the
|
||||
# digits of years thereafter".
|
||||
if len(date_fields[0]) == 2:
|
||||
date_fields[0] = '19' + date_fields[0]
|
||||
date_fields = map(string.atoi, date_fields)
|
||||
EPOCH = 1970
|
||||
if date_fields[0] < EPOCH:
|
||||
raise ValueError, 'invalid year'
|
||||
timestamp = calendar.timegm(tuple(date_fields) + (0, 0, 0,))
|
||||
|
||||
# Parse author
|
||||
### NOTE: authors containing whitespace are violations of the
|
||||
### RCS specification. We are making an allowance here because
|
||||
### CVSNT is known to produce these sorts of authors.
|
||||
self.ts.match('author')
|
||||
author = ' '.join(self._read_until_semicolon())
|
||||
|
||||
# Parse state
|
||||
self.ts.match('state')
|
||||
state = ''
|
||||
while 1:
|
||||
token = self.ts.get()
|
||||
if token == ';':
|
||||
break
|
||||
state = state + token + ' '
|
||||
state = state[:-1] # toss the trailing space
|
||||
|
||||
# Parse branches
|
||||
self.ts.match('branches')
|
||||
branches = self._read_until_semicolon()
|
||||
|
||||
# Parse revision of next delta in chain
|
||||
self.ts.match('next')
|
||||
next = self.ts.get()
|
||||
if next == ';':
|
||||
next = None
|
||||
else:
|
||||
self.ts.match(';')
|
||||
|
||||
# there are some files with extra tags in them. for example:
|
||||
# owner 640;
|
||||
# group 15;
|
||||
# permissions 644;
|
||||
# hardlinks @configure.in@;
|
||||
# this is "newphrase" in RCSFILE(5). we just want to skip over these.
|
||||
while 1:
|
||||
token = self.ts.get()
|
||||
if token == 'desc' or token[0] in string.digits:
|
||||
self.ts.unget(token)
|
||||
break
|
||||
# consume everything up to the semicolon
|
||||
self._read_until_semicolon()
|
||||
|
||||
self.sink.define_revision(revision, timestamp, author, state, branches,
|
||||
next)
|
||||
|
||||
def parse_rcs_tree(self):
|
||||
while 1:
|
||||
@@ -163,86 +277,7 @@ class _Parser:
|
||||
self.ts.unget(revision)
|
||||
return
|
||||
|
||||
# Parse date
|
||||
semi, date, sym = self.ts.mget(3)
|
||||
if sym != 'date':
|
||||
raise RCSExpected(sym, 'date')
|
||||
if semi != ';':
|
||||
raise RCSExpected(semi, ';')
|
||||
|
||||
# Convert date into timestamp
|
||||
date_fields = string.split(date, '.') + ['0', '0', '0']
|
||||
date_fields = map(string.atoi, date_fields)
|
||||
# need to make the date four digits for timegm
|
||||
EPOCH = 1970
|
||||
if date_fields[0] < EPOCH:
|
||||
if date_fields[0] < 70:
|
||||
date_fields[0] = date_fields[0] + 2000
|
||||
else:
|
||||
date_fields[0] = date_fields[0] + 1900
|
||||
if date_fields[0] < EPOCH:
|
||||
raise ValueError, 'invalid year'
|
||||
|
||||
timestamp = compat.timegm(tuple(date_fields))
|
||||
|
||||
# Parse author
|
||||
### NOTE: authors containing whitespace are violations of the
|
||||
### RCS specification. We are making an allowance here because
|
||||
### CVSNT is known to produce these sorts of authors.
|
||||
self.ts.match('author')
|
||||
author = ''
|
||||
while 1:
|
||||
token = self.ts.get()
|
||||
if token == ';':
|
||||
break
|
||||
author = author + token + ' '
|
||||
author = author[:-1] # toss the trailing space
|
||||
|
||||
# Parse state
|
||||
self.ts.match('state')
|
||||
state = ''
|
||||
while 1:
|
||||
token = self.ts.get()
|
||||
if token == ';':
|
||||
break
|
||||
state = state + token + ' '
|
||||
state = state[:-1] # toss the trailing space
|
||||
|
||||
# Parse branches
|
||||
self.ts.match('branches')
|
||||
branches = [ ]
|
||||
while 1:
|
||||
token = self.ts.get()
|
||||
if token == ';':
|
||||
break
|
||||
branches.append(token)
|
||||
|
||||
# Parse revision of next delta in chain
|
||||
next, sym = self.ts.mget(2)
|
||||
if sym != 'next':
|
||||
raise RCSExpected(sym, 'next')
|
||||
if next == ';':
|
||||
next = None
|
||||
else:
|
||||
self.ts.match(';')
|
||||
|
||||
# there are some files with extra tags in them. for example:
|
||||
# owner 640;
|
||||
# group 15;
|
||||
# permissions 644;
|
||||
# hardlinks @configure.in@;
|
||||
# this is "newphrase" in RCSFILE(5). we just want to skip over these.
|
||||
while 1:
|
||||
token = self.ts.get()
|
||||
if token == 'desc' or token[0] in string.digits:
|
||||
self.ts.unget(token)
|
||||
break
|
||||
# consume everything up to the semicolon
|
||||
while self.ts.get() != ';':
|
||||
pass
|
||||
|
||||
self.sink.define_revision(revision, timestamp, author, state, branches,
|
||||
next)
|
||||
self._parse_rcs_tree_entry(revision)
|
||||
|
||||
def parse_rcs_description(self):
|
||||
self.ts.match('desc')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -19,14 +19,18 @@ import string
|
||||
import common
|
||||
|
||||
class _TokenStream:
|
||||
token_term = string.whitespace + ';'
|
||||
token_term = string.whitespace + ";:"
|
||||
try:
|
||||
token_term = frozenset(token_term)
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
# the algorithm is about the same speed for any CHUNK_SIZE chosen.
|
||||
# grab a good-sized chunk, but not too large to overwhelm memory.
|
||||
# note: we use a multiple of a standard block size
|
||||
CHUNK_SIZE = 192 * 512 # about 100k
|
||||
|
||||
# CHUNK_SIZE = 5 # for debugging, make the function grind...
|
||||
# CHUNK_SIZE = 5 # for debugging, make the function grind...
|
||||
|
||||
def __init__(self, file):
|
||||
self.rcsfile = file
|
||||
@@ -44,15 +48,17 @@ class _TokenStream:
|
||||
# out more complex solutions.
|
||||
|
||||
buf = self.buf
|
||||
lbuf = len(buf)
|
||||
idx = self.idx
|
||||
|
||||
while 1:
|
||||
if idx == len(buf):
|
||||
if idx == lbuf:
|
||||
buf = self.rcsfile.read(self.CHUNK_SIZE)
|
||||
if buf == '':
|
||||
# signal EOF by returning None as the token
|
||||
del self.buf # so we fail if get() is called again
|
||||
del self.buf # so we fail if get() is called again
|
||||
return None
|
||||
lbuf = len(buf)
|
||||
idx = 0
|
||||
|
||||
if buf[idx] not in string.whitespace:
|
||||
@@ -60,27 +66,28 @@ class _TokenStream:
|
||||
|
||||
idx = idx + 1
|
||||
|
||||
if buf[idx] == ';':
|
||||
if buf[idx] in ';:':
|
||||
self.buf = buf
|
||||
self.idx = idx + 1
|
||||
return ';'
|
||||
return buf[idx]
|
||||
|
||||
if buf[idx] != '@':
|
||||
end = idx + 1
|
||||
token = ''
|
||||
while 1:
|
||||
# find token characters in the current buffer
|
||||
while end < len(buf) and buf[end] not in self.token_term:
|
||||
while end < lbuf and buf[end] not in self.token_term:
|
||||
end = end + 1
|
||||
token = token + buf[idx:end]
|
||||
|
||||
if end < len(buf):
|
||||
if end < lbuf:
|
||||
# we stopped before the end, so we have a full token
|
||||
idx = end
|
||||
break
|
||||
|
||||
# we stopped at the end of the buffer, so we may have a partial token
|
||||
buf = self.rcsfile.read(self.CHUNK_SIZE)
|
||||
lbuf = len(buf)
|
||||
idx = end = 0
|
||||
|
||||
self.buf = buf
|
||||
@@ -94,22 +101,24 @@ class _TokenStream:
|
||||
chunks = [ ]
|
||||
|
||||
while 1:
|
||||
if idx == len(buf):
|
||||
if idx == lbuf:
|
||||
idx = 0
|
||||
buf = self.rcsfile.read(self.CHUNK_SIZE)
|
||||
if buf == '':
|
||||
raise RuntimeError, 'EOF'
|
||||
lbuf = len(buf)
|
||||
i = string.find(buf, '@', idx)
|
||||
if i == -1:
|
||||
chunks.append(buf[idx:])
|
||||
idx = len(buf)
|
||||
idx = lbuf
|
||||
continue
|
||||
if i == len(buf) - 1:
|
||||
if i == lbuf - 1:
|
||||
chunks.append(buf[idx:i])
|
||||
idx = 0
|
||||
buf = '@' + self.rcsfile.read(self.CHUNK_SIZE)
|
||||
if buf == '@':
|
||||
raise RuntimeError, 'EOF'
|
||||
lbuf = len(buf)
|
||||
continue
|
||||
if buf[i + 1] == '@':
|
||||
chunks.append(buf[idx:i+1])
|
||||
@@ -134,8 +143,7 @@ class _TokenStream:
|
||||
|
||||
token = self.get()
|
||||
if token != match:
|
||||
raise RuntimeError, ('Unexpected parsing error in RCS file.\n' +
|
||||
'Expected token: %s, but saw: %s' % (match, token))
|
||||
raise common.RCSExpected(token, match)
|
||||
|
||||
def unget(self, token):
|
||||
"Put this token back, for the next get() to return."
|
||||
@@ -166,75 +174,3 @@ class _TokenStream:
|
||||
|
||||
class Parser(common._Parser):
|
||||
stream_class = _TokenStream
|
||||
|
||||
def parse_rcs_admin(self):
|
||||
while 1:
|
||||
# Read initial token at beginning of line
|
||||
token = self.ts.get()
|
||||
|
||||
# We're done once we reach the description of the RCS tree
|
||||
if token[0] in string.digits:
|
||||
self.ts.unget(token)
|
||||
return
|
||||
|
||||
if token == "head":
|
||||
semi, rev = self.ts.mget(2)
|
||||
self.sink.set_head_revision(rev)
|
||||
if semi != ';':
|
||||
raise common.RCSExpected(semi, ';')
|
||||
elif token == "branch":
|
||||
semi, branch = self.ts.mget(2)
|
||||
if semi == ';':
|
||||
self.sink.set_principal_branch(branch)
|
||||
else:
|
||||
if branch == ';':
|
||||
self.ts.unget(semi);
|
||||
else:
|
||||
raise common.RCSExpected(semi, ';')
|
||||
elif token == "symbols":
|
||||
while 1:
|
||||
tag = self.ts.get()
|
||||
if tag == ';':
|
||||
break
|
||||
(tag_name, tag_rev) = string.split(tag, ':')
|
||||
self.sink.define_tag(tag_name, tag_rev)
|
||||
elif token == "comment":
|
||||
semi, comment = self.ts.mget(2)
|
||||
self.sink.set_comment(comment)
|
||||
if semi != ';':
|
||||
raise common.RCSExpected(semi, ';')
|
||||
elif token == "expand":
|
||||
semi, expand_mode = self.ts.mget(2)
|
||||
self.sink.set_expansion(expand_mode)
|
||||
if semi != ';':
|
||||
raise RCSExpected(semi, ';')
|
||||
elif token == "locks":
|
||||
while 1:
|
||||
tag = self.ts.get()
|
||||
if tag == ';':
|
||||
break
|
||||
(locker, rev) = string.split(tag,':')
|
||||
self.sink.set_locker(rev, locker)
|
||||
|
||||
tag = self.ts.get()
|
||||
if tag == "strict":
|
||||
self.sink.set_locking("strict")
|
||||
self.ts.match(';')
|
||||
else:
|
||||
self.ts.unget(tag)
|
||||
elif token == "access":
|
||||
accessors = []
|
||||
while 1:
|
||||
tag = self.ts.get()
|
||||
if tag == ';':
|
||||
if accessors != []:
|
||||
self.sink.set_access(accessors)
|
||||
break
|
||||
accessors = accessors + [ tag ]
|
||||
|
||||
# Chew up "newphrase".
|
||||
else:
|
||||
pass
|
||||
# warn("Unexpected RCS token: $token\n")
|
||||
|
||||
raise RuntimeError, "Unexpected EOF"
|
||||
|
||||
73
lib/vclib/ccvs/rcsparse/parse_rcs_file.py
Executable file
73
lib/vclib/ccvs/rcsparse/parse_rcs_file.py
Executable file
@@ -0,0 +1,73 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
# (Be in -*- python -*- mode.)
|
||||
#
|
||||
# ====================================================================
|
||||
# Copyright (c) 2006-2007 CollabNet. All rights reserved.
|
||||
#
|
||||
# This software is licensed as described in the file COPYING, which
|
||||
# you should have received as part of this distribution. The terms
|
||||
# are also available at http://subversion.tigris.org/license-1.html.
|
||||
# If newer versions of this license are posted there, you may use a
|
||||
# newer version instead, at your option.
|
||||
#
|
||||
# This software consists of voluntary contributions made by many
|
||||
# individuals. For exact contribution history, see the revision
|
||||
# history and logs, available at http://cvs2svn.tigris.org/.
|
||||
# ====================================================================
|
||||
|
||||
"""Parse an RCS file, showing the rcsparse callbacks that are called.
|
||||
|
||||
This program is useful to see whether an RCS file has a problem (in
|
||||
the sense of not being parseable by rcsparse) and also to illuminate
|
||||
the correspondence between RCS file contents and rcsparse callbacks.
|
||||
|
||||
The output of this program can also be considered to be a kind of
|
||||
'canonical' format for RCS files, at least in so far as rcsparse
|
||||
returns all relevant information in the file and provided that the
|
||||
order of callbacks is always the same."""
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
class Logger:
|
||||
def __init__(self, f, name):
|
||||
self.f = f
|
||||
self.name = name
|
||||
|
||||
def __call__(self, *args):
|
||||
self.f.write(
|
||||
'%s(%s)\n' % (self.name, ', '.join(['%r' % arg for arg in args]),)
|
||||
)
|
||||
|
||||
|
||||
class LoggingSink:
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
|
||||
def __getattr__(self, name):
|
||||
return Logger(self.f, name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Since there is nontrivial logic in __init__.py, we have to import
|
||||
# parse() via that file. First make sure that the directory
|
||||
# containing this script is in the path:
|
||||
sys.path.insert(0, os.path.dirname(sys.argv[0]))
|
||||
|
||||
from __init__ import parse
|
||||
|
||||
if sys.argv[1:]:
|
||||
for path in sys.argv[1:]:
|
||||
if os.path.isfile(path) and path.endswith(',v'):
|
||||
parse(
|
||||
open(path, 'rb'), LoggingSink(sys.stdout)
|
||||
)
|
||||
else:
|
||||
sys.stderr.write('%r is being ignored.\n' % path)
|
||||
else:
|
||||
parse(sys.stdin, LoggingSink(sys.stdout))
|
||||
|
||||
|
||||
73
lib/vclib/ccvs/rcsparse/run-tests.py
Executable file
73
lib/vclib/ccvs/rcsparse/run-tests.py
Executable file
@@ -0,0 +1,73 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
# (Be in -*- python -*- mode.)
|
||||
#
|
||||
# ====================================================================
|
||||
# Copyright (c) 2007 CollabNet. All rights reserved.
|
||||
#
|
||||
# This software is licensed as described in the file COPYING, which
|
||||
# you should have received as part of this distribution. The terms
|
||||
# are also available at http://subversion.tigris.org/license-1.html.
|
||||
# If newer versions of this license are posted there, you may use a
|
||||
# newer version instead, at your option.
|
||||
#
|
||||
# This software consists of voluntary contributions made by many
|
||||
# individuals. For exact contribution history, see the revision
|
||||
# history and logs, available at http://viewvc.tigris.org/.
|
||||
# ====================================================================
|
||||
|
||||
"""Run tests of rcsparse code."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import glob
|
||||
from cStringIO import StringIO
|
||||
from difflib import Differ
|
||||
|
||||
# Since there is nontrivial logic in __init__.py, we have to import
|
||||
# parse() via that file. First make sure that the directory
|
||||
# containing this script is in the path:
|
||||
script_dir = os.path.dirname(sys.argv[0])
|
||||
sys.path.insert(0, script_dir)
|
||||
|
||||
from __init__ import parse
|
||||
from parse_rcs_file import LoggingSink
|
||||
|
||||
|
||||
test_dir = os.path.join(script_dir, 'test-data')
|
||||
|
||||
filelist = glob.glob(os.path.join(test_dir, '*,v'))
|
||||
filelist.sort()
|
||||
|
||||
all_tests_ok = 1
|
||||
|
||||
for filename in filelist:
|
||||
sys.stderr.write('%s: ' % (filename,))
|
||||
f = StringIO()
|
||||
try:
|
||||
parse(open(filename, 'rb'), LoggingSink(f))
|
||||
except Exception, e:
|
||||
sys.stderr.write('Error parsing file: %s!\n' % (e,))
|
||||
all_tests_ok = 0
|
||||
else:
|
||||
output = f.getvalue()
|
||||
|
||||
expected_output_filename = filename[:-2] + '.out'
|
||||
expected_output = open(expected_output_filename, 'rb').read()
|
||||
|
||||
if output == expected_output:
|
||||
sys.stderr.write('OK\n')
|
||||
else:
|
||||
sys.stderr.write('Output does not match expected output!\n')
|
||||
differ = Differ()
|
||||
for diffline in differ.compare(
|
||||
expected_output.splitlines(1), output.splitlines(1)
|
||||
):
|
||||
sys.stderr.write(diffline)
|
||||
all_tests_ok = 0
|
||||
|
||||
if all_tests_ok:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
102
lib/vclib/ccvs/rcsparse/test-data/default,v
Normal file
102
lib/vclib/ccvs/rcsparse/test-data/default,v
Normal file
@@ -0,0 +1,102 @@
|
||||
head 1.2;
|
||||
access;
|
||||
symbols
|
||||
B_SPLIT:1.2.0.4
|
||||
B_MIXED:1.2.0.2
|
||||
T_MIXED:1.2
|
||||
B_FROM_INITIALS_BUT_ONE:1.1.1.1.0.4
|
||||
B_FROM_INITIALS:1.1.1.1.0.2
|
||||
T_ALL_INITIAL_FILES_BUT_ONE:1.1.1.1
|
||||
T_ALL_INITIAL_FILES:1.1.1.1
|
||||
vendortag:1.1.1.1
|
||||
vendorbranch:1.1.1;
|
||||
locks; strict;
|
||||
comment @# @;
|
||||
|
||||
|
||||
1.2
|
||||
date 2003.05.23.00.17.53; author jrandom; state Exp;
|
||||
branches
|
||||
1.2.2.1
|
||||
1.2.4.1;
|
||||
next 1.1;
|
||||
|
||||
1.1
|
||||
date 98.05.22.23.20.19; author jrandom; state Exp;
|
||||
branches
|
||||
1.1.1.1;
|
||||
next ;
|
||||
|
||||
1.1.1.1
|
||||
date 98.05.22.23.20.19; author jrandom; state Exp;
|
||||
branches;
|
||||
next ;
|
||||
|
||||
1.2.2.1
|
||||
date 2003.05.23.00.31.36; author jrandom; state Exp;
|
||||
branches;
|
||||
next ;
|
||||
|
||||
1.2.4.1
|
||||
date 2003.06.03.03.20.31; author jrandom; state Exp;
|
||||
branches;
|
||||
next ;
|
||||
|
||||
|
||||
desc
|
||||
@@
|
||||
|
||||
|
||||
1.2
|
||||
log
|
||||
@Second commit to proj, affecting all 7 files.
|
||||
@
|
||||
text
|
||||
@This is the file `default' in the top level of the project.
|
||||
|
||||
Every directory in the `proj' project has a file named `default'.
|
||||
|
||||
This line was added in the second commit (affecting all 7 files).
|
||||
@
|
||||
|
||||
|
||||
1.2.4.1
|
||||
log
|
||||
@First change on branch B_SPLIT.
|
||||
|
||||
This change excludes sub3/default, because it was not part of this
|
||||
commit, and sub1/subsubB/default, which is not even on the branch yet.
|
||||
@
|
||||
text
|
||||
@a5 2
|
||||
|
||||
First change on branch B_SPLIT.
|
||||
@
|
||||
|
||||
|
||||
1.2.2.1
|
||||
log
|
||||
@Modify three files, on branch B_MIXED.
|
||||
@
|
||||
text
|
||||
@a5 2
|
||||
|
||||
This line was added on branch B_MIXED only (affecting 3 files).
|
||||
@
|
||||
|
||||
|
||||
1.1
|
||||
log
|
||||
@Initial revision
|
||||
@
|
||||
text
|
||||
@d4 2
|
||||
@
|
||||
|
||||
|
||||
1.1.1.1
|
||||
log
|
||||
@Initial import.
|
||||
@
|
||||
text
|
||||
@@
|
||||
26
lib/vclib/ccvs/rcsparse/test-data/default.out
Normal file
26
lib/vclib/ccvs/rcsparse/test-data/default.out
Normal file
@@ -0,0 +1,26 @@
|
||||
set_head_revision('1.2')
|
||||
define_tag('B_SPLIT', '1.2.0.4')
|
||||
define_tag('B_MIXED', '1.2.0.2')
|
||||
define_tag('T_MIXED', '1.2')
|
||||
define_tag('B_FROM_INITIALS_BUT_ONE', '1.1.1.1.0.4')
|
||||
define_tag('B_FROM_INITIALS', '1.1.1.1.0.2')
|
||||
define_tag('T_ALL_INITIAL_FILES_BUT_ONE', '1.1.1.1')
|
||||
define_tag('T_ALL_INITIAL_FILES', '1.1.1.1')
|
||||
define_tag('vendortag', '1.1.1.1')
|
||||
define_tag('vendorbranch', '1.1.1')
|
||||
set_locking('strict')
|
||||
set_comment('# ')
|
||||
admin_completed()
|
||||
define_revision('1.2', 1053649073, 'jrandom', 'Exp', ['1.2.2.1', '1.2.4.1'], '1.1')
|
||||
define_revision('1.1', 895879219, 'jrandom', 'Exp', ['1.1.1.1'], None)
|
||||
define_revision('1.1.1.1', 895879219, 'jrandom', 'Exp', [], None)
|
||||
define_revision('1.2.2.1', 1053649896, 'jrandom', 'Exp', [], None)
|
||||
define_revision('1.2.4.1', 1054610431, 'jrandom', 'Exp', [], None)
|
||||
tree_completed()
|
||||
set_description('')
|
||||
set_revision_info('1.2', 'Second commit to proj, affecting all 7 files.\n', "This is the file `default' in the top level of the project.\n\nEvery directory in the `proj' project has a file named `default'.\n\nThis line was added in the second commit (affecting all 7 files).\n")
|
||||
set_revision_info('1.2.4.1', 'First change on branch B_SPLIT.\n\nThis change excludes sub3/default, because it was not part of this\ncommit, and sub1/subsubB/default, which is not even on the branch yet.\n', 'a5 2\n\nFirst change on branch B_SPLIT.\n')
|
||||
set_revision_info('1.2.2.1', 'Modify three files, on branch B_MIXED.\n', 'a5 2\n\nThis line was added on branch B_MIXED only (affecting 3 files).\n')
|
||||
set_revision_info('1.1', 'Initial revision\n', 'd4 2\n')
|
||||
set_revision_info('1.1.1.1', 'Initial import.\n', '')
|
||||
parse_completed()
|
||||
10
lib/vclib/ccvs/rcsparse/test-data/empty-file,v
Normal file
10
lib/vclib/ccvs/rcsparse/test-data/empty-file,v
Normal file
@@ -0,0 +1,10 @@
|
||||
head ;
|
||||
access;
|
||||
symbols;
|
||||
locks; strict;
|
||||
comment @# @;
|
||||
|
||||
|
||||
|
||||
desc
|
||||
@@
|
||||
6
lib/vclib/ccvs/rcsparse/test-data/empty-file.out
Normal file
6
lib/vclib/ccvs/rcsparse/test-data/empty-file.out
Normal file
@@ -0,0 +1,6 @@
|
||||
set_locking('strict')
|
||||
set_comment('# ')
|
||||
admin_completed()
|
||||
tree_completed()
|
||||
set_description('')
|
||||
parse_completed()
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -25,7 +25,7 @@ _tt = TextTools
|
||||
_idchar_list = map(chr, range(33, 127)) + map(chr, range(160, 256))
|
||||
_idchar_list.remove('$')
|
||||
_idchar_list.remove(',')
|
||||
#_idchar_list.remove('.') leave as part of 'num' symbol
|
||||
#_idchar_list.remove('.') # leave as part of 'num' symbol
|
||||
_idchar_list.remove(':')
|
||||
_idchar_list.remove(';')
|
||||
_idchar_list.remove('@')
|
||||
@@ -41,10 +41,10 @@ _T_STRING_START = 40
|
||||
_T_STRING_SPAN = 60
|
||||
_T_STRING_END = 70
|
||||
|
||||
_E_COMPLETE = 100 # ended on a complete token
|
||||
_E_TOKEN = 110 # ended mid-token
|
||||
_E_STRING_SPAN = 130 # ended within a string
|
||||
_E_STRING_END = 140 # ended with string-end ('@') (could be mid-@@)
|
||||
_E_COMPLETE = 100 # ended on a complete token
|
||||
_E_TOKEN = 110 # ended mid-token
|
||||
_E_STRING_SPAN = 130 # ended within a string
|
||||
_E_STRING_END = 140 # ended with string-end ('@') (could be mid-@@)
|
||||
|
||||
_SUCCESS = +100
|
||||
|
||||
@@ -65,7 +65,7 @@ class _mxTokenStream:
|
||||
# note: we use a multiple of a standard block size
|
||||
CHUNK_SIZE = 192 * 512 # about 100k
|
||||
|
||||
# CHUNK_SIZE = 5 # for debugging, make the function grind...
|
||||
# CHUNK_SIZE = 5 # for debugging, make the function grind...
|
||||
|
||||
def __init__(self, file):
|
||||
self.rcsfile = file
|
||||
@@ -83,40 +83,84 @@ class _mxTokenStream:
|
||||
|
||||
# construct a tag table which refers to the buffer we need to parse.
|
||||
table = (
|
||||
# ignore whitespace. with or without whitespace, move to the next rule.
|
||||
#1: ignore whitespace. with or without whitespace, move to the next rule.
|
||||
(None, _tt.AllInSet, _tt.whitespace_set, +1),
|
||||
|
||||
#2
|
||||
(_E_COMPLETE, _tt.EOF + _tt.AppendTagobj, _tt.Here, +1, _SUCCESS),
|
||||
|
||||
# accumulate token text and exit, or move to the next rule.
|
||||
#3: accumulate token text and exit, or move to the next rule.
|
||||
(_UNUSED, _tt.AllInSet + _tt.AppendMatch, _idchar_set, +2),
|
||||
|
||||
#4
|
||||
(_E_TOKEN, _tt.EOF + _tt.AppendTagobj, _tt.Here, -3, _SUCCESS),
|
||||
|
||||
# single character tokens exit immediately, or move to the next rule
|
||||
#5: single character tokens exit immediately, or move to the next rule
|
||||
(_UNUSED, _tt.IsInSet + _tt.AppendMatch, _onechar_token_set, +2),
|
||||
|
||||
#6
|
||||
(_E_COMPLETE, _tt.EOF + _tt.AppendTagobj, _tt.Here, -5, _SUCCESS),
|
||||
|
||||
# if this isn't an '@' symbol, then we have a syntax error (go to a
|
||||
#7: if this isn't an '@' symbol, then we have a syntax error (go to a
|
||||
# negative index to indicate that condition). otherwise, suck it up
|
||||
# and move to the next rule.
|
||||
(_T_STRING_START, _tt.Is + _tt.AppendTagobj, '@'),
|
||||
|
||||
#8
|
||||
(None, _tt.Is, '@', +4, +1),
|
||||
#9
|
||||
(buf, _tt.Is, '@', +1, -1),
|
||||
#10
|
||||
(_T_STRING_END, _tt.Skip + _tt.AppendTagobj, 0, 0, +1),
|
||||
#11
|
||||
(_E_STRING_END, _tt.EOF + _tt.AppendTagobj, _tt.Here, -10, _SUCCESS),
|
||||
|
||||
#12
|
||||
(_E_STRING_SPAN, _tt.EOF + _tt.AppendTagobj, _tt.Here, +1, _SUCCESS),
|
||||
|
||||
# suck up everything that isn't an AT. go to next rule to look for EOF
|
||||
#13: suck up everything that isn't an AT. go to next rule to look for EOF
|
||||
(buf, _tt.AllInSet, _not_at_set, 0, +1),
|
||||
|
||||
# go back to look for double AT if we aren't at the end of the string
|
||||
#14: go back to look for double AT if we aren't at the end of the string
|
||||
(_E_STRING_SPAN, _tt.EOF + _tt.AppendTagobj, _tt.Here, -6, _SUCCESS),
|
||||
)
|
||||
|
||||
# Fast, texttools may be, but it's somewhat lacking in clarity.
|
||||
# Here's an attempt to document the logic encoded in the table above:
|
||||
#
|
||||
# Flowchart:
|
||||
# _____
|
||||
# / /\
|
||||
# 1 -> 2 -> 3 -> 5 -> 7 -> 8 -> 9 -> 10 -> 11
|
||||
# | \/ \/ \/ /\ \/
|
||||
# \ 4 6 12 14 /
|
||||
# \_______/_____/ \ / /
|
||||
# \ 13 /
|
||||
# \__________________________________________/
|
||||
#
|
||||
# #1: Skip over any whitespace.
|
||||
# #2: If now EOF, exit with code _E_COMPLETE.
|
||||
# #3: If we have a series of characters in _idchar_set, then:
|
||||
# #4: Output them as a token, and go back to #1.
|
||||
# #5: If we have a character in _onechar_token_set, then:
|
||||
# #6: Output it as a token, and go back to #1.
|
||||
# #7: If we do not have an '@', then error.
|
||||
# If we do, then log a _T_STRING_START and continue.
|
||||
# #8: If we have another '@', continue on to #9. Otherwise:
|
||||
# #12: If now EOF, exit with code _E_STRING_SPAN.
|
||||
# #13: Record the slice up to the next '@' (or EOF).
|
||||
# #14: If now EOF, exit with code _E_STRING_SPAN.
|
||||
# Otherwise, go back to #8.
|
||||
# #9: If we have another '@', then we've just seen an escaped
|
||||
# (by doubling) '@' within an @-string. Record a slice including
|
||||
# just one '@' character, and jump back to #8.
|
||||
# Otherwise, we've *either* seen the terminating '@' of an @-string,
|
||||
# *or* we've seen one half of an escaped @@ sequence that just
|
||||
# happened to be split over a chunk boundary - in either case,
|
||||
# we continue on to #10.
|
||||
# #10: Log a _T_STRING_END.
|
||||
# #11: If now EOF, exit with _E_STRING_END. Otherwise, go back to #1.
|
||||
|
||||
success, taglist, idx = _tt.tag(buf, table, start)
|
||||
|
||||
if not success:
|
||||
@@ -279,16 +323,11 @@ class _mxTokenStream:
|
||||
def match(self, match):
|
||||
if self.tokens:
|
||||
token = self.tokens.pop()
|
||||
if token != match:
|
||||
raise RuntimeError, ('Unexpected parsing error in RCS file.\n'
|
||||
'Expected token: %s, but saw: %s'
|
||||
% (match, token))
|
||||
else:
|
||||
token = self.get()
|
||||
if token != match:
|
||||
raise RuntimeError, ('Unexpected parsing error in RCS file.\n'
|
||||
'Expected token: %s, but saw: %s'
|
||||
% (match, token))
|
||||
|
||||
if token != match:
|
||||
raise common.RCSExpected(token, match)
|
||||
|
||||
def unget(self, token):
|
||||
self.tokens.append(token)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
@@ -10,743 +10,80 @@
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
"Version Control lib driver for locally accessible Subversion repositories"
|
||||
"Version Control lib driver for Subversion repositories"
|
||||
|
||||
import vclib
|
||||
import os
|
||||
import os.path
|
||||
import stat
|
||||
import string
|
||||
import cStringIO
|
||||
import signal
|
||||
import shutil
|
||||
import time
|
||||
import tempfile
|
||||
import popen
|
||||
import re
|
||||
from svn import fs, repos, core, delta
|
||||
import urllib
|
||||
|
||||
_re_url = re.compile('^(http|https|file|svn|svn\+[^:]+)://')
|
||||
|
||||
### Require Subversion 1.2.0 or better.
|
||||
if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 2, 0):
|
||||
raise Exception, "Version requirement not met (needs 1.2.0 or better)"
|
||||
|
||||
|
||||
def _allow_all(root, path, pool):
|
||||
"""Generic authz_read_func that permits access to all paths"""
|
||||
return 1
|
||||
|
||||
|
||||
def _fs_path_join(base, relative):
|
||||
# Subversion filesystem paths are '/'-delimited, regardless of OS.
|
||||
joined_path = base + '/' + relative
|
||||
parts = filter(None, string.split(joined_path, '/'))
|
||||
return string.join(parts, '/')
|
||||
|
||||
|
||||
def _cleanup_path(path):
|
||||
"""Return a cleaned-up Subversion filesystem path"""
|
||||
return string.join(filter(None, string.split(path, '/')), '/')
|
||||
|
||||
|
||||
def _compare_paths(path1, path2):
|
||||
path1_len = len (path1);
|
||||
path2_len = len (path2);
|
||||
min_len = min(path1_len, path2_len)
|
||||
i = 0
|
||||
|
||||
# Are the paths exactly the same?
|
||||
if path1 == path2:
|
||||
return 0
|
||||
|
||||
# Skip past common prefix
|
||||
while (i < min_len) and (path1[i] == path2[i]):
|
||||
i = i + 1
|
||||
|
||||
# Children of paths are greater than their parents, but less than
|
||||
# greater siblings of their parents
|
||||
char1 = '\0'
|
||||
char2 = '\0'
|
||||
if (i < path1_len):
|
||||
char1 = path1[i]
|
||||
if (i < path2_len):
|
||||
char2 = path2[i]
|
||||
|
||||
if (char1 == '/') and (i == path2_len):
|
||||
return 1
|
||||
if (char2 == '/') and (i == path1_len):
|
||||
return -1
|
||||
if (i < path1_len) and (char1 == '/'):
|
||||
return -1
|
||||
if (i < path2_len) and (char2 == '/'):
|
||||
return 1
|
||||
|
||||
# Common prefix was skipped above, next character is compared to
|
||||
# determine order
|
||||
return cmp(char1, char2)
|
||||
|
||||
|
||||
def _datestr_to_date(datestr, pool):
|
||||
if datestr is None:
|
||||
return None
|
||||
return core.svn_time_from_cstring(datestr, pool) / 1000000
|
||||
|
||||
|
||||
def _fs_rev_props(fsptr, rev, pool):
|
||||
author = fs.revision_prop(fsptr, rev, core.SVN_PROP_REVISION_AUTHOR, pool)
|
||||
msg = fs.revision_prop(fsptr, rev, core.SVN_PROP_REVISION_LOG, pool)
|
||||
date = fs.revision_prop(fsptr, rev, core.SVN_PROP_REVISION_DATE, pool)
|
||||
return date, author, msg
|
||||
|
||||
|
||||
def date_from_rev(svnrepos, rev):
|
||||
if (rev < 0) or (rev > svnrepos.youngest):
|
||||
raise vclib.InvalidRevision(rev)
|
||||
datestr = fs.revision_prop(svnrepos.fs_ptr, rev,
|
||||
core.SVN_PROP_REVISION_DATE, svnrepos.pool)
|
||||
return _datestr_to_date(datestr, svnrepos.pool)
|
||||
|
||||
|
||||
def get_location(svnrepos, path, rev, old_rev):
|
||||
def _canonicalize_path(path):
|
||||
import svn.core
|
||||
try:
|
||||
results = repos.svn_repos_trace_node_locations(svnrepos.fs_ptr, path,
|
||||
rev, [old_rev],
|
||||
_allow_all, svnrepos.pool)
|
||||
except core.SubversionException, e:
|
||||
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
|
||||
raise vclib.ItemNotFound(path)
|
||||
raise
|
||||
|
||||
try:
|
||||
old_path = results[old_rev]
|
||||
except KeyError:
|
||||
raise vclib.ItemNotFound(path)
|
||||
|
||||
return _cleanup_path(old_path)
|
||||
|
||||
|
||||
def last_rev(svnrepos, path, peg_revision, limit_revision=None):
|
||||
"""Given PATH, known to exist in PEG_REVISION, find the youngest
|
||||
revision older than, or equal to, LIMIT_REVISION in which path
|
||||
exists. Return that revision, and the path at which PATH exists in
|
||||
that revision."""
|
||||
|
||||
# Here's the plan, man. In the trivial case (where PEG_REVISION is
|
||||
# the same as LIMIT_REVISION), this is a no-brainer. If
|
||||
# LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's
|
||||
# history tracing code to find the right location. If, however,
|
||||
# LIMIT_REVISION is younger than PEG_REVISION, we suffer from
|
||||
# Subversion's lack of forward history searching. Our workaround,
|
||||
# ugly as it may be, involves a binary search through the revisions
|
||||
# between PEG_REVISION and LIMIT_REVISION to find our last live
|
||||
# revision.
|
||||
peg_revision = svnrepos._getrev(peg_revision)
|
||||
limit_revision = svnrepos._getrev(limit_revision)
|
||||
try:
|
||||
if peg_revision == limit_revision:
|
||||
return peg_revision, path
|
||||
elif peg_revision > limit_revision:
|
||||
fsroot = svnrepos._getroot(peg_revision)
|
||||
history = fs.node_history(fsroot, path, svnrepos.scratch_pool)
|
||||
while history:
|
||||
path, peg_revision = fs.history_location(history,
|
||||
svnrepos.scratch_pool);
|
||||
if peg_revision <= limit_revision:
|
||||
return max(peg_revision, limit_revision), _cleanup_path(path)
|
||||
history = fs.history_prev(history, 1, svnrepos.scratch_pool)
|
||||
return peg_revision, _cleanup_path(path)
|
||||
return svn.core.svn_path_canonicalize(path)
|
||||
except AttributeError: # svn_path_canonicalize() appeared in 1.4.0 bindings
|
||||
# There's so much more that we *could* do here, but if we're
|
||||
# here at all its because there's a really old Subversion in
|
||||
# place, and those older Subversion versions cared quite a bit
|
||||
# less about the specifics of path canonicalization.
|
||||
if re.search(_re_url, path):
|
||||
return path.rstrip('/')
|
||||
else:
|
||||
### Warning: this is *not* an example of good pool usage.
|
||||
orig_id = fs.node_id(svnrepos._getroot(peg_revision), path,
|
||||
svnrepos.scratch_pool)
|
||||
while peg_revision != limit_revision:
|
||||
mid = (peg_revision + 1 + limit_revision) / 2
|
||||
try:
|
||||
mid_id = fs.node_id(svnrepos._getroot(mid), path,
|
||||
svnrepos.scratch_pool)
|
||||
except core.SubversionException, e:
|
||||
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
|
||||
cmp = -1
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
### Not quite right. Need a comparison function that only returns
|
||||
### true when the two nodes are the same copy, not just related.
|
||||
cmp = fs.compare_ids(orig_id, mid_id)
|
||||
|
||||
if cmp in (0, 1):
|
||||
peg_revision = mid
|
||||
else:
|
||||
limit_revision = mid - 1
|
||||
|
||||
return peg_revision, path
|
||||
finally:
|
||||
svnrepos._scratch_clear()
|
||||
return os.path.normpath(path)
|
||||
|
||||
|
||||
def created_rev(svnrepos, full_name, rev):
|
||||
fsroot = svnrepos._getroot(rev)
|
||||
return fs.node_created_rev(fsroot, full_name, svnrepos.pool)
|
||||
|
||||
|
||||
class Revision(vclib.Revision):
|
||||
"Hold state for each revision's log entry."
|
||||
def __init__(self, rev, date, author, msg, size,
|
||||
filename, copy_path, copy_rev):
|
||||
vclib.Revision.__init__(self, rev, str(rev), date, author, None, msg, size)
|
||||
self.filename = filename
|
||||
self.copy_path = copy_path
|
||||
self.copy_rev = copy_rev
|
||||
|
||||
|
||||
class NodeHistory:
|
||||
def __init__(self, fs_ptr, show_all_logs):
|
||||
self.histories = {}
|
||||
self.fs_ptr = fs_ptr
|
||||
self.show_all_logs = show_all_logs
|
||||
|
||||
def add_history(self, path, revision, pool):
|
||||
# If filtering, only add the path and revision to the histories
|
||||
# list if they were actually changed in this revision (where
|
||||
# change means the path itself was changed, or one of its parents
|
||||
# was copied). This is useful for omitting bubble-up directory
|
||||
# changes.
|
||||
if not self.show_all_logs:
|
||||
rev_root = fs.revision_root(self.fs_ptr, revision, pool)
|
||||
changed_paths = fs.paths_changed(rev_root, pool)
|
||||
paths = changed_paths.keys()
|
||||
if path not in paths:
|
||||
# Look for a copied parent
|
||||
test_path = path
|
||||
found = 0
|
||||
subpool = core.svn_pool_create(pool)
|
||||
while 1:
|
||||
core.svn_pool_clear(subpool)
|
||||
off = string.rfind(test_path, '/')
|
||||
if off < 0:
|
||||
break
|
||||
test_path = test_path[0:off]
|
||||
if test_path in paths:
|
||||
copyfrom_rev, copyfrom_path = \
|
||||
fs.copied_from(rev_root, test_path, subpool)
|
||||
if copyfrom_rev >= 0 and copyfrom_path:
|
||||
found = 1
|
||||
break
|
||||
core.svn_pool_destroy(subpool)
|
||||
if not found:
|
||||
return
|
||||
self.histories[revision] = _cleanup_path(path)
|
||||
|
||||
def canonicalize_rootpath(rootpath):
|
||||
# Try to canonicalize the rootpath using Subversion semantics.
|
||||
rootpath = _canonicalize_path(rootpath)
|
||||
|
||||
def _get_history(svnrepos, full_name, rev, options={}):
|
||||
fsroot = svnrepos._getroot(rev)
|
||||
show_all_logs = options.get('svn_show_all_dir_logs', 0)
|
||||
if not show_all_logs:
|
||||
# See if the path is a file or directory.
|
||||
kind = fs.check_path(fsroot, full_name, svnrepos.pool)
|
||||
if kind is core.svn_node_file:
|
||||
show_all_logs = 1
|
||||
|
||||
# Instantiate a NodeHistory collector object.
|
||||
history = NodeHistory(svnrepos.fs_ptr, show_all_logs)
|
||||
# ViewVC's support for local repositories is more complete and more
|
||||
# performant than its support for remote ones, so if we're on a
|
||||
# Unix-y system and we have a file:/// URL, convert it to a local
|
||||
# path instead.
|
||||
if os.name == 'posix':
|
||||
rootpath_lower = rootpath.lower()
|
||||
if rootpath_lower in ['file://localhost',
|
||||
'file://localhost/',
|
||||
'file://',
|
||||
'file:///'
|
||||
]:
|
||||
return '/'
|
||||
if rootpath_lower.startswith('file://localhost/'):
|
||||
rootpath = os.path.normpath(urllib.unquote(rootpath[16:]))
|
||||
elif rootpath_lower.startswith('file:///'):
|
||||
rootpath = os.path.normpath(urllib.unquote(rootpath[7:]))
|
||||
|
||||
# Do we want to cross copy history?
|
||||
cross_copies = options.get('svn_cross_copies', 0)
|
||||
|
||||
# Get the history items for PATH.
|
||||
repos.svn_repos_history(svnrepos.fs_ptr, full_name, history.add_history,
|
||||
1, rev, cross_copies, svnrepos.pool)
|
||||
return history.histories
|
||||
# Ensure that we have an absolute path (or URL), and return.
|
||||
if not re.search(_re_url, rootpath):
|
||||
assert os.path.isabs(rootpath)
|
||||
return rootpath
|
||||
|
||||
|
||||
class ChangedPath:
|
||||
def __init__(self, filename, pathtype, prop_mods, text_mods,
|
||||
base_path, base_rev, action, is_copy):
|
||||
self.filename = filename
|
||||
self.pathtype = pathtype
|
||||
self.prop_mods = prop_mods
|
||||
self.text_mods = text_mods
|
||||
self.base_path = base_path
|
||||
self.base_rev = base_rev
|
||||
self.action = action
|
||||
self.is_copy = is_copy
|
||||
|
||||
|
||||
def get_revision_info(svnrepos, rev):
|
||||
fsroot = svnrepos._getroot(rev)
|
||||
|
||||
# Get the changes for the revision
|
||||
editor = repos.ChangeCollector(svnrepos.fs_ptr, fsroot, svnrepos.pool)
|
||||
e_ptr, e_baton = delta.make_editor(editor, svnrepos.pool)
|
||||
repos.svn_repos_replay(fsroot, e_ptr, e_baton, svnrepos.pool)
|
||||
changes = editor.get_changes()
|
||||
changedpaths = {}
|
||||
|
||||
# Copy the Subversion changes into a new hash, converting them into
|
||||
# ChangedPath objects.
|
||||
for path in changes.keys():
|
||||
change = changes[path]
|
||||
if change.path:
|
||||
change.path = _cleanup_path(change.path)
|
||||
if change.base_path:
|
||||
change.base_path = _cleanup_path(change.base_path)
|
||||
is_copy = 0
|
||||
if not hasattr(change, 'action'): # new to subversion 1.4.0
|
||||
action = 'modified'
|
||||
if not change.path:
|
||||
action = 'deleted'
|
||||
elif change.added:
|
||||
action = 'added'
|
||||
replace_check_path = path
|
||||
if change.base_path and change.base_rev:
|
||||
replace_check_path = change.base_path
|
||||
if changedpaths.has_key(replace_check_path) \
|
||||
and changedpaths[replace_check_path].action == 'deleted':
|
||||
action = 'replaced'
|
||||
else:
|
||||
if change.action == repos.CHANGE_ACTION_ADD:
|
||||
action = 'added'
|
||||
elif change.action == repos.CHANGE_ACTION_DELETE:
|
||||
action = 'deleted'
|
||||
elif change.action == repos.CHANGE_ACTION_REPLACE:
|
||||
action = 'replaced'
|
||||
else:
|
||||
action = 'modified'
|
||||
if (action == 'added' or action == 'replaced') \
|
||||
and change.base_path \
|
||||
and change.base_rev:
|
||||
is_copy = 1
|
||||
if change.item_kind == core.svn_node_dir:
|
||||
pathtype = vclib.DIR
|
||||
elif change.item_kind == core.svn_node_file:
|
||||
pathtype = vclib.FILE
|
||||
else:
|
||||
pathtype = None
|
||||
changedpaths[path] = ChangedPath(path, pathtype, change.prop_changes,
|
||||
change.text_changed, change.base_path,
|
||||
change.base_rev, action, is_copy)
|
||||
|
||||
# Actually, what we want is a sorted list of ChangedPath objects.
|
||||
change_items = changedpaths.values()
|
||||
change_items.sort(lambda a, b: _compare_paths(a.filename, b.filename))
|
||||
|
||||
# Now get the revision property info. Would use
|
||||
# editor.get_root_props(), but something is broken there...
|
||||
datestr, author, msg = _fs_rev_props(svnrepos.fs_ptr, rev, svnrepos.pool)
|
||||
date = _datestr_to_date(datestr, svnrepos.pool)
|
||||
|
||||
return date, author, msg, change_items
|
||||
|
||||
|
||||
def _log_helper(svnrepos, rev, path, pool):
|
||||
rev_root = fs.revision_root(svnrepos.fs_ptr, rev, pool)
|
||||
|
||||
# Was this path@rev the target of a copy?
|
||||
copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, path, pool)
|
||||
|
||||
# Assemble our LogEntry
|
||||
datestr, author, msg = _fs_rev_props(svnrepos.fs_ptr, rev, pool)
|
||||
date = _datestr_to_date(datestr, pool)
|
||||
if fs.is_file(rev_root, path, pool):
|
||||
size = fs.file_length(rev_root, path, pool)
|
||||
def expand_root_parent(parent_path):
|
||||
roots = {}
|
||||
if re.search(_re_url, parent_path):
|
||||
pass
|
||||
else:
|
||||
size = None
|
||||
entry = Revision(rev, date, author, msg, size, path,
|
||||
copyfrom_path and _cleanup_path(copyfrom_path),
|
||||
copyfrom_rev)
|
||||
return entry
|
||||
|
||||
# Any subdirectories of PARENT_PATH which themselves have a child
|
||||
# "format" are returned as roots.
|
||||
assert os.path.isabs(parent_path)
|
||||
subpaths = os.listdir(parent_path)
|
||||
for rootname in subpaths:
|
||||
rootpath = os.path.join(parent_path, rootname)
|
||||
if os.path.exists(os.path.join(rootpath, "format")):
|
||||
roots[rootname] = canonicalize_rootpath(rootpath)
|
||||
return roots
|
||||
|
||||
def _fetch_log(svnrepos, full_name, which_rev, options, pool):
|
||||
revs = []
|
||||
|
||||
if options.get('svn_latest_log', 0):
|
||||
rev = _log_helper(svnrepos, which_rev, full_name, pool)
|
||||
if rev:
|
||||
revs.append(rev)
|
||||
def SubversionRepository(name, rootpath, authorizer, utilities, config_dir):
|
||||
rootpath = canonicalize_rootpath(rootpath)
|
||||
if re.search(_re_url, rootpath):
|
||||
import svn_ra
|
||||
return svn_ra.RemoteSubversionRepository(name, rootpath, authorizer,
|
||||
utilities, config_dir)
|
||||
else:
|
||||
history_set = _get_history(svnrepos, full_name, which_rev, options)
|
||||
history_revs = history_set.keys()
|
||||
history_revs.sort()
|
||||
history_revs.reverse()
|
||||
subpool = core.svn_pool_create(pool)
|
||||
for history_rev in history_revs:
|
||||
core.svn_pool_clear(subpool)
|
||||
rev = _log_helper(svnrepos, history_rev, history_set[history_rev],
|
||||
subpool)
|
||||
if rev:
|
||||
revs.append(rev)
|
||||
core.svn_pool_destroy(subpool)
|
||||
return revs
|
||||
|
||||
|
||||
def _get_last_history_rev(fsroot, path, pool):
|
||||
history = fs.node_history(fsroot, path, pool)
|
||||
history = fs.history_prev(history, 0, pool)
|
||||
history_path, history_rev = fs.history_location(history, pool);
|
||||
return history_rev
|
||||
|
||||
|
||||
def get_logs(svnrepos, full_name, rev, files):
|
||||
fsroot = svnrepos._getroot(rev)
|
||||
subpool = core.svn_pool_create(svnrepos.pool)
|
||||
for file in files:
|
||||
core.svn_pool_clear(subpool)
|
||||
path = _fs_path_join(full_name, file.name)
|
||||
rev = _get_last_history_rev(fsroot, path, subpool)
|
||||
datestr, author, msg = _fs_rev_props(svnrepos.fs_ptr, rev, subpool)
|
||||
date = _datestr_to_date(datestr, subpool)
|
||||
file.rev = str(rev)
|
||||
file.date = date
|
||||
file.author = author
|
||||
file.log = msg
|
||||
if file.kind == vclib.FILE:
|
||||
file.size = fs.file_length(fsroot, path, subpool)
|
||||
core.svn_pool_destroy(subpool)
|
||||
|
||||
|
||||
def get_youngest_revision(svnrepos):
|
||||
return svnrepos.youngest
|
||||
|
||||
def temp_checkout(svnrepos, path, rev, pool):
|
||||
"""Check out file revision to temporary file"""
|
||||
temp = tempfile.mktemp()
|
||||
fp = open(temp, 'wb')
|
||||
try:
|
||||
root = svnrepos._getroot(rev)
|
||||
stream = fs.file_contents(root, path, pool)
|
||||
try:
|
||||
while 1:
|
||||
chunk = core.svn_stream_read(stream, core.SVN_STREAM_CHUNK_SIZE)
|
||||
if not chunk:
|
||||
break
|
||||
fp.write(chunk)
|
||||
finally:
|
||||
core.svn_stream_close(stream)
|
||||
finally:
|
||||
fp.close()
|
||||
return temp
|
||||
|
||||
class FileContentsPipe:
|
||||
def __init__(self, root, path, pool):
|
||||
self._pool = core.svn_pool_create(pool)
|
||||
self._stream = fs.file_contents(root, path, self._pool)
|
||||
self._eof = 0
|
||||
|
||||
def __del__(self):
|
||||
core.svn_pool_destroy(self._pool)
|
||||
|
||||
def read(self, len=None):
|
||||
chunk = None
|
||||
if not self._eof:
|
||||
if len is None:
|
||||
buffer = cStringIO.StringIO()
|
||||
try:
|
||||
while 1:
|
||||
hunk = core.svn_stream_read(self._stream, 8192)
|
||||
if not hunk:
|
||||
break
|
||||
buffer.write(hunk)
|
||||
chunk = buffer.getvalue()
|
||||
finally:
|
||||
buffer.close()
|
||||
|
||||
else:
|
||||
chunk = core.svn_stream_read(self._stream, len)
|
||||
if not chunk:
|
||||
self._eof = 1
|
||||
return chunk
|
||||
|
||||
def readline(self):
|
||||
chunk = None
|
||||
if not self._eof:
|
||||
chunk, self._eof = core.svn_stream_readline(self._stream, '\n',
|
||||
self._pool)
|
||||
if not self._eof:
|
||||
chunk = chunk + '\n'
|
||||
if not chunk:
|
||||
self._eof = 1
|
||||
return chunk
|
||||
|
||||
def readlines(self):
|
||||
lines = []
|
||||
while True:
|
||||
line = self.readline()
|
||||
if not line:
|
||||
break
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
def close(self):
|
||||
return core.svn_stream_close(self._stream)
|
||||
|
||||
def eof(self):
|
||||
return self._eof
|
||||
|
||||
|
||||
_re_blameinfo = re.compile(r"\s*(\d+)\s*(.*)")
|
||||
|
||||
class BlameSource:
|
||||
def __init__(self, svn_client_path, rootpath, fs_path, rev, first_rev):
|
||||
self.idx = -1
|
||||
self.line_number = 1
|
||||
self.last = None
|
||||
self.first_rev = first_rev
|
||||
|
||||
# Do a little dance to get a URL that works in both Unix-y and
|
||||
# Windows worlds.
|
||||
rootpath = os.path.abspath(rootpath)
|
||||
if rootpath and rootpath[0] != '/':
|
||||
rootpath = '/' + rootpath
|
||||
if os.sep != '/':
|
||||
rootpath = string.replace(rootpath, os.sep, '/')
|
||||
|
||||
# Make a read-only temporary directory for Subversion to use as
|
||||
# its runtime config dir. (Read-only because that will prevent
|
||||
# Subversion from fleshing out all the default runtime config
|
||||
# contents.)
|
||||
self.config_dir = self._mkdtemp()
|
||||
os.chmod(self.config_dir, stat.S_IRUSR | stat.S_IXUSR)
|
||||
|
||||
url = 'file://' + string.join([rootpath, fs_path], "/")
|
||||
fp = popen.popen(svn_client_path,
|
||||
("blame",
|
||||
"-r%d" % int(rev),
|
||||
"--non-interactive",
|
||||
"--config-dir", self.config_dir,
|
||||
"%s@%d" % (url, int(rev))),
|
||||
'rb', 1)
|
||||
self.fp = fp
|
||||
|
||||
def _mkdtemp(self):
|
||||
### FIXME: When we require Python 2.3, this can go away.
|
||||
for i in range(10):
|
||||
dir = tempfile.mktemp()
|
||||
try:
|
||||
os.mkdir(dir, 0700)
|
||||
return dir
|
||||
except OSError, e:
|
||||
if e.errno == errno.EEXIST:
|
||||
continue # try again
|
||||
raise
|
||||
raise IOError, (errno.EEXIST, "No usable temporary directory name found")
|
||||
|
||||
def __getitem__(self, idx):
|
||||
if idx == self.idx:
|
||||
return self.last
|
||||
if idx != self.idx + 1:
|
||||
raise BlameSequencingError()
|
||||
line = self.fp.readline()
|
||||
if not line:
|
||||
raise IndexError("No more annotations")
|
||||
m = _re_blameinfo.match(line[:17])
|
||||
if not m:
|
||||
raise vclib.Error("Could not parse blame output at line %i\n%s"
|
||||
% (idx+1, line))
|
||||
rev, author = m.groups()
|
||||
text = line[18:]
|
||||
rev = int(rev)
|
||||
prev_rev = None
|
||||
if rev > self.first_rev:
|
||||
prev_rev = rev - 1
|
||||
item = _item(text=text, line_number=idx+1, rev=rev,
|
||||
prev_rev=prev_rev, author=author, date=None)
|
||||
self.last = item
|
||||
self.idx = idx
|
||||
return item
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
if self.config_dir:
|
||||
shutil.rmtree(self.config_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class BlameSequencingError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SubversionRepository(vclib.Repository):
|
||||
def __init__(self, name, rootpath, svn_path):
|
||||
if not os.path.isdir(rootpath):
|
||||
raise vclib.ReposNotFound(name)
|
||||
|
||||
# Initialize some stuff.
|
||||
self.pool = None
|
||||
self.apr_init = 0
|
||||
self.rootpath = rootpath
|
||||
self.name = name
|
||||
self.svn_client_path = os.path.normpath(os.path.join(svn_path, 'svn'))
|
||||
|
||||
# Register a handler for SIGTERM so we can have a chance to
|
||||
# cleanup. If ViewVC takes too long to start generating CGI
|
||||
# output, Apache will grow impatient and SIGTERM it. While we
|
||||
# don't mind getting told to bail, we want to gracefully close the
|
||||
# repository before we bail.
|
||||
def _sigterm_handler(signum, frame, self=self):
|
||||
self._close()
|
||||
sys.exit(-1)
|
||||
try:
|
||||
signal.signal(signal.SIGTERM, _sigterm_handler)
|
||||
except ValueError:
|
||||
# This is probably "ValueError: signal only works in main
|
||||
# thread", which will get thrown by the likes of mod_python
|
||||
# when trying to install a signal handler from a thread that
|
||||
# isn't the main one. We'll just not care.
|
||||
pass
|
||||
|
||||
# Initialize APR and get our top-level pool.
|
||||
core.apr_initialize()
|
||||
self.apr_init = 1
|
||||
self.pool = core.svn_pool_create(None)
|
||||
self.scratch_pool = core.svn_pool_create(self.pool)
|
||||
|
||||
# Open the repository and init some other variables.
|
||||
self.repos = repos.svn_repos_open(rootpath, self.pool)
|
||||
self.fs_ptr = repos.svn_repos_fs(self.repos)
|
||||
self.youngest = fs.youngest_rev(self.fs_ptr, self.pool)
|
||||
self._fsroots = {}
|
||||
|
||||
def __del__(self):
|
||||
self._close()
|
||||
|
||||
def _close(self):
|
||||
if self.pool:
|
||||
core.svn_pool_destroy(self.pool)
|
||||
self.pool = None
|
||||
if self.apr_init:
|
||||
core.apr_terminate()
|
||||
self.apr_init = 0
|
||||
|
||||
def _scratch_clear(self):
|
||||
core.svn_pool_clear(self.scratch_pool)
|
||||
|
||||
def itemtype(self, path_parts, rev):
|
||||
rev = self._getrev(rev)
|
||||
basepath = self._getpath(path_parts)
|
||||
kind = fs.check_path(self._getroot(rev), basepath, self.scratch_pool)
|
||||
self._scratch_clear()
|
||||
if kind == core.svn_node_dir:
|
||||
return vclib.DIR
|
||||
if kind == core.svn_node_file:
|
||||
return vclib.FILE
|
||||
raise vclib.ItemNotFound(path_parts)
|
||||
|
||||
def openfile(self, path_parts, rev):
|
||||
path = self._getpath(path_parts)
|
||||
rev = self._getrev(rev)
|
||||
fsroot = self._getroot(rev)
|
||||
revision = str(_get_last_history_rev(fsroot, path, self.scratch_pool))
|
||||
self._scratch_clear()
|
||||
fp = FileContentsPipe(fsroot, path, self.pool)
|
||||
return fp, revision
|
||||
|
||||
def listdir(self, path_parts, rev, options):
|
||||
basepath = self._getpath(path_parts)
|
||||
if self.itemtype(path_parts, rev) != vclib.DIR:
|
||||
raise vclib.Error("Path '%s' is not a directory." % basepath)
|
||||
|
||||
rev = self._getrev(rev)
|
||||
fsroot = self._getroot(rev)
|
||||
dirents = fs.dir_entries(fsroot, basepath, self.scratch_pool)
|
||||
entries = [ ]
|
||||
for entry in dirents.values():
|
||||
if entry.kind == core.svn_node_dir:
|
||||
kind = vclib.DIR
|
||||
elif entry.kind == core.svn_node_file:
|
||||
kind = vclib.FILE
|
||||
entries.append(vclib.DirEntry(entry.name, kind))
|
||||
self._scratch_clear()
|
||||
return entries
|
||||
|
||||
def dirlogs(self, path_parts, rev, entries, options):
|
||||
get_logs(self, self._getpath(path_parts), self._getrev(rev), entries)
|
||||
|
||||
def itemlog(self, path_parts, rev, options):
|
||||
"""see vclib.Repository.itemlog docstring
|
||||
|
||||
Option values recognized by this implementation
|
||||
|
||||
svn_show_all_dir_logs
|
||||
boolean, default false. if set for a directory path, will include
|
||||
revisions where files underneath the directory have changed
|
||||
|
||||
svn_cross_copies
|
||||
boolean, default false. if set for a path created by a copy, will
|
||||
include revisions from before the copy
|
||||
|
||||
svn_latest_log
|
||||
boolean, default false. if set will return only newest single log
|
||||
entry
|
||||
"""
|
||||
path = self._getpath(path_parts)
|
||||
rev = self._getrev(rev)
|
||||
|
||||
revs = _fetch_log(self, path, rev, options, self.scratch_pool)
|
||||
self._scratch_clear()
|
||||
|
||||
revs.sort()
|
||||
prev = None
|
||||
for rev in revs:
|
||||
rev.prev = prev
|
||||
prev = rev
|
||||
|
||||
return revs
|
||||
|
||||
def annotate(self, path_parts, rev):
|
||||
path = self._getpath(path_parts)
|
||||
rev = self._getrev(rev)
|
||||
fsroot = self._getroot(rev)
|
||||
|
||||
history_set = _get_history(self, path, rev, {'svn_cross_copies': 1})
|
||||
history_revs = history_set.keys()
|
||||
history_revs.sort()
|
||||
revision = history_revs[-1]
|
||||
first_rev = history_revs[0]
|
||||
source = BlameSource(self.svn_client_path, self.rootpath,
|
||||
path, rev, first_rev)
|
||||
return source, revision
|
||||
|
||||
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
|
||||
p1 = self._getpath(path_parts1)
|
||||
p2 = self._getpath(path_parts2)
|
||||
r1 = self._getrev(rev1)
|
||||
r2 = self._getrev(rev2)
|
||||
args = vclib._diff_args(type, options)
|
||||
|
||||
try:
|
||||
temp1 = temp_checkout(self, p1, r1, self.pool)
|
||||
temp2 = temp_checkout(self, p2, r2, self.pool)
|
||||
info1 = p1, date_from_rev(self, r1), r1
|
||||
info2 = p2, date_from_rev(self, r2), r2
|
||||
return vclib._diff_fp(temp1, temp2, info1, info2, args)
|
||||
except vclib.svn.core.SubversionException, e:
|
||||
if e.apr_err == vclib.svn.core.SVN_ERR_FS_NOT_FOUND:
|
||||
raise vclib.InvalidRevision
|
||||
raise
|
||||
|
||||
def _getpath(self, path_parts):
|
||||
return string.join(path_parts, '/')
|
||||
|
||||
def _getrev(self, rev):
|
||||
if rev is None or rev == 'HEAD':
|
||||
return self.youngest
|
||||
try:
|
||||
rev = int(rev)
|
||||
except ValueError:
|
||||
raise vclib.InvalidRevision(rev)
|
||||
if (rev < 0) or (rev > self.youngest):
|
||||
raise vclib.InvalidRevision(rev)
|
||||
return rev
|
||||
|
||||
def _getroot(self, rev):
|
||||
try:
|
||||
return self._fsroots[rev]
|
||||
except KeyError:
|
||||
r = self._fsroots[rev] = fs.revision_root(self.fs_ptr, rev, self.pool)
|
||||
return r
|
||||
|
||||
class _item:
|
||||
def __init__(self, **kw):
|
||||
vars(self).update(kw)
|
||||
import svn_repos
|
||||
return svn_repos.LocalSubversionRepository(name, rootpath, authorizer,
|
||||
utilities, config_dir)
|
||||
|
||||
737
lib/vclib/svn/svn_ra.py
Normal file
737
lib/vclib/svn/svn_ra.py
Normal file
@@ -0,0 +1,737 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
"Version Control lib driver for remotely accessible Subversion repositories."
|
||||
|
||||
import vclib
|
||||
import sys
|
||||
import os
|
||||
import string
|
||||
import re
|
||||
import tempfile
|
||||
import time
|
||||
import urllib
|
||||
from svn_repos import Revision, SVNChangedPath, _datestr_to_date, \
|
||||
_compare_paths, _path_parts, _cleanup_path, \
|
||||
_rev2optrev, _fix_subversion_exception, \
|
||||
_split_revprops, _canonicalize_path
|
||||
from svn import core, delta, client, wc, ra
|
||||
|
||||
|
||||
### Require Subversion 1.3.1 or better. (for svn_ra_get_locations support)
|
||||
if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 3, 1):
|
||||
raise Exception, "Version requirement not met (needs 1.3.1 or better)"
|
||||
|
||||
|
||||
### BEGIN COMPATABILITY CODE ###
|
||||
|
||||
try:
|
||||
SVN_INVALID_REVNUM = core.SVN_INVALID_REVNUM
|
||||
except AttributeError: # The 1.4.x bindings are missing core.SVN_INVALID_REVNUM
|
||||
SVN_INVALID_REVNUM = -1
|
||||
|
||||
def list_directory(url, peg_rev, rev, flag, ctx):
|
||||
try:
|
||||
dirents, locks = client.svn_client_ls3(url, peg_rev, rev, flag, ctx)
|
||||
except TypeError: # 1.4.x bindings are goofed
|
||||
dirents = client.svn_client_ls3(None, url, peg_rev, rev, flag, ctx)
|
||||
locks = {}
|
||||
return dirents, locks
|
||||
|
||||
def get_directory_props(ra_session, path, rev):
|
||||
try:
|
||||
dirents, fetched_rev, props = ra.svn_ra_get_dir(ra_session, path, rev)
|
||||
except ValueError: # older bindings are goofed
|
||||
props = ra.svn_ra_get_dir(ra_session, path, rev)
|
||||
return props
|
||||
|
||||
def client_log(url, start_rev, end_rev, log_limit, include_changes,
|
||||
cross_copies, cb_func, ctx):
|
||||
include_changes = include_changes and 1 or 0
|
||||
cross_copies = cross_copies and 1 or 0
|
||||
try:
|
||||
client.svn_client_log4([url], start_rev, start_rev, end_rev,
|
||||
log_limit, include_changes, not cross_copies,
|
||||
0, None, cb_func, ctx)
|
||||
except AttributeError:
|
||||
# Wrap old svn_log_message_receiver_t interface with a
|
||||
# svn_log_entry_t one.
|
||||
def cb_convert(paths, revision, author, date, message, pool):
|
||||
class svn_log_entry_t:
|
||||
pass
|
||||
log_entry = svn_log_entry_t()
|
||||
log_entry.changed_paths = paths
|
||||
log_entry.revision = revision
|
||||
log_entry.revprops = { core.SVN_PROP_REVISION_LOG : message,
|
||||
core.SVN_PROP_REVISION_AUTHOR : author,
|
||||
core.SVN_PROP_REVISION_DATE : date,
|
||||
}
|
||||
cb_func(log_entry, pool)
|
||||
client.svn_client_log2([url], start_rev, end_rev, log_limit,
|
||||
include_changes, not cross_copies, cb_convert, ctx)
|
||||
|
||||
### END COMPATABILITY CODE ###
|
||||
|
||||
|
||||
class LogCollector:
|
||||
|
||||
def __init__(self, path, show_all_logs, lockinfo, access_check_func):
|
||||
# This class uses leading slashes for paths internally
|
||||
if not path:
|
||||
self.path = '/'
|
||||
else:
|
||||
self.path = path[0] == '/' and path or '/' + path
|
||||
self.logs = []
|
||||
self.show_all_logs = show_all_logs
|
||||
self.lockinfo = lockinfo
|
||||
self.access_check_func = access_check_func
|
||||
self.done = False
|
||||
|
||||
def add_log(self, log_entry, pool):
|
||||
if self.done:
|
||||
return
|
||||
paths = log_entry.changed_paths
|
||||
revision = log_entry.revision
|
||||
msg, author, date, revprops = _split_revprops(log_entry.revprops)
|
||||
|
||||
# Changed paths have leading slashes
|
||||
changed_paths = paths.keys()
|
||||
changed_paths.sort(lambda a, b: _compare_paths(a, b))
|
||||
this_path = None
|
||||
if self.path in changed_paths:
|
||||
this_path = self.path
|
||||
change = paths[self.path]
|
||||
if change.copyfrom_path:
|
||||
this_path = change.copyfrom_path
|
||||
for changed_path in changed_paths:
|
||||
if changed_path != self.path:
|
||||
# If a parent of our path was copied, our "next previous"
|
||||
# (huh?) path will exist elsewhere (under the copy source).
|
||||
if (string.rfind(self.path, changed_path) == 0) and \
|
||||
self.path[len(changed_path)] == '/':
|
||||
change = paths[changed_path]
|
||||
if change.copyfrom_path:
|
||||
this_path = change.copyfrom_path + self.path[len(changed_path):]
|
||||
if self.show_all_logs or this_path:
|
||||
if self.access_check_func is None \
|
||||
or self.access_check_func(self.path[1:], revision):
|
||||
entry = Revision(revision, date, author, msg, None, self.lockinfo,
|
||||
self.path[1:], None, None)
|
||||
self.logs.append(entry)
|
||||
else:
|
||||
self.done = True
|
||||
if this_path:
|
||||
self.path = this_path
|
||||
|
||||
def temp_checkout(svnrepos, path, rev):
|
||||
"""Check out file revision to temporary file"""
|
||||
temp = tempfile.mktemp()
|
||||
stream = core.svn_stream_from_aprfile(temp)
|
||||
url = svnrepos._geturl(path)
|
||||
client.svn_client_cat(core.Stream(stream), url, _rev2optrev(rev),
|
||||
svnrepos.ctx)
|
||||
core.svn_stream_close(stream)
|
||||
return temp
|
||||
|
||||
class SelfCleanFP:
|
||||
def __init__(self, path):
|
||||
self._fp = open(path, 'r')
|
||||
self._path = path
|
||||
self._eof = 0
|
||||
|
||||
def read(self, len=None):
|
||||
if len:
|
||||
chunk = self._fp.read(len)
|
||||
else:
|
||||
chunk = self._fp.read()
|
||||
if chunk == '':
|
||||
self._eof = 1
|
||||
return chunk
|
||||
|
||||
def readline(self):
|
||||
chunk = self._fp.readline()
|
||||
if chunk == '':
|
||||
self._eof = 1
|
||||
return chunk
|
||||
|
||||
def readlines(self):
|
||||
lines = self._fp.readlines()
|
||||
self._eof = 1
|
||||
return lines
|
||||
|
||||
def close(self):
|
||||
self._fp.close()
|
||||
os.remove(self._path)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def eof(self):
|
||||
return self._eof
|
||||
|
||||
|
||||
class RemoteSubversionRepository(vclib.Repository):
|
||||
def __init__(self, name, rootpath, authorizer, utilities, config_dir):
|
||||
self.name = name
|
||||
self.rootpath = rootpath
|
||||
self.auth = authorizer
|
||||
self.diff_cmd = utilities.diff or 'diff'
|
||||
self.config_dir = config_dir or None
|
||||
|
||||
# See if this repository is even viewable, authz-wise.
|
||||
if not vclib.check_root_access(self):
|
||||
raise vclib.ReposNotFound(name)
|
||||
|
||||
def open(self):
|
||||
# Setup the client context baton, complete with non-prompting authstuffs.
|
||||
# TODO: svn_cmdline_setup_auth_baton() is mo' better (when available)
|
||||
core.svn_config_ensure(self.config_dir)
|
||||
self.ctx = client.svn_client_create_context()
|
||||
self.ctx.auth_baton = core.svn_auth_open([
|
||||
client.svn_client_get_simple_provider(),
|
||||
client.svn_client_get_username_provider(),
|
||||
client.svn_client_get_ssl_server_trust_file_provider(),
|
||||
client.svn_client_get_ssl_client_cert_file_provider(),
|
||||
client.svn_client_get_ssl_client_cert_pw_file_provider(),
|
||||
])
|
||||
self.ctx.config = core.svn_config_get_config(self.config_dir)
|
||||
if self.config_dir is not None:
|
||||
core.svn_auth_set_parameter(self.ctx.auth_baton,
|
||||
core.SVN_AUTH_PARAM_CONFIG_DIR,
|
||||
self.config_dir)
|
||||
ra_callbacks = ra.svn_ra_callbacks_t()
|
||||
ra_callbacks.auth_baton = self.ctx.auth_baton
|
||||
self.ra_session = ra.svn_ra_open(self.rootpath, ra_callbacks, None,
|
||||
self.ctx.config)
|
||||
self.youngest = ra.svn_ra_get_latest_revnum(self.ra_session)
|
||||
self._dirent_cache = { }
|
||||
self._revinfo_cache = { }
|
||||
|
||||
# See if a universal read access determination can be made.
|
||||
if self.auth and self.auth.check_universal_access(self.name) == 1:
|
||||
self.auth = None
|
||||
|
||||
def rootname(self):
|
||||
return self.name
|
||||
|
||||
def rootpath(self):
|
||||
return self.rootpath
|
||||
|
||||
def roottype(self):
|
||||
return vclib.SVN
|
||||
|
||||
def authorizer(self):
|
||||
return self.auth
|
||||
|
||||
def itemtype(self, path_parts, rev):
|
||||
pathtype = None
|
||||
if not len(path_parts):
|
||||
pathtype = vclib.DIR
|
||||
else:
|
||||
path = self._getpath(path_parts)
|
||||
rev = self._getrev(rev)
|
||||
try:
|
||||
kind = ra.svn_ra_check_path(self.ra_session, path, rev)
|
||||
if kind == core.svn_node_file:
|
||||
pathtype = vclib.FILE
|
||||
elif kind == core.svn_node_dir:
|
||||
pathtype = vclib.DIR
|
||||
except:
|
||||
pass
|
||||
if pathtype is None:
|
||||
raise vclib.ItemNotFound(path_parts)
|
||||
if not vclib.check_path_access(self, path_parts, pathtype, rev):
|
||||
raise vclib.ItemNotFound(path_parts)
|
||||
return pathtype
|
||||
|
||||
def openfile(self, path_parts, rev, options):
|
||||
path = self._getpath(path_parts)
|
||||
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file." % path)
|
||||
rev = self._getrev(rev)
|
||||
url = self._geturl(path)
|
||||
tmp_file = tempfile.mktemp()
|
||||
stream = core.svn_stream_from_aprfile(tmp_file)
|
||||
### rev here should be the last history revision of the URL
|
||||
client.svn_client_cat(core.Stream(stream), url, _rev2optrev(rev), self.ctx)
|
||||
core.svn_stream_close(stream)
|
||||
lh_rev, c_rev = self._get_last_history_rev(path_parts, rev)
|
||||
return SelfCleanFP(tmp_file), lh_rev
|
||||
|
||||
def listdir(self, path_parts, rev, options):
|
||||
path = self._getpath(path_parts)
|
||||
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a directory." % path)
|
||||
rev = self._getrev(rev)
|
||||
entries = []
|
||||
dirents, locks = self._get_dirents(path, rev)
|
||||
for name in dirents.keys():
|
||||
entry = dirents[name]
|
||||
if entry.kind == core.svn_node_dir:
|
||||
kind = vclib.DIR
|
||||
elif entry.kind == core.svn_node_file:
|
||||
kind = vclib.FILE
|
||||
else:
|
||||
kind = None
|
||||
entries.append(vclib.DirEntry(name, kind))
|
||||
return entries
|
||||
|
||||
def dirlogs(self, path_parts, rev, entries, options):
|
||||
path = self._getpath(path_parts)
|
||||
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a directory." % path)
|
||||
rev = self._getrev(rev)
|
||||
dirents, locks = self._get_dirents(path, rev)
|
||||
for entry in entries:
|
||||
entry_path_parts = path_parts + [entry.name]
|
||||
dirent = dirents.get(entry.name, None)
|
||||
# dirents is authz-sanitized, so ensure the entry is found therein.
|
||||
if dirent is None:
|
||||
continue
|
||||
# Get authz-sanitized revision metadata.
|
||||
entry.date, entry.author, entry.log, revprops, changes = \
|
||||
self._revinfo(dirent.created_rev)
|
||||
entry.rev = str(dirent.created_rev)
|
||||
entry.size = dirent.size
|
||||
entry.lockinfo = None
|
||||
if locks.has_key(entry.name):
|
||||
entry.lockinfo = locks[entry.name].owner
|
||||
|
||||
def itemlog(self, path_parts, rev, sortby, first, limit, options):
|
||||
assert sortby == vclib.SORTBY_DEFAULT or sortby == vclib.SORTBY_REV
|
||||
path_type = self.itemtype(path_parts, rev) # does auth-check
|
||||
path = self._getpath(path_parts)
|
||||
rev = self._getrev(rev)
|
||||
url = self._geturl(path)
|
||||
|
||||
# Use ls3 to fetch the lock status and size (as of REV) for this item.
|
||||
lockinfo = None
|
||||
basename = path_parts and path_parts[-1] or ""
|
||||
dirents, locks = list_directory(url, _rev2optrev(rev),
|
||||
_rev2optrev(rev), 0, self.ctx)
|
||||
if locks.has_key(basename):
|
||||
lockinfo = locks[basename].owner
|
||||
size_in_rev = dirents[basename].size
|
||||
|
||||
# Special handling for the 'svn_latest_log' scenario.
|
||||
### FIXME: Don't like this hack. We should just introduce
|
||||
### something more direct in the vclib API.
|
||||
if options.get('svn_latest_log', 0):
|
||||
dir_lh_rev, dir_c_rev = self._get_last_history_rev(path_parts, rev)
|
||||
date, author, log, revprops, changes = self._revinfo(dir_lh_rev)
|
||||
return [vclib.Revision(dir_lh_rev, str(dir_lh_rev), date, author,
|
||||
None, log, size_in_rev, lockinfo)]
|
||||
|
||||
def _access_checker(check_path, check_rev):
|
||||
return vclib.check_path_access(self, _path_parts(check_path),
|
||||
path_type, check_rev)
|
||||
|
||||
# It's okay if we're told to not show all logs on a file -- all
|
||||
# the revisions should match correctly anyway.
|
||||
lc = LogCollector(path, options.get('svn_show_all_dir_logs', 0),
|
||||
lockinfo, _access_checker)
|
||||
|
||||
cross_copies = options.get('svn_cross_copies', 0)
|
||||
log_limit = 0
|
||||
if limit:
|
||||
log_limit = first + limit
|
||||
client_log(url, _rev2optrev(rev), _rev2optrev(1), log_limit, 1,
|
||||
cross_copies, lc.add_log, self.ctx)
|
||||
revs = lc.logs
|
||||
revs.sort()
|
||||
prev = None
|
||||
for rev in revs:
|
||||
# Swap out revision info with stuff from the cache (which is
|
||||
# authz-sanitized).
|
||||
rev.date, rev.author, rev.log, revprops, changes \
|
||||
= self._revinfo(rev.number)
|
||||
rev.prev = prev
|
||||
prev = rev
|
||||
revs.reverse()
|
||||
|
||||
if len(revs) < first:
|
||||
return []
|
||||
if limit:
|
||||
return revs[first:first+limit]
|
||||
return revs
|
||||
|
||||
def itemprops(self, path_parts, rev):
|
||||
path = self._getpath(path_parts)
|
||||
path_type = self.itemtype(path_parts, rev) # does auth-check
|
||||
rev = self._getrev(rev)
|
||||
url = self._geturl(path)
|
||||
pairs = client.svn_client_proplist2(url, _rev2optrev(rev),
|
||||
_rev2optrev(rev), 0, self.ctx)
|
||||
return pairs and pairs[0][1] or {}
|
||||
|
||||
def annotate(self, path_parts, rev, include_text=False):
|
||||
path = self._getpath(path_parts)
|
||||
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file." % path)
|
||||
rev = self._getrev(rev)
|
||||
url = self._geturl(path)
|
||||
|
||||
# Examine logs for the file to determine the oldest revision we are
|
||||
# permitted to see.
|
||||
log_options = {
|
||||
'svn_cross_copies' : 1,
|
||||
'svn_show_all_dir_logs' : 1,
|
||||
}
|
||||
revs = self.itemlog(path_parts, rev, vclib.SORTBY_REV, 0, 0, log_options)
|
||||
oldest_rev = revs[-1].number
|
||||
|
||||
# Now calculate the annotation data. Note that we'll not
|
||||
# inherently trust the provided author and date, because authz
|
||||
# rules might necessitate that we strip that information out.
|
||||
blame_data = []
|
||||
|
||||
def _blame_cb(line_no, revision, author, date,
|
||||
line, pool, blame_data=blame_data):
|
||||
prev_rev = None
|
||||
if revision > 1:
|
||||
prev_rev = revision - 1
|
||||
|
||||
# If we have an invalid revision, clear the date and author
|
||||
# values. Otherwise, if we have authz filtering to do, use the
|
||||
# revinfo cache to do so.
|
||||
if revision < 0:
|
||||
date = author = None
|
||||
elif self.auth:
|
||||
date, author, msg, revprops, changes = self._revinfo(revision)
|
||||
|
||||
# Strip text if the caller doesn't want it.
|
||||
if not include_text:
|
||||
line = None
|
||||
blame_data.append(vclib.Annotation(line, line_no + 1, revision, prev_rev,
|
||||
author, date))
|
||||
|
||||
client.blame2(url, _rev2optrev(rev), _rev2optrev(oldest_rev),
|
||||
_rev2optrev(rev), _blame_cb, self.ctx)
|
||||
return blame_data, rev
|
||||
|
||||
def revinfo(self, rev):
|
||||
return self._revinfo(rev, 1)
|
||||
|
||||
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
|
||||
p1 = self._getpath(path_parts1)
|
||||
p2 = self._getpath(path_parts2)
|
||||
r1 = self._getrev(rev1)
|
||||
r2 = self._getrev(rev2)
|
||||
if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1):
|
||||
raise vclib.ItemNotFound(path_parts1)
|
||||
if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2):
|
||||
raise vclib.ItemNotFound(path_parts2)
|
||||
|
||||
args = vclib._diff_args(type, options)
|
||||
|
||||
def _date_from_rev(rev):
|
||||
date, author, msg, revprops, changes = self._revinfo(rev)
|
||||
return date
|
||||
|
||||
try:
|
||||
temp1 = temp_checkout(self, p1, r1)
|
||||
temp2 = temp_checkout(self, p2, r2)
|
||||
info1 = p1, _date_from_rev(r1), r1
|
||||
info2 = p2, _date_from_rev(r2), r2
|
||||
return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args)
|
||||
except core.SubversionException, e:
|
||||
_fix_subversion_exception(e)
|
||||
if e.apr_err == vclib.svn.core.SVN_ERR_FS_NOT_FOUND:
|
||||
raise vclib.InvalidRevision
|
||||
raise
|
||||
|
||||
def isexecutable(self, path_parts, rev):
|
||||
props = self.itemprops(path_parts, rev) # does authz-check
|
||||
return props.has_key(core.SVN_PROP_EXECUTABLE)
|
||||
|
||||
def _getpath(self, path_parts):
|
||||
return string.join(path_parts, '/')
|
||||
|
||||
def _getrev(self, rev):
|
||||
if rev is None or rev == 'HEAD':
|
||||
return self.youngest
|
||||
try:
|
||||
if type(rev) == type(''):
|
||||
while rev[0] == 'r':
|
||||
rev = rev[1:]
|
||||
rev = int(rev)
|
||||
except:
|
||||
raise vclib.InvalidRevision(rev)
|
||||
if (rev < 0) or (rev > self.youngest):
|
||||
raise vclib.InvalidRevision(rev)
|
||||
return rev
|
||||
|
||||
def _geturl(self, path=None):
|
||||
if not path:
|
||||
return self.rootpath
|
||||
path = self.rootpath + '/' + urllib.quote(path)
|
||||
return _canonicalize_path(path)
|
||||
|
||||
def _get_dirents(self, path, rev):
|
||||
"""Return a 2-type of dirents and locks, possibly reading/writing
|
||||
from a local cache of that information. This functions performs
|
||||
authz checks, stripping out unreadable dirents."""
|
||||
|
||||
dir_url = self._geturl(path)
|
||||
path_parts = _path_parts(path)
|
||||
if path:
|
||||
key = str(rev) + '/' + path
|
||||
else:
|
||||
key = str(rev)
|
||||
|
||||
# Ensure that the cache gets filled...
|
||||
dirents_locks = self._dirent_cache.get(key)
|
||||
if not dirents_locks:
|
||||
tmp_dirents, locks = list_directory(dir_url, _rev2optrev(rev),
|
||||
_rev2optrev(rev), 0, self.ctx)
|
||||
dirents = {}
|
||||
for name, dirent in tmp_dirents.items():
|
||||
dirent_parts = path_parts + [name]
|
||||
kind = dirent.kind
|
||||
if (kind == core.svn_node_dir or kind == core.svn_node_file) \
|
||||
and vclib.check_path_access(self, dirent_parts,
|
||||
kind == core.svn_node_dir \
|
||||
and vclib.DIR or vclib.FILE, rev):
|
||||
lh_rev, c_rev = self._get_last_history_rev(dirent_parts, rev)
|
||||
dirent.created_rev = lh_rev
|
||||
dirents[name] = dirent
|
||||
dirents_locks = [dirents, locks]
|
||||
self._dirent_cache[key] = dirents_locks
|
||||
|
||||
# ...then return the goodies from the cache.
|
||||
return dirents_locks[0], dirents_locks[1]
|
||||
|
||||
def _get_last_history_rev(self, path_parts, rev):
|
||||
"""Return the a 2-tuple which contains:
|
||||
- the last interesting revision equal to or older than REV in
|
||||
the history of PATH_PARTS.
|
||||
- the created_rev of of PATH_PARTS as of REV."""
|
||||
|
||||
path = self._getpath(path_parts)
|
||||
url = self._geturl(self._getpath(path_parts))
|
||||
optrev = _rev2optrev(rev)
|
||||
|
||||
# Get the last-changed-rev.
|
||||
revisions = []
|
||||
def _info_cb(path, info, pool, retval=revisions):
|
||||
revisions.append(info.last_changed_rev)
|
||||
client.svn_client_info(url, optrev, optrev, _info_cb, 0, self.ctx)
|
||||
last_changed_rev = revisions[0]
|
||||
|
||||
# Now, this object might not have been directly edited since the
|
||||
# last-changed-rev, but it might have been the child of a copy.
|
||||
# To determine this, we'll run a potentially no-op log between
|
||||
# LAST_CHANGED_REV and REV.
|
||||
lc = LogCollector(path, 1, None, None)
|
||||
client_log(url, optrev, _rev2optrev(last_changed_rev), 1, 1, 0,
|
||||
lc.add_log, self.ctx)
|
||||
revs = lc.logs
|
||||
if revs:
|
||||
revs.sort()
|
||||
return revs[0].number, last_changed_rev
|
||||
else:
|
||||
return last_changed_rev, last_changed_rev
|
||||
|
||||
def _revinfo_fetch(self, rev, include_changed_paths=0):
|
||||
need_changes = include_changed_paths or self.auth
|
||||
revs = []
|
||||
|
||||
def _log_cb(log_entry, pool, retval=revs):
|
||||
# If Subversion happens to call us more than once, we choose not
|
||||
# to care.
|
||||
if retval:
|
||||
return
|
||||
|
||||
revision = log_entry.revision
|
||||
msg, author, date, revprops = _split_revprops(log_entry.revprops)
|
||||
action_map = { 'D' : vclib.DELETED,
|
||||
'A' : vclib.ADDED,
|
||||
'R' : vclib.REPLACED,
|
||||
'M' : vclib.MODIFIED,
|
||||
}
|
||||
|
||||
# Easy out: if we won't use the changed-path info, just return a
|
||||
# changes-less tuple.
|
||||
if not need_changes:
|
||||
return revs.append([date, author, msg, revprops, None])
|
||||
|
||||
# Subversion 1.5 and earlier didn't offer the 'changed_paths2'
|
||||
# hash, and in Subversion 1.6, it's offered but broken.
|
||||
try:
|
||||
changed_paths = log_entry.changed_paths2
|
||||
paths = (changed_paths or {}).keys()
|
||||
except:
|
||||
changed_paths = log_entry.changed_paths
|
||||
paths = (changed_paths or {}).keys()
|
||||
paths.sort(lambda a, b: _compare_paths(a, b))
|
||||
|
||||
# If we get this far, our caller needs changed-paths, or we need
|
||||
# them for authz-related sanitization.
|
||||
changes = []
|
||||
found_readable = found_unreadable = 0
|
||||
for path in paths:
|
||||
change = changed_paths[path]
|
||||
|
||||
# svn_log_changed_path_t (which we might get instead of the
|
||||
# svn_log_changed_path2_t we'd prefer) doesn't have the
|
||||
# 'node_kind' member.
|
||||
pathtype = None
|
||||
if hasattr(change, 'node_kind'):
|
||||
if change.node_kind == core.svn_node_dir:
|
||||
pathtype = vclib.DIR
|
||||
elif change.node_kind == core.svn_node_file:
|
||||
pathtype = vclib.FILE
|
||||
|
||||
# svn_log_changed_path2_t only has the 'text_modified' and
|
||||
# 'props_modified' bits in Subversion 1.7 and beyond. And
|
||||
# svn_log_changed_path_t is without.
|
||||
text_modified = props_modified = 0
|
||||
if hasattr(change, 'text_modified'):
|
||||
if change.text_modified == core.svn_tristate_true:
|
||||
text_modified = 1
|
||||
if hasattr(change, 'props_modified'):
|
||||
if change.props_modified == core.svn_tristate_true:
|
||||
props_modified = 1
|
||||
|
||||
# Wrong, diddily wrong wrong wrong. Can you say,
|
||||
# "Manufacturing data left and right because it hurts to
|
||||
# figure out the right stuff?"
|
||||
action = action_map.get(change.action, vclib.MODIFIED)
|
||||
if change.copyfrom_path and change.copyfrom_rev:
|
||||
is_copy = 1
|
||||
base_path = change.copyfrom_path
|
||||
base_rev = change.copyfrom_rev
|
||||
elif action == vclib.ADDED or action == vclib.REPLACED:
|
||||
is_copy = 0
|
||||
base_path = base_rev = None
|
||||
else:
|
||||
is_copy = 0
|
||||
base_path = path
|
||||
base_rev = revision - 1
|
||||
|
||||
# Check authz rules (sadly, we have to lie about the path type)
|
||||
parts = _path_parts(path)
|
||||
if vclib.check_path_access(self, parts, vclib.FILE, revision):
|
||||
if is_copy and base_path and (base_path != path):
|
||||
parts = _path_parts(base_path)
|
||||
if not vclib.check_path_access(self, parts, vclib.FILE, base_rev):
|
||||
is_copy = 0
|
||||
base_path = None
|
||||
base_rev = None
|
||||
found_unreadable = 1
|
||||
changes.append(SVNChangedPath(path, revision, pathtype, base_path,
|
||||
base_rev, action, is_copy,
|
||||
text_modified, props_modified))
|
||||
found_readable = 1
|
||||
else:
|
||||
found_unreadable = 1
|
||||
|
||||
# If our caller doesn't want changed-path stuff, and we have
|
||||
# the info we need to make an authz determination already,
|
||||
# quit this loop and get on with it.
|
||||
if (not include_changed_paths) and found_unreadable and found_readable:
|
||||
break
|
||||
|
||||
# Filter unreadable information.
|
||||
if found_unreadable:
|
||||
msg = None
|
||||
if not found_readable:
|
||||
author = None
|
||||
date = None
|
||||
|
||||
# Drop unrequested changes.
|
||||
if not include_changed_paths:
|
||||
changes = None
|
||||
|
||||
# Add this revision information to the "return" array.
|
||||
retval.append([date, author, msg, revprops, changes])
|
||||
|
||||
optrev = _rev2optrev(rev)
|
||||
client_log(self.rootpath, optrev, optrev, 1, need_changes, 0,
|
||||
_log_cb, self.ctx)
|
||||
return tuple(revs[0])
|
||||
|
||||
def _revinfo(self, rev, include_changed_paths=0):
|
||||
"""Internal-use, cache-friendly revision information harvester."""
|
||||
|
||||
# Consult the revinfo cache first. If we don't have cached info,
|
||||
# or our caller wants changed paths and we don't have those for
|
||||
# this revision, go do the real work.
|
||||
rev = self._getrev(rev)
|
||||
cached_info = self._revinfo_cache.get(rev)
|
||||
if not cached_info \
|
||||
or (include_changed_paths and cached_info[4] is None):
|
||||
cached_info = self._revinfo_fetch(rev, include_changed_paths)
|
||||
self._revinfo_cache[rev] = cached_info
|
||||
return cached_info
|
||||
|
||||
##--- custom --##
|
||||
|
||||
def get_youngest_revision(self):
|
||||
return self.youngest
|
||||
|
||||
def get_location(self, path, rev, old_rev):
|
||||
try:
|
||||
results = ra.get_locations(self.ra_session, path, rev, [old_rev])
|
||||
except core.SubversionException, e:
|
||||
_fix_subversion_exception(e)
|
||||
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
|
||||
raise vclib.ItemNotFound(path)
|
||||
raise
|
||||
try:
|
||||
old_path = results[old_rev]
|
||||
except KeyError:
|
||||
raise vclib.ItemNotFound(path)
|
||||
old_path = _cleanup_path(old_path)
|
||||
old_path_parts = _path_parts(old_path)
|
||||
# Check access (lying about path types)
|
||||
if not vclib.check_path_access(self, old_path_parts, vclib.FILE, old_rev):
|
||||
raise vclib.ItemNotFound(path)
|
||||
return old_path
|
||||
|
||||
def created_rev(self, path, rev):
|
||||
lh_rev, c_rev = self._get_last_history_rev(_path_parts(path), rev)
|
||||
return lh_rev
|
||||
|
||||
def last_rev(self, path, peg_revision, limit_revision=None):
|
||||
"""Given PATH, known to exist in PEG_REVISION, find the youngest
|
||||
revision older than, or equal to, LIMIT_REVISION in which path
|
||||
exists. Return that revision, and the path at which PATH exists in
|
||||
that revision."""
|
||||
|
||||
# Here's the plan, man. In the trivial case (where PEG_REVISION is
|
||||
# the same as LIMIT_REVISION), this is a no-brainer. If
|
||||
# LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's
|
||||
# history tracing code to find the right location. If, however,
|
||||
# LIMIT_REVISION is younger than PEG_REVISION, we suffer from
|
||||
# Subversion's lack of forward history searching. Our workaround,
|
||||
# ugly as it may be, involves a binary search through the revisions
|
||||
# between PEG_REVISION and LIMIT_REVISION to find our last live
|
||||
# revision.
|
||||
peg_revision = self._getrev(peg_revision)
|
||||
limit_revision = self._getrev(limit_revision)
|
||||
if peg_revision == limit_revision:
|
||||
return peg_revision, path
|
||||
elif peg_revision > limit_revision:
|
||||
path = self.get_location(path, peg_revision, limit_revision)
|
||||
return limit_revision, path
|
||||
else:
|
||||
direction = 1
|
||||
while peg_revision != limit_revision:
|
||||
mid = (peg_revision + 1 + limit_revision) / 2
|
||||
try:
|
||||
path = self.get_location(path, peg_revision, mid)
|
||||
except vclib.ItemNotFound:
|
||||
limit_revision = mid - 1
|
||||
else:
|
||||
peg_revision = mid
|
||||
return peg_revision, path
|
||||
936
lib/vclib/svn/svn_repos.py
Normal file
936
lib/vclib/svn/svn_repos.py
Normal file
@@ -0,0 +1,936 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
"Version Control lib driver for locally accessible Subversion repositories"
|
||||
|
||||
import vclib
|
||||
import os
|
||||
import os.path
|
||||
import string
|
||||
import cStringIO
|
||||
import signal
|
||||
import time
|
||||
import tempfile
|
||||
import popen
|
||||
import re
|
||||
import urllib
|
||||
from svn import fs, repos, core, client, delta
|
||||
|
||||
|
||||
### Require Subversion 1.3.1 or better.
|
||||
if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 3, 1):
|
||||
raise Exception, "Version requirement not met (needs 1.3.1 or better)"
|
||||
|
||||
|
||||
### Pre-1.5 Subversion doesn't have SVN_ERR_CEASE_INVOCATION
|
||||
try:
|
||||
_SVN_ERR_CEASE_INVOCATION = core.SVN_ERR_CEASE_INVOCATION
|
||||
except:
|
||||
_SVN_ERR_CEASE_INVOCATION = core.SVN_ERR_CANCELLED
|
||||
|
||||
### Pre-1.5 SubversionException's might not have the .msg and .apr_err members
|
||||
def _fix_subversion_exception(e):
|
||||
if not hasattr(e, 'apr_err'):
|
||||
e.apr_err = e[1]
|
||||
if not hasattr(e, 'message'):
|
||||
e.message = e[0]
|
||||
|
||||
### Pre-1.4 Subversion doesn't have svn_path_canonicalize()
|
||||
def _canonicalize_path(path):
|
||||
try:
|
||||
return core.svn_path_canonicalize(path)
|
||||
except AttributeError:
|
||||
return path
|
||||
|
||||
def _allow_all(root, path, pool):
|
||||
"""Generic authz_read_func that permits access to all paths"""
|
||||
return 1
|
||||
|
||||
|
||||
def _path_parts(path):
|
||||
return filter(None, string.split(path, '/'))
|
||||
|
||||
|
||||
def _cleanup_path(path):
|
||||
"""Return a cleaned-up Subversion filesystem path"""
|
||||
return string.join(_path_parts(path), '/')
|
||||
|
||||
|
||||
def _fs_path_join(base, relative):
|
||||
return _cleanup_path(base + '/' + relative)
|
||||
|
||||
|
||||
def _compare_paths(path1, path2):
|
||||
path1_len = len (path1);
|
||||
path2_len = len (path2);
|
||||
min_len = min(path1_len, path2_len)
|
||||
i = 0
|
||||
|
||||
# Are the paths exactly the same?
|
||||
if path1 == path2:
|
||||
return 0
|
||||
|
||||
# Skip past common prefix
|
||||
while (i < min_len) and (path1[i] == path2[i]):
|
||||
i = i + 1
|
||||
|
||||
# Children of paths are greater than their parents, but less than
|
||||
# greater siblings of their parents
|
||||
char1 = '\0'
|
||||
char2 = '\0'
|
||||
if (i < path1_len):
|
||||
char1 = path1[i]
|
||||
if (i < path2_len):
|
||||
char2 = path2[i]
|
||||
|
||||
if (char1 == '/') and (i == path2_len):
|
||||
return 1
|
||||
if (char2 == '/') and (i == path1_len):
|
||||
return -1
|
||||
if (i < path1_len) and (char1 == '/'):
|
||||
return -1
|
||||
if (i < path2_len) and (char2 == '/'):
|
||||
return 1
|
||||
|
||||
# Common prefix was skipped above, next character is compared to
|
||||
# determine order
|
||||
return cmp(char1, char2)
|
||||
|
||||
|
||||
def _rev2optrev(rev):
|
||||
assert type(rev) is int
|
||||
rt = core.svn_opt_revision_t()
|
||||
rt.kind = core.svn_opt_revision_number
|
||||
rt.value.number = rev
|
||||
return rt
|
||||
|
||||
|
||||
def _rootpath2url(rootpath, path):
|
||||
rootpath = os.path.abspath(rootpath)
|
||||
drive, rootpath = os.path.splitdrive(rootpath)
|
||||
if os.sep != '/':
|
||||
rootpath = string.replace(rootpath, os.sep, '/')
|
||||
rootpath = urllib.quote(rootpath)
|
||||
path = urllib.quote(path)
|
||||
if drive:
|
||||
url = 'file:///' + drive + rootpath + '/' + path
|
||||
else:
|
||||
url = 'file://' + rootpath + '/' + path
|
||||
return _canonicalize_path(url)
|
||||
|
||||
|
||||
# Given a dictionary REVPROPS of revision properties, pull special
|
||||
# ones out of them and return a 4-tuple containing the log message,
|
||||
# the author, the date (converted from the date string property), and
|
||||
# a dictionary of any/all other revprops.
|
||||
def _split_revprops(revprops):
|
||||
if not revprops:
|
||||
return None, None, None, {}
|
||||
special_props = []
|
||||
for prop in core.SVN_PROP_REVISION_LOG, \
|
||||
core.SVN_PROP_REVISION_AUTHOR, \
|
||||
core.SVN_PROP_REVISION_DATE:
|
||||
if revprops.has_key(prop):
|
||||
special_props.append(revprops[prop])
|
||||
del(revprops[prop])
|
||||
else:
|
||||
special_props.append(None)
|
||||
msg, author, datestr = tuple(special_props)
|
||||
date = _datestr_to_date(datestr)
|
||||
return msg, author, date, revprops
|
||||
|
||||
|
||||
def _datestr_to_date(datestr):
|
||||
try:
|
||||
return core.svn_time_from_cstring(datestr) / 1000000
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class Revision(vclib.Revision):
|
||||
"Hold state for each revision's log entry."
|
||||
def __init__(self, rev, date, author, msg, size, lockinfo,
|
||||
filename, copy_path, copy_rev):
|
||||
vclib.Revision.__init__(self, rev, str(rev), date, author, None,
|
||||
msg, size, lockinfo)
|
||||
self.filename = filename
|
||||
self.copy_path = copy_path
|
||||
self.copy_rev = copy_rev
|
||||
|
||||
|
||||
class NodeHistory:
|
||||
"""An iterable object that returns 2-tuples of (revision, path)
|
||||
locations along a node's change history, ordered from youngest to
|
||||
oldest."""
|
||||
|
||||
def __init__(self, fs_ptr, show_all_logs, limit=0):
|
||||
self.histories = []
|
||||
self.fs_ptr = fs_ptr
|
||||
self.show_all_logs = show_all_logs
|
||||
self.oldest_rev = None
|
||||
self.limit = limit
|
||||
|
||||
def add_history(self, path, revision, pool):
|
||||
# If filtering, only add the path and revision to the histories
|
||||
# list if they were actually changed in this revision (where
|
||||
# change means the path itself was changed, or one of its parents
|
||||
# was copied). This is useful for omitting bubble-up directory
|
||||
# changes.
|
||||
if not self.oldest_rev:
|
||||
self.oldest_rev = revision
|
||||
else:
|
||||
assert(revision < self.oldest_rev)
|
||||
|
||||
if not self.show_all_logs:
|
||||
rev_root = fs.revision_root(self.fs_ptr, revision)
|
||||
changed_paths = fs.paths_changed(rev_root)
|
||||
paths = changed_paths.keys()
|
||||
if path not in paths:
|
||||
# Look for a copied parent
|
||||
test_path = path
|
||||
found = 0
|
||||
while 1:
|
||||
off = string.rfind(test_path, '/')
|
||||
if off < 0:
|
||||
break
|
||||
test_path = test_path[0:off]
|
||||
if test_path in paths:
|
||||
copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, test_path)
|
||||
if copyfrom_rev >= 0 and copyfrom_path:
|
||||
found = 1
|
||||
break
|
||||
if not found:
|
||||
return
|
||||
self.histories.append([revision, _cleanup_path(path)])
|
||||
if self.limit and len(self.histories) == self.limit:
|
||||
raise core.SubversionException("", _SVN_ERR_CEASE_INVOCATION)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.histories[idx]
|
||||
|
||||
def _get_last_history_rev(fsroot, path):
|
||||
history = fs.node_history(fsroot, path)
|
||||
history = fs.history_prev(history, 0)
|
||||
history_path, history_rev = fs.history_location(history)
|
||||
return history_rev
|
||||
|
||||
def temp_checkout(svnrepos, path, rev):
|
||||
"""Check out file revision to temporary file"""
|
||||
temp = tempfile.mktemp()
|
||||
fp = open(temp, 'wb')
|
||||
try:
|
||||
root = svnrepos._getroot(rev)
|
||||
stream = fs.file_contents(root, path)
|
||||
try:
|
||||
while 1:
|
||||
chunk = core.svn_stream_read(stream, core.SVN_STREAM_CHUNK_SIZE)
|
||||
if not chunk:
|
||||
break
|
||||
fp.write(chunk)
|
||||
finally:
|
||||
core.svn_stream_close(stream)
|
||||
finally:
|
||||
fp.close()
|
||||
return temp
|
||||
|
||||
class FileContentsPipe:
|
||||
def __init__(self, root, path):
|
||||
self._stream = fs.file_contents(root, path)
|
||||
self._eof = 0
|
||||
|
||||
def read(self, len=None):
|
||||
chunk = None
|
||||
if not self._eof:
|
||||
if len is None:
|
||||
buffer = cStringIO.StringIO()
|
||||
try:
|
||||
while 1:
|
||||
hunk = core.svn_stream_read(self._stream, 8192)
|
||||
if not hunk:
|
||||
break
|
||||
buffer.write(hunk)
|
||||
chunk = buffer.getvalue()
|
||||
finally:
|
||||
buffer.close()
|
||||
|
||||
else:
|
||||
chunk = core.svn_stream_read(self._stream, len)
|
||||
if not chunk:
|
||||
self._eof = 1
|
||||
return chunk
|
||||
|
||||
def readline(self):
|
||||
chunk = None
|
||||
if not self._eof:
|
||||
chunk, self._eof = core.svn_stream_readline(self._stream, '\n')
|
||||
if not self._eof:
|
||||
chunk = chunk + '\n'
|
||||
if not chunk:
|
||||
self._eof = 1
|
||||
return chunk
|
||||
|
||||
def readlines(self):
|
||||
lines = []
|
||||
while True:
|
||||
line = self.readline()
|
||||
if not line:
|
||||
break
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
def close(self):
|
||||
return core.svn_stream_close(self._stream)
|
||||
|
||||
def eof(self):
|
||||
return self._eof
|
||||
|
||||
|
||||
class BlameSource:
|
||||
def __init__(self, local_url, rev, first_rev, include_text, config_dir):
|
||||
self.idx = -1
|
||||
self.first_rev = first_rev
|
||||
self.blame_data = []
|
||||
self.include_text = include_text
|
||||
|
||||
ctx = client.svn_client_create_context()
|
||||
core.svn_config_ensure(config_dir)
|
||||
ctx.config = core.svn_config_get_config(config_dir)
|
||||
ctx.auth_baton = core.svn_auth_open([])
|
||||
try:
|
||||
### TODO: Is this use of FIRST_REV always what we want? Should we
|
||||
### pass 1 here instead and do filtering later?
|
||||
client.blame2(local_url, _rev2optrev(rev), _rev2optrev(first_rev),
|
||||
_rev2optrev(rev), self._blame_cb, ctx)
|
||||
except core.SubversionException, e:
|
||||
_fix_subversion_exception(e)
|
||||
if e.apr_err == core.SVN_ERR_CLIENT_IS_BINARY_FILE:
|
||||
raise vclib.NonTextualFileContents
|
||||
raise
|
||||
|
||||
def _blame_cb(self, line_no, rev, author, date, text, pool):
|
||||
prev_rev = None
|
||||
if rev > self.first_rev:
|
||||
prev_rev = rev - 1
|
||||
if not self.include_text:
|
||||
text = None
|
||||
self.blame_data.append(vclib.Annotation(text, line_no + 1, rev,
|
||||
prev_rev, author, None))
|
||||
|
||||
def __getitem__(self, idx):
|
||||
if idx != self.idx + 1:
|
||||
raise BlameSequencingError()
|
||||
self.idx = idx
|
||||
return self.blame_data[idx]
|
||||
|
||||
|
||||
class BlameSequencingError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SVNChangedPath(vclib.ChangedPath):
|
||||
"""Wrapper around vclib.ChangedPath which handles path splitting."""
|
||||
|
||||
def __init__(self, path, rev, pathtype, base_path, base_rev,
|
||||
action, copied, text_changed, props_changed):
|
||||
path_parts = _path_parts(path or '')
|
||||
base_path_parts = _path_parts(base_path or '')
|
||||
vclib.ChangedPath.__init__(self, path_parts, rev, pathtype,
|
||||
base_path_parts, base_rev, action,
|
||||
copied, text_changed, props_changed)
|
||||
|
||||
|
||||
class LocalSubversionRepository(vclib.Repository):
|
||||
def __init__(self, name, rootpath, authorizer, utilities, config_dir):
|
||||
if not (os.path.isdir(rootpath) \
|
||||
and os.path.isfile(os.path.join(rootpath, 'format'))):
|
||||
raise vclib.ReposNotFound(name)
|
||||
|
||||
# Initialize some stuff.
|
||||
self.rootpath = rootpath
|
||||
self.name = name
|
||||
self.auth = authorizer
|
||||
self.svn_client_path = utilities.svn or 'svn'
|
||||
self.diff_cmd = utilities.diff or 'diff'
|
||||
self.config_dir = config_dir or None
|
||||
|
||||
# See if this repository is even viewable, authz-wise.
|
||||
if not vclib.check_root_access(self):
|
||||
raise vclib.ReposNotFound(name)
|
||||
|
||||
def open(self):
|
||||
# Register a handler for SIGTERM so we can have a chance to
|
||||
# cleanup. If ViewVC takes too long to start generating CGI
|
||||
# output, Apache will grow impatient and SIGTERM it. While we
|
||||
# don't mind getting told to bail, we want to gracefully close the
|
||||
# repository before we bail.
|
||||
def _sigterm_handler(signum, frame, self=self):
|
||||
sys.exit(-1)
|
||||
try:
|
||||
signal.signal(signal.SIGTERM, _sigterm_handler)
|
||||
except ValueError:
|
||||
# This is probably "ValueError: signal only works in main
|
||||
# thread", which will get thrown by the likes of mod_python
|
||||
# when trying to install a signal handler from a thread that
|
||||
# isn't the main one. We'll just not care.
|
||||
pass
|
||||
|
||||
# Open the repository and init some other variables.
|
||||
self.repos = repos.svn_repos_open(self.rootpath)
|
||||
self.fs_ptr = repos.svn_repos_fs(self.repos)
|
||||
self.youngest = fs.youngest_rev(self.fs_ptr)
|
||||
self._fsroots = {}
|
||||
self._revinfo_cache = {}
|
||||
|
||||
# See if a universal read access determination can be made.
|
||||
if self.auth and self.auth.check_universal_access(self.name) == 1:
|
||||
self.auth = None
|
||||
|
||||
def rootname(self):
|
||||
return self.name
|
||||
|
||||
def rootpath(self):
|
||||
return self.rootpath
|
||||
|
||||
def roottype(self):
|
||||
return vclib.SVN
|
||||
|
||||
def authorizer(self):
|
||||
return self.auth
|
||||
|
||||
def itemtype(self, path_parts, rev):
|
||||
rev = self._getrev(rev)
|
||||
basepath = self._getpath(path_parts)
|
||||
pathtype = self._gettype(basepath, rev)
|
||||
if pathtype is None:
|
||||
raise vclib.ItemNotFound(path_parts)
|
||||
if not vclib.check_path_access(self, path_parts, pathtype, rev):
|
||||
raise vclib.ItemNotFound(path_parts)
|
||||
return pathtype
|
||||
|
||||
def openfile(self, path_parts, rev, options):
|
||||
path = self._getpath(path_parts)
|
||||
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a file." % path)
|
||||
rev = self._getrev(rev)
|
||||
fsroot = self._getroot(rev)
|
||||
revision = str(_get_last_history_rev(fsroot, path))
|
||||
fp = FileContentsPipe(fsroot, path)
|
||||
return fp, revision
|
||||
|
||||
def listdir(self, path_parts, rev, options):
|
||||
path = self._getpath(path_parts)
|
||||
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a directory." % path)
|
||||
rev = self._getrev(rev)
|
||||
fsroot = self._getroot(rev)
|
||||
dirents = fs.dir_entries(fsroot, path)
|
||||
entries = [ ]
|
||||
for entry in dirents.values():
|
||||
if entry.kind == core.svn_node_dir:
|
||||
kind = vclib.DIR
|
||||
elif entry.kind == core.svn_node_file:
|
||||
kind = vclib.FILE
|
||||
if vclib.check_path_access(self, path_parts + [entry.name], kind, rev):
|
||||
entries.append(vclib.DirEntry(entry.name, kind))
|
||||
return entries
|
||||
|
||||
def dirlogs(self, path_parts, rev, entries, options):
|
||||
path = self._getpath(path_parts)
|
||||
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
|
||||
raise vclib.Error("Path '%s' is not a directory." % path)
|
||||
fsroot = self._getroot(self._getrev(rev))
|
||||
rev = self._getrev(rev)
|
||||
for entry in entries:
|
||||
entry_path_parts = path_parts + [entry.name]
|
||||
if not vclib.check_path_access(self, entry_path_parts, entry.kind, rev):
|
||||
continue
|
||||
path = self._getpath(entry_path_parts)
|
||||
entry_rev = _get_last_history_rev(fsroot, path)
|
||||
date, author, msg, revprops, changes = self._revinfo(entry_rev)
|
||||
entry.rev = str(entry_rev)
|
||||
entry.date = date
|
||||
entry.author = author
|
||||
entry.log = msg
|
||||
if entry.kind == vclib.FILE:
|
||||
entry.size = fs.file_length(fsroot, path)
|
||||
lock = fs.get_lock(self.fs_ptr, path)
|
||||
entry.lockinfo = lock and lock.owner or None
|
||||
|
||||
def itemlog(self, path_parts, rev, sortby, first, limit, options):
|
||||
"""see vclib.Repository.itemlog docstring
|
||||
|
||||
Option values recognized by this implementation
|
||||
|
||||
svn_show_all_dir_logs
|
||||
boolean, default false. if set for a directory path, will include
|
||||
revisions where files underneath the directory have changed
|
||||
|
||||
svn_cross_copies
|
||||
boolean, default false. if set for a path created by a copy, will
|
||||
include revisions from before the copy
|
||||
|
||||
svn_latest_log
|
||||
boolean, default false. if set will return only newest single log
|
||||
entry
|
||||
"""
|
||||
assert sortby == vclib.SORTBY_DEFAULT or sortby == vclib.SORTBY_REV
|
||||
|
||||
path = self._getpath(path_parts)
|
||||
path_type = self.itemtype(path_parts, rev) # does auth-check
|
||||
rev = self._getrev(rev)
|
||||
revs = []
|
||||
lockinfo = None
|
||||
|
||||
# See if this path is locked.
|
||||
try:
|
||||
lock = fs.get_lock(self.fs_ptr, path)
|
||||
if lock:
|
||||
lockinfo = lock.owner
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
# If our caller only wants the latest log, we'll invoke
|
||||
# _log_helper for just the one revision. Otherwise, we go off
|
||||
# into history-fetching mode. ### TODO: we could stand to have a
|
||||
# 'limit' parameter here as numeric cut-off for the depth of our
|
||||
# history search.
|
||||
if options.get('svn_latest_log', 0):
|
||||
revision = self._log_helper(path, rev, lockinfo)
|
||||
if revision:
|
||||
revision.prev = None
|
||||
revs.append(revision)
|
||||
else:
|
||||
history = self._get_history(path, rev, path_type, first + limit, options)
|
||||
if len(history) < first:
|
||||
history = []
|
||||
if limit:
|
||||
history = history[first:first+limit]
|
||||
|
||||
for hist_rev, hist_path in history:
|
||||
revision = self._log_helper(hist_path, hist_rev, lockinfo)
|
||||
if revision:
|
||||
# If we have unreadable copyfrom data, obscure it.
|
||||
if revision.copy_path is not None:
|
||||
cp_parts = _path_parts(revision.copy_path)
|
||||
if not vclib.check_path_access(self, cp_parts, path_type,
|
||||
revision.copy_rev):
|
||||
revision.copy_path = revision.copy_rev = None
|
||||
revision.prev = None
|
||||
if len(revs):
|
||||
revs[-1].prev = revision
|
||||
revs.append(revision)
|
||||
return revs
|
||||
|
||||
def itemprops(self, path_parts, rev):
|
||||
path = self._getpath(path_parts)
|
||||
path_type = self.itemtype(path_parts, rev) # does auth-check
|
||||
rev = self._getrev(rev)
|
||||
fsroot = self._getroot(rev)
|
||||
return fs.node_proplist(fsroot, path)
|
||||
|
||||
def annotate(self, path_parts, rev, include_text=False):
|
||||
path = self._getpath(path_parts)
|
||||
path_type = self.itemtype(path_parts, rev) # does auth-check
|
||||
if path_type != vclib.FILE:
|
||||
raise vclib.Error("Path '%s' is not a file." % path)
|
||||
rev = self._getrev(rev)
|
||||
fsroot = self._getroot(rev)
|
||||
history = self._get_history(path, rev, path_type, 0,
|
||||
{'svn_cross_copies': 1})
|
||||
youngest_rev, youngest_path = history[0]
|
||||
oldest_rev, oldest_path = history[-1]
|
||||
source = BlameSource(_rootpath2url(self.rootpath, path), youngest_rev,
|
||||
oldest_rev, include_text, self.config_dir)
|
||||
return source, youngest_rev
|
||||
|
||||
def revinfo(self, rev):
|
||||
return self._revinfo(rev, 1)
|
||||
|
||||
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
|
||||
p1 = self._getpath(path_parts1)
|
||||
p2 = self._getpath(path_parts2)
|
||||
r1 = self._getrev(rev1)
|
||||
r2 = self._getrev(rev2)
|
||||
if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1):
|
||||
raise vclib.ItemNotFound(path_parts1)
|
||||
if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2):
|
||||
raise vclib.ItemNotFound(path_parts2)
|
||||
|
||||
args = vclib._diff_args(type, options)
|
||||
|
||||
def _date_from_rev(rev):
|
||||
date, author, msg, revprops, changes = self._revinfo(rev)
|
||||
return date
|
||||
|
||||
try:
|
||||
temp1 = temp_checkout(self, p1, r1)
|
||||
temp2 = temp_checkout(self, p2, r2)
|
||||
info1 = p1, _date_from_rev(r1), r1
|
||||
info2 = p2, _date_from_rev(r2), r2
|
||||
return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args)
|
||||
except core.SubversionException, e:
|
||||
_fix_subversion_exception(e)
|
||||
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
|
||||
raise vclib.InvalidRevision
|
||||
raise
|
||||
|
||||
def isexecutable(self, path_parts, rev):
|
||||
props = self.itemprops(path_parts, rev) # does authz-check
|
||||
return props.has_key(core.SVN_PROP_EXECUTABLE)
|
||||
|
||||
##--- helpers ---##
|
||||
|
||||
def _revinfo(self, rev, include_changed_paths=0):
|
||||
"""Internal-use, cache-friendly revision information harvester."""
|
||||
|
||||
def _get_changed_paths(fsroot):
|
||||
"""Return a 3-tuple: found_readable, found_unreadable, changed_paths."""
|
||||
editor = repos.ChangeCollector(self.fs_ptr, fsroot)
|
||||
e_ptr, e_baton = delta.make_editor(editor)
|
||||
repos.svn_repos_replay(fsroot, e_ptr, e_baton)
|
||||
changedpaths = {}
|
||||
changes = editor.get_changes()
|
||||
|
||||
# Copy the Subversion changes into a new hash, checking
|
||||
# authorization and converting them into ChangedPath objects.
|
||||
found_readable = found_unreadable = 0
|
||||
for path in changes.keys():
|
||||
change = changes[path]
|
||||
if change.path:
|
||||
change.path = _cleanup_path(change.path)
|
||||
if change.base_path:
|
||||
change.base_path = _cleanup_path(change.base_path)
|
||||
is_copy = 0
|
||||
if not hasattr(change, 'action'): # new to subversion 1.4.0
|
||||
action = vclib.MODIFIED
|
||||
if not change.path:
|
||||
action = vclib.DELETED
|
||||
elif change.added:
|
||||
action = vclib.ADDED
|
||||
replace_check_path = path
|
||||
if change.base_path and change.base_rev:
|
||||
replace_check_path = change.base_path
|
||||
if changedpaths.has_key(replace_check_path) \
|
||||
and changedpaths[replace_check_path].action == vclib.DELETED:
|
||||
action = vclib.REPLACED
|
||||
else:
|
||||
if change.action == repos.CHANGE_ACTION_ADD:
|
||||
action = vclib.ADDED
|
||||
elif change.action == repos.CHANGE_ACTION_DELETE:
|
||||
action = vclib.DELETED
|
||||
elif change.action == repos.CHANGE_ACTION_REPLACE:
|
||||
action = vclib.REPLACED
|
||||
else:
|
||||
action = vclib.MODIFIED
|
||||
if (action == vclib.ADDED or action == vclib.REPLACED) \
|
||||
and change.base_path \
|
||||
and change.base_rev:
|
||||
is_copy = 1
|
||||
if change.item_kind == core.svn_node_dir:
|
||||
pathtype = vclib.DIR
|
||||
elif change.item_kind == core.svn_node_file:
|
||||
pathtype = vclib.FILE
|
||||
else:
|
||||
pathtype = None
|
||||
|
||||
parts = _path_parts(path)
|
||||
if vclib.check_path_access(self, parts, pathtype, rev):
|
||||
if is_copy and change.base_path and (change.base_path != path):
|
||||
parts = _path_parts(change.base_path)
|
||||
if not vclib.check_path_access(self, parts, pathtype,
|
||||
change.base_rev):
|
||||
is_copy = 0
|
||||
change.base_path = None
|
||||
change.base_rev = None
|
||||
found_unreadable = 1
|
||||
changedpaths[path] = SVNChangedPath(path, rev, pathtype,
|
||||
change.base_path,
|
||||
change.base_rev, action,
|
||||
is_copy, change.text_changed,
|
||||
change.prop_changes)
|
||||
found_readable = 1
|
||||
else:
|
||||
found_unreadable = 1
|
||||
return found_readable, found_unreadable, changedpaths.values()
|
||||
|
||||
def _get_change_copyinfo(fsroot, path, change):
|
||||
if hasattr(change, 'copyfrom_known') and change.copyfrom_known:
|
||||
copyfrom_path = change.copyfrom_path
|
||||
copyfrom_rev = change.copyfrom_rev
|
||||
else:
|
||||
copyfrom_rev, copyfrom_path = fs.copied_from(fsroot, path)
|
||||
return copyfrom_path, copyfrom_rev
|
||||
|
||||
def _simple_auth_check(fsroot):
|
||||
"""Return a 2-tuple: found_readable, found_unreadable."""
|
||||
found_unreadable = found_readable = 0
|
||||
if hasattr(fs, 'paths_changed2'):
|
||||
changes = fs.paths_changed2(fsroot)
|
||||
else:
|
||||
changes = fs.paths_changed(fsroot)
|
||||
paths = changes.keys()
|
||||
for path in paths:
|
||||
change = changes[path]
|
||||
pathtype = None
|
||||
if hasattr(change, 'node_kind'):
|
||||
if change.node_kind == core.svn_node_file:
|
||||
pathtype = vclib.FILE
|
||||
elif change.node_kind == core.svn_node_dir:
|
||||
pathtype = vclib.DIR
|
||||
parts = _path_parts(path)
|
||||
if pathtype is None:
|
||||
# Figure out the pathtype so we can query the authz subsystem.
|
||||
if change.change_kind == fs.path_change_delete:
|
||||
# Deletions are annoying, because they might be underneath
|
||||
# copies (make their previous location non-trivial).
|
||||
prev_parts = parts
|
||||
prev_rev = rev - 1
|
||||
parent_parts = parts[:-1]
|
||||
while parent_parts:
|
||||
parent_path = '/' + self._getpath(parent_parts)
|
||||
parent_change = changes.get(parent_path)
|
||||
if not (parent_change and \
|
||||
(parent_change.change_kind == fs.path_change_add or
|
||||
parent_change.change_kind == fs.path_change_replace)):
|
||||
del(parent_parts[-1])
|
||||
continue
|
||||
copyfrom_path, copyfrom_rev = \
|
||||
_get_change_copyinfo(fsroot, parent_path, parent_change)
|
||||
if copyfrom_path:
|
||||
prev_rev = copyfrom_rev
|
||||
prev_parts = _path_parts(copyfrom_path) + \
|
||||
parts[len(parent_parts):]
|
||||
break
|
||||
del(parent_parts[-1])
|
||||
pathtype = self._gettype(self._getpath(prev_parts), prev_rev)
|
||||
else:
|
||||
pathtype = self._gettype(self._getpath(parts), rev)
|
||||
if vclib.check_path_access(self, parts, pathtype, rev):
|
||||
found_readable = 1
|
||||
copyfrom_path, copyfrom_rev = \
|
||||
_get_change_copyinfo(fsroot, path, change)
|
||||
if copyfrom_path and copyfrom_path != path:
|
||||
parts = _path_parts(copyfrom_path)
|
||||
if not vclib.check_path_access(self, parts, pathtype,
|
||||
copyfrom_rev):
|
||||
found_unreadable = 1
|
||||
else:
|
||||
found_unreadable = 1
|
||||
if found_readable and found_unreadable:
|
||||
break
|
||||
return found_readable, found_unreadable
|
||||
|
||||
def _revinfo_helper(rev, include_changed_paths):
|
||||
# Get the revision property info. (Would use
|
||||
# editor.get_root_props(), but something is broken there...)
|
||||
revprops = fs.revision_proplist(self.fs_ptr, rev)
|
||||
msg, author, date, revprops = _split_revprops(revprops)
|
||||
|
||||
# Optimization: If our caller doesn't care about the changed
|
||||
# paths, and we don't need them to do authz determinations, let's
|
||||
# get outta here.
|
||||
if self.auth is None and not include_changed_paths:
|
||||
return date, author, msg, revprops, None
|
||||
|
||||
# If we get here, then we either need the changed paths because we
|
||||
# were asked for them, or we need them to do authorization checks.
|
||||
#
|
||||
# If we only need them for authorization checks, though, we
|
||||
# won't bother generating fully populated ChangedPath items (the
|
||||
# cost is too great).
|
||||
fsroot = self._getroot(rev)
|
||||
if include_changed_paths:
|
||||
found_readable, found_unreadable, changedpaths = \
|
||||
_get_changed_paths(fsroot)
|
||||
else:
|
||||
changedpaths = None
|
||||
found_readable, found_unreadable = _simple_auth_check(fsroot)
|
||||
|
||||
# Filter our metadata where necessary, and return the requested data.
|
||||
if found_unreadable:
|
||||
msg = None
|
||||
if not found_readable:
|
||||
author = None
|
||||
date = None
|
||||
return date, author, msg, revprops, changedpaths
|
||||
|
||||
# Consult the revinfo cache first. If we don't have cached info,
|
||||
# or our caller wants changed paths and we don't have those for
|
||||
# this revision, go do the real work.
|
||||
rev = self._getrev(rev)
|
||||
cached_info = self._revinfo_cache.get(rev)
|
||||
if not cached_info \
|
||||
or (include_changed_paths and cached_info[4] is None):
|
||||
cached_info = _revinfo_helper(rev, include_changed_paths)
|
||||
self._revinfo_cache[rev] = cached_info
|
||||
return tuple(cached_info)
|
||||
|
||||
def _log_helper(self, path, rev, lockinfo):
|
||||
rev_root = fs.revision_root(self.fs_ptr, rev)
|
||||
copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, path)
|
||||
date, author, msg, revprops, changes = self._revinfo(rev)
|
||||
if fs.is_file(rev_root, path):
|
||||
size = fs.file_length(rev_root, path)
|
||||
else:
|
||||
size = None
|
||||
return Revision(rev, date, author, msg, size, lockinfo, path,
|
||||
copyfrom_path and _cleanup_path(copyfrom_path),
|
||||
copyfrom_rev)
|
||||
|
||||
def _get_history(self, path, rev, path_type, limit=0, options={}):
|
||||
if self.youngest == 0:
|
||||
return []
|
||||
|
||||
rev_paths = []
|
||||
fsroot = self._getroot(rev)
|
||||
show_all_logs = options.get('svn_show_all_dir_logs', 0)
|
||||
if not show_all_logs:
|
||||
# See if the path is a file or directory.
|
||||
kind = fs.check_path(fsroot, path)
|
||||
if kind is core.svn_node_file:
|
||||
show_all_logs = 1
|
||||
|
||||
# Instantiate a NodeHistory collector object, and use it to collect
|
||||
# history items for PATH@REV.
|
||||
history = NodeHistory(self.fs_ptr, show_all_logs, limit)
|
||||
try:
|
||||
repos.svn_repos_history(self.fs_ptr, path, history.add_history,
|
||||
1, rev, options.get('svn_cross_copies', 0))
|
||||
except core.SubversionException, e:
|
||||
_fix_subversion_exception(e)
|
||||
if e.apr_err != _SVN_ERR_CEASE_INVOCATION:
|
||||
raise
|
||||
|
||||
# Now, iterate over those history items, checking for changes of
|
||||
# location, pruning as necessitated by authz rules.
|
||||
for hist_rev, hist_path in history:
|
||||
path_parts = _path_parts(hist_path)
|
||||
if not vclib.check_path_access(self, path_parts, path_type, hist_rev):
|
||||
break
|
||||
rev_paths.append([hist_rev, hist_path])
|
||||
return rev_paths
|
||||
|
||||
def _getpath(self, path_parts):
|
||||
return string.join(path_parts, '/')
|
||||
|
||||
def _getrev(self, rev):
|
||||
if rev is None or rev == 'HEAD':
|
||||
return self.youngest
|
||||
try:
|
||||
if type(rev) == type(''):
|
||||
while rev[0] == 'r':
|
||||
rev = rev[1:]
|
||||
rev = int(rev)
|
||||
except:
|
||||
raise vclib.InvalidRevision(rev)
|
||||
if (rev < 0) or (rev > self.youngest):
|
||||
raise vclib.InvalidRevision(rev)
|
||||
return rev
|
||||
|
||||
def _getroot(self, rev):
|
||||
try:
|
||||
return self._fsroots[rev]
|
||||
except KeyError:
|
||||
r = self._fsroots[rev] = fs.revision_root(self.fs_ptr, rev)
|
||||
return r
|
||||
|
||||
def _gettype(self, path, rev):
|
||||
# Similar to itemtype(), but without the authz check. Returns
|
||||
# None for missing paths.
|
||||
try:
|
||||
kind = fs.check_path(self._getroot(rev), path)
|
||||
except:
|
||||
return None
|
||||
if kind == core.svn_node_dir:
|
||||
return vclib.DIR
|
||||
if kind == core.svn_node_file:
|
||||
return vclib.FILE
|
||||
return None
|
||||
|
||||
##--- custom ---##
|
||||
|
||||
def get_youngest_revision(self):
|
||||
return self.youngest
|
||||
|
||||
def get_location(self, path, rev, old_rev):
|
||||
try:
|
||||
results = repos.svn_repos_trace_node_locations(self.fs_ptr, path,
|
||||
rev, [old_rev], _allow_all)
|
||||
except core.SubversionException, e:
|
||||
_fix_subversion_exception(e)
|
||||
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
|
||||
raise vclib.ItemNotFound(path)
|
||||
raise
|
||||
try:
|
||||
old_path = results[old_rev]
|
||||
except KeyError:
|
||||
raise vclib.ItemNotFound(path)
|
||||
|
||||
return _cleanup_path(old_path)
|
||||
|
||||
def created_rev(self, full_name, rev):
|
||||
return fs.node_created_rev(self._getroot(rev), full_name)
|
||||
|
||||
def last_rev(self, path, peg_revision, limit_revision=None):
|
||||
"""Given PATH, known to exist in PEG_REVISION, find the youngest
|
||||
revision older than, or equal to, LIMIT_REVISION in which path
|
||||
exists. Return that revision, and the path at which PATH exists in
|
||||
that revision."""
|
||||
|
||||
# Here's the plan, man. In the trivial case (where PEG_REVISION is
|
||||
# the same as LIMIT_REVISION), this is a no-brainer. If
|
||||
# LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's
|
||||
# history tracing code to find the right location. If, however,
|
||||
# LIMIT_REVISION is younger than PEG_REVISION, we suffer from
|
||||
# Subversion's lack of forward history searching. Our workaround,
|
||||
# ugly as it may be, involves a binary search through the revisions
|
||||
# between PEG_REVISION and LIMIT_REVISION to find our last live
|
||||
# revision.
|
||||
peg_revision = self._getrev(peg_revision)
|
||||
limit_revision = self._getrev(limit_revision)
|
||||
try:
|
||||
if peg_revision == limit_revision:
|
||||
return peg_revision, path
|
||||
elif peg_revision > limit_revision:
|
||||
fsroot = self._getroot(peg_revision)
|
||||
history = fs.node_history(fsroot, path)
|
||||
while history:
|
||||
path, peg_revision = fs.history_location(history)
|
||||
if peg_revision <= limit_revision:
|
||||
return max(peg_revision, limit_revision), _cleanup_path(path)
|
||||
history = fs.history_prev(history, 1)
|
||||
return peg_revision, _cleanup_path(path)
|
||||
else:
|
||||
orig_id = fs.node_id(self._getroot(peg_revision), path)
|
||||
while peg_revision != limit_revision:
|
||||
mid = (peg_revision + 1 + limit_revision) / 2
|
||||
try:
|
||||
mid_id = fs.node_id(self._getroot(mid), path)
|
||||
except core.SubversionException, e:
|
||||
_fix_subversion_exception(e)
|
||||
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
|
||||
cmp = -1
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
### Not quite right. Need a comparison function that only returns
|
||||
### true when the two nodes are the same copy, not just related.
|
||||
cmp = fs.compare_ids(orig_id, mid_id)
|
||||
|
||||
if cmp in (0, 1):
|
||||
peg_revision = mid
|
||||
else:
|
||||
limit_revision = mid - 1
|
||||
|
||||
return peg_revision, path
|
||||
finally:
|
||||
pass
|
||||
@@ -1,450 +0,0 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
# distribution or at http://viewvc.org/license-1.html.
|
||||
#
|
||||
# For more information, visit http://viewvc.org/
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
"Version Control lib driver for remotely accessible Subversion repositories."
|
||||
|
||||
import vclib
|
||||
import sys
|
||||
import os
|
||||
import string
|
||||
import re
|
||||
import tempfile
|
||||
import popen2
|
||||
import time
|
||||
from vclib.svn import Revision, ChangedPath, _datestr_to_date, _compare_paths, _cleanup_path
|
||||
from svn import core, delta, client, wc, ra
|
||||
|
||||
|
||||
### Require Subversion 1.3.0 or better. (for svn_ra_get_locations support)
|
||||
if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 3, 0):
|
||||
raise Exception, "Version requirement not met (needs 1.3.0 or better)"
|
||||
|
||||
|
||||
def _rev2optrev(rev):
|
||||
assert type(rev) is int
|
||||
rt = core.svn_opt_revision_t()
|
||||
rt.kind = core.svn_opt_revision_number
|
||||
rt.value.number = rev
|
||||
return rt
|
||||
|
||||
|
||||
def date_from_rev(svnrepos, rev):
|
||||
datestr = ra.svn_ra_rev_prop(svnrepos.ra_session, rev,
|
||||
'svn:date', svnrepos.pool)
|
||||
return _datestr_to_date(datestr, svnrepos.pool)
|
||||
|
||||
|
||||
def get_location(svnrepos, path, rev, old_rev):
|
||||
try:
|
||||
results = ra.get_locations(svnrepos.ra_session, path, rev,
|
||||
[old_rev], svnrepos.pool)
|
||||
except core.SubversionException, e:
|
||||
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
|
||||
raise vclib.ItemNotFound(path)
|
||||
raise
|
||||
|
||||
try:
|
||||
old_path = results[old_rev]
|
||||
except KeyError:
|
||||
raise vclib.ItemNotFound(path)
|
||||
|
||||
return _cleanup_path(old_path)
|
||||
|
||||
|
||||
def last_rev(svnrepos, path, peg_revision, limit_revision=None):
|
||||
"""Given PATH, known to exist in PEG_REVISION, find the youngest
|
||||
revision older than, or equal to, LIMIT_REVISION in which path
|
||||
exists. Return that revision, and the path at which PATH exists in
|
||||
that revision."""
|
||||
|
||||
# Here's the plan, man. In the trivial case (where PEG_REVISION is
|
||||
# the same as LIMIT_REVISION), this is a no-brainer. If
|
||||
# LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's
|
||||
# history tracing code to find the right location. If, however,
|
||||
# LIMIT_REVISION is younger than PEG_REVISION, we suffer from
|
||||
# Subversion's lack of forward history searching. Our workaround,
|
||||
# ugly as it may be, involves a binary search through the revisions
|
||||
# between PEG_REVISION and LIMIT_REVISION to find our last live
|
||||
# revision.
|
||||
peg_revision = svnrepos._getrev(peg_revision)
|
||||
limit_revision = svnrepos._getrev(limit_revision)
|
||||
if peg_revision == limit_revision:
|
||||
return peg_revision, path
|
||||
elif peg_revision > limit_revision:
|
||||
path = get_location(svnrepos, path, peg_revision, limit_revision)
|
||||
return limit_revision, path
|
||||
else:
|
||||
### Warning: this is *not* an example of good pool usage.
|
||||
direction = 1
|
||||
while peg_revision != limit_revision:
|
||||
mid = (peg_revision + 1 + limit_revision) / 2
|
||||
try:
|
||||
path = get_location(svnrepos, path, peg_revision, mid)
|
||||
except vclib.ItemNotFound:
|
||||
limit_revision = mid - 1
|
||||
else:
|
||||
peg_revision = mid
|
||||
return peg_revision, path
|
||||
|
||||
|
||||
def created_rev(svnrepos, full_name, rev):
|
||||
kind = ra.svn_ra_check_path(svnrepos.ra_session, full_name, rev,
|
||||
svnrepos.pool)
|
||||
if kind == core.svn_node_dir:
|
||||
retval = ra.svn_ra_get_dir(svnrepos.ra_session, full_name,
|
||||
rev, svnrepos.pool)
|
||||
if type(retval) == type([]) and len(retval) == 3:
|
||||
props = retval[2]
|
||||
else: # compat with older (broken) bindings
|
||||
props = retval
|
||||
return int(props[core.SVN_PROP_ENTRY_COMMITTED_REV])
|
||||
return core.SVN_INVALID_REVNUM
|
||||
|
||||
|
||||
class LastHistoryCollector:
|
||||
def __init__(self):
|
||||
self.has_history = 0
|
||||
|
||||
def add_history(self, paths, revision, author, date, message, pool):
|
||||
if not self.has_history:
|
||||
self.has_history = 1
|
||||
self.revision = revision
|
||||
self.author = author
|
||||
self.date = date
|
||||
self.message = message
|
||||
self.changes = []
|
||||
|
||||
if not paths:
|
||||
return
|
||||
changed_paths = paths.keys()
|
||||
changed_paths.sort(lambda a, b: _compare_paths(a, b))
|
||||
action_map = { 'D' : 'deleted',
|
||||
'A' : 'added',
|
||||
'R' : 'replaced',
|
||||
'M' : 'modified',
|
||||
}
|
||||
for changed_path in changed_paths:
|
||||
change = paths[changed_path]
|
||||
action = action_map.get(change.action, 'modified')
|
||||
### Wrong, diddily wrong wrong wrong. Can you say,
|
||||
### "Manufacturing data left and right because it hurts to
|
||||
### figure out the right stuff?"
|
||||
if change.copyfrom_path and change.copyfrom_rev:
|
||||
self.changes.append(ChangedPath(changed_path[1:], None, 0, 0,
|
||||
change.copyfrom_path,
|
||||
change.copyfrom_rev, action, 1))
|
||||
else:
|
||||
self.changes.append(ChangedPath(changed_path[1:], None, 0, 0,
|
||||
changed_path[1:], 0, action, 0))
|
||||
|
||||
def get_history(self):
|
||||
if not self.has_history:
|
||||
return None, None, None, None, None
|
||||
return self.revision, self.author, self.date, self.message, self.changes
|
||||
|
||||
|
||||
def _get_rev_details(svnrepos, rev, pool):
|
||||
lhc = LastHistoryCollector()
|
||||
client.svn_client_log([svnrepos.rootpath],
|
||||
_rev2optrev(rev), _rev2optrev(rev),
|
||||
1, 0, lhc.add_history, svnrepos.ctx, pool)
|
||||
return lhc.get_history()
|
||||
|
||||
|
||||
def get_revision_info(svnrepos, rev):
|
||||
rev, author, date, log, changes = \
|
||||
_get_rev_details(svnrepos, rev, svnrepos.pool)
|
||||
return _datestr_to_date(date, svnrepos.pool), author, log, changes
|
||||
|
||||
|
||||
class LogCollector:
|
||||
def __init__(self, path, show_all_logs):
|
||||
# This class uses leading slashes for paths internally
|
||||
if not path:
|
||||
self.path = '/'
|
||||
else:
|
||||
self.path = path[0] == '/' and path or '/' + path
|
||||
self.logs = []
|
||||
self.show_all_logs = show_all_logs
|
||||
|
||||
def add_log(self, paths, revision, author, date, message, pool):
|
||||
# Changed paths have leading slashes
|
||||
changed_paths = paths.keys()
|
||||
changed_paths.sort(lambda a, b: _compare_paths(a, b))
|
||||
copyfrom_path = copyfrom_rev = this_path = None
|
||||
if self.path in changed_paths:
|
||||
this_path = self.path
|
||||
change = paths[self.path]
|
||||
if change.copyfrom_path:
|
||||
this_path = change.copyfrom_path
|
||||
copyfrom_path = change.copyfrom_path[1:]
|
||||
copyfrom_rev = change.copyfrom_rev
|
||||
for changed_path in changed_paths:
|
||||
if changed_path != self.path:
|
||||
# If a parent of our path was copied, our "next previous"
|
||||
# (huh?) path will exist elsewhere (under the copy source).
|
||||
if (string.rfind(self.path, changed_path) == 0) and \
|
||||
self.path[len(changed_path)] == '/':
|
||||
change = paths[changed_path]
|
||||
if change.copyfrom_path:
|
||||
this_path = change.copyfrom_path + self.path[len(changed_path):]
|
||||
if self.show_all_logs or this_path:
|
||||
date = _datestr_to_date(date, pool)
|
||||
entry = Revision(revision, date, author, message, None,
|
||||
self.path[1:], copyfrom_path, copyfrom_rev)
|
||||
self.logs.append(entry)
|
||||
if this_path:
|
||||
self.path = this_path
|
||||
|
||||
|
||||
def get_logs(svnrepos, full_name, rev, files):
|
||||
dirents = svnrepos._get_dirents(full_name, rev)
|
||||
subpool = core.svn_pool_create(svnrepos.pool)
|
||||
rev_info_cache = { }
|
||||
for file in files:
|
||||
core.svn_pool_clear(subpool)
|
||||
entry = dirents[file.name]
|
||||
if rev_info_cache.has_key(entry.created_rev):
|
||||
rev, author, date, log = rev_info_cache[entry.created_rev]
|
||||
else:
|
||||
### i think this needs some get_last_history action to be accurate
|
||||
rev, author, date, log, changes = \
|
||||
_get_rev_details(svnrepos, entry.created_rev, subpool)
|
||||
rev_info_cache[entry.created_rev] = rev, author, date, log
|
||||
file.rev = rev
|
||||
file.author = author
|
||||
file.date = _datestr_to_date(date, subpool)
|
||||
file.log = log
|
||||
file.size = entry.size
|
||||
core.svn_pool_destroy(subpool)
|
||||
|
||||
def get_youngest_revision(svnrepos):
|
||||
return svnrepos.youngest
|
||||
|
||||
def temp_checkout(svnrepos, path, rev, pool):
|
||||
"""Check out file revision to temporary file"""
|
||||
temp = tempfile.mktemp()
|
||||
stream = core.svn_stream_from_aprfile(temp, pool)
|
||||
url = svnrepos.rootpath + (path and '/' + path)
|
||||
client.svn_client_cat(core.Stream(stream), url, _rev2optrev(rev),
|
||||
svnrepos.ctx, pool)
|
||||
core.svn_stream_close(stream)
|
||||
return temp
|
||||
|
||||
class SelfCleanFP:
|
||||
def __init__(self, path):
|
||||
self._fp = open(path, 'r')
|
||||
self._path = path
|
||||
self._eof = 0
|
||||
|
||||
def read(self, len=None):
|
||||
if len:
|
||||
chunk = self._fp.read(len)
|
||||
else:
|
||||
chunk = self._fp.read()
|
||||
if chunk == '':
|
||||
self._eof = 1
|
||||
return chunk
|
||||
|
||||
def readline(self):
|
||||
chunk = self._fp.readline()
|
||||
if chunk == '':
|
||||
self._eof = 1
|
||||
return chunk
|
||||
|
||||
def close(self):
|
||||
self._fp.close()
|
||||
os.remove(self._path)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def eof(self):
|
||||
return self._eof
|
||||
|
||||
|
||||
class SubversionRepository(vclib.Repository):
|
||||
def __init__(self, name, rootpath):
|
||||
# Init the client app
|
||||
core.apr_initialize()
|
||||
pool = core.svn_pool_create(None)
|
||||
core.svn_config_ensure(None, pool)
|
||||
|
||||
# Start populating our members
|
||||
self.pool = pool
|
||||
self.name = name
|
||||
self.rootpath = rootpath
|
||||
|
||||
# Setup the client context baton, complete with non-prompting authstuffs.
|
||||
ctx = client.svn_client_ctx_t()
|
||||
providers = []
|
||||
providers.append(client.svn_client_get_simple_provider(pool))
|
||||
providers.append(client.svn_client_get_username_provider(pool))
|
||||
providers.append(client.svn_client_get_ssl_server_trust_file_provider(pool))
|
||||
providers.append(client.svn_client_get_ssl_client_cert_file_provider(pool))
|
||||
providers.append(client.svn_client_get_ssl_client_cert_pw_file_provider(pool))
|
||||
ctx.auth_baton = core.svn_auth_open(providers, pool)
|
||||
ctx.config = core.svn_config_get_config(None, pool)
|
||||
self.ctx = ctx
|
||||
|
||||
ra_callbacks = ra.svn_ra_callbacks_t()
|
||||
ra_callbacks.auth_baton = ctx.auth_baton
|
||||
self.ra_session = ra.svn_ra_open(self.rootpath, ra_callbacks, None,
|
||||
ctx.config, pool)
|
||||
self.youngest = ra.svn_ra_get_latest_revnum(self.ra_session, pool)
|
||||
self._dirent_cache = { }
|
||||
|
||||
def __del__(self):
|
||||
core.svn_pool_destroy(self.pool)
|
||||
core.apr_terminate()
|
||||
|
||||
def itemtype(self, path_parts, rev):
|
||||
path = self._getpath(path_parts[:-1])
|
||||
rev = self._getrev(rev)
|
||||
if not len(path_parts):
|
||||
return vclib.DIR
|
||||
dirents = self._get_dirents(path, rev)
|
||||
try:
|
||||
entry = dirents[path_parts[-1]]
|
||||
if entry.kind == core.svn_node_dir:
|
||||
return vclib.DIR
|
||||
if entry.kind == core.svn_node_file:
|
||||
return vclib.FILE
|
||||
except KeyError:
|
||||
raise vclib.ItemNotFound(path_parts)
|
||||
|
||||
def openfile(self, path_parts, rev):
|
||||
rev = self._getrev(rev)
|
||||
url = self.rootpath
|
||||
if len(path_parts):
|
||||
url = self.rootpath + '/' + self._getpath(path_parts)
|
||||
tmp_file = tempfile.mktemp()
|
||||
stream = core.svn_stream_from_aprfile(tmp_file, self.pool)
|
||||
### rev here should be the last history revision of the URL
|
||||
client.svn_client_cat(core.Stream(stream), url,
|
||||
_rev2optrev(rev), self.ctx, self.pool)
|
||||
core.svn_stream_close(stream)
|
||||
return SelfCleanFP(tmp_file), rev
|
||||
|
||||
def listdir(self, path_parts, rev, options):
|
||||
path = self._getpath(path_parts)
|
||||
rev = self._getrev(rev)
|
||||
entries = [ ]
|
||||
dirents = self._get_dirents(path, rev)
|
||||
for name in dirents.keys():
|
||||
entry = dirents[name]
|
||||
if entry.kind == core.svn_node_dir:
|
||||
kind = vclib.DIR
|
||||
elif entry.kind == core.svn_node_file:
|
||||
kind = vclib.FILE
|
||||
entries.append(vclib.DirEntry(name, kind))
|
||||
return entries
|
||||
|
||||
def dirlogs(self, path_parts, rev, entries, options):
|
||||
get_logs(self, self._getpath(path_parts), self._getrev(rev), entries)
|
||||
|
||||
def itemlog(self, path_parts, rev, options):
|
||||
full_name = self._getpath(path_parts)
|
||||
rev = self._getrev(rev)
|
||||
|
||||
# It's okay if we're told to not show all logs on a file -- all
|
||||
# the revisions should match correctly anyway.
|
||||
lc = LogCollector(full_name, options.get('svn_show_all_dir_logs', 0))
|
||||
dir_url = self.rootpath
|
||||
if full_name:
|
||||
dir_url = dir_url + '/' + full_name
|
||||
|
||||
cross_copies = options.get('svn_cross_copies', 0)
|
||||
client.svn_client_log([dir_url], _rev2optrev(rev), _rev2optrev(1),
|
||||
1, not cross_copies, lc.add_log,
|
||||
self.ctx, self.pool)
|
||||
revs = lc.logs
|
||||
revs.sort()
|
||||
prev = None
|
||||
for rev in revs:
|
||||
rev.prev = prev
|
||||
prev = rev
|
||||
|
||||
return revs
|
||||
|
||||
def annotate(self, path_parts, rev):
|
||||
path = self._getpath(path_parts)
|
||||
rev = self._getrev(rev)
|
||||
url = self.rootpath + (path and '/' + path)
|
||||
|
||||
blame_data = []
|
||||
|
||||
def _blame_cb(line_no, revision, author, date,
|
||||
line, pool, blame_data=blame_data):
|
||||
prev_rev = None
|
||||
if revision > 1:
|
||||
prev_rev = revision - 1
|
||||
blame_data.append(_item(text=line, line_number=line_no+1,
|
||||
rev=revision, prev_rev=prev_rev,
|
||||
author=author, date=None))
|
||||
|
||||
client.svn_client_blame(url, _rev2optrev(1), _rev2optrev(rev),
|
||||
_blame_cb, self.ctx, self.pool)
|
||||
|
||||
return blame_data, rev
|
||||
|
||||
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
|
||||
p1 = self._getpath(path_parts1)
|
||||
p2 = self._getpath(path_parts2)
|
||||
r1 = self._getrev(rev1)
|
||||
r2 = self._getrev(rev2)
|
||||
args = vclib._diff_args(type, options)
|
||||
|
||||
try:
|
||||
temp1 = temp_checkout(self, p1, r1, self.pool)
|
||||
temp2 = temp_checkout(self, p2, r2, self.pool)
|
||||
info1 = p1, date_from_rev(self, r1), r1
|
||||
info2 = p2, date_from_rev(self, r2), r2
|
||||
return vclib._diff_fp(temp1, temp2, info1, info2, args)
|
||||
except vclib.svn.core.SubversionException, e:
|
||||
if e.apr_err == vclib.svn.core.SVN_ERR_FS_NOT_FOUND:
|
||||
raise vclib.InvalidRevision
|
||||
raise
|
||||
|
||||
def _getpath(self, path_parts):
|
||||
return string.join(path_parts, '/')
|
||||
|
||||
def _getrev(self, rev):
|
||||
if rev is None or rev == 'HEAD':
|
||||
return self.youngest
|
||||
try:
|
||||
rev = int(rev)
|
||||
except ValueError:
|
||||
raise vclib.InvalidRevision(rev)
|
||||
if (rev < 0) or (rev > self.youngest):
|
||||
raise vclib.InvalidRevision(rev)
|
||||
return rev
|
||||
|
||||
def _get_dirents(self, path, rev):
|
||||
if path:
|
||||
key = str(rev) + '/' + path
|
||||
dir_url = self.rootpath + '/' + path
|
||||
else:
|
||||
key = str(rev)
|
||||
dir_url = self.rootpath
|
||||
dirents = self._dirent_cache.get(key)
|
||||
if dirents:
|
||||
return dirents
|
||||
dirents = client.svn_client_ls(dir_url, _rev2optrev(rev), 0,
|
||||
self.ctx, self.pool)
|
||||
self._dirent_cache[key] = dirents
|
||||
return dirents
|
||||
|
||||
|
||||
class _item:
|
||||
def __init__(self, **kw):
|
||||
vars(self).update(kw)
|
||||
3252
lib/viewvc.py
3252
lib/viewvc.py
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# -*-python-*-
|
||||
#
|
||||
# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved.
|
||||
# Copyright (C) 1999-2012 The ViewCVS Group. All Rights Reserved.
|
||||
#
|
||||
# By using this file, you agree to the terms and conditions set forth in
|
||||
# the LICENSE.html file which can be found at the top level of the ViewVC
|
||||
|
||||
82
notes/authz-dev-TODO
Normal file
82
notes/authz-dev-TODO
Normal file
@@ -0,0 +1,82 @@
|
||||
Here lie TODO items for the pluggable authz system:
|
||||
|
||||
* Subversion uses path privelege to determine visibility of revision
|
||||
metadata. That logic is pretty Subversion-specific, so it feels like it
|
||||
belongs outside the vcauth library as just a helper function in viewvc.py
|
||||
or something. The algorithm is something like this (culled from the
|
||||
CollabNet implementation, and not expected to work as edited):
|
||||
|
||||
# Subversion revision access levels
|
||||
REVISION_ACCESS_NONE = 0
|
||||
REVISION_ACCESS_PARTIAL = 1
|
||||
REVISION_ACCESS_FULL = 2
|
||||
|
||||
def check_svn_revision_access(request, rev):
|
||||
# Check our revision access cache first.
|
||||
if request.rev_access_cache.has_key(rev):
|
||||
return request.rev_access_cache[rev]
|
||||
|
||||
# Check our cached answer to the question "Does the user have
|
||||
# an all-access or a not-at-all-access pass?"
|
||||
if request.full_access is not None:
|
||||
return request.full_access \
|
||||
and REVISION_ACCESS_FULL or REVISION_ACCESS_NONE
|
||||
|
||||
# Get a list of paths changed in REV.
|
||||
### FIXME: There outta be a vclib-complaint way to do this,
|
||||
### as this won't work for vclib.svn_ra.
|
||||
import svn.fs
|
||||
rev_root = svn.fs.revision_root(self.repos.fs_ptr, rev)
|
||||
changes = svn.fs.paths_changed(rev_root)
|
||||
|
||||
# Loop over the list of changed paths, asking the access question
|
||||
# for each one. We'll track whether we've found any readable paths
|
||||
# as well as any un-readable (non-authorized) paths, and quit
|
||||
# checking as soon as we know our revision access level.
|
||||
found_readable = 0
|
||||
found_unreadable = 0
|
||||
for path in changes.keys():
|
||||
parts = _path_parts(path)
|
||||
kind = request.repos.itemtype(parts, rev)
|
||||
if kind == vclib.DIR:
|
||||
access = request.auth.check_dir_access(parts, rev)
|
||||
elif:
|
||||
access = request.auth.check_file_access(parts, rev)
|
||||
if access:
|
||||
found_readable = 1
|
||||
else:
|
||||
found_unreadable = 1
|
||||
# Optimization: if we've found at least one readable, and one
|
||||
# unreadable, we needn't ask about any more paths.
|
||||
if found_readable and found_unreadable:
|
||||
break
|
||||
|
||||
# If there are paths but we can't read any of them, no access is
|
||||
# granted.
|
||||
if len(changes) and not found_readable:
|
||||
request.rev_access_cache[rev] = REVISION_ACCESS_NONE
|
||||
# If we found at least one unreadable path, partial access is
|
||||
# granted.
|
||||
elif found_unreadable:
|
||||
request.rev_access_cache[rev] = REVISION_ACCESS_PARTIAL
|
||||
# Finally, if there were no paths at all, or none of the existing
|
||||
# ones were unreadable, grant full access.
|
||||
else:
|
||||
request.rev_access_cache[rev] = REVISION_ACCESS_FULL
|
||||
return request.rev_access_cache[rev]
|
||||
|
||||
The problems are: where does one hang the revision access cache
|
||||
so that it doesn't survive a given request? On the request, as
|
||||
shown in the edited code above?
|
||||
|
||||
Can we actually get a good interface into the vcauth layer for
|
||||
asking the all-access / no-access question? Obviously each vcauth
|
||||
provider can cache that value for itself and use it as it deems
|
||||
necessary, but ideally the revision access level question will
|
||||
want to know if said auth provider was able to answer the question
|
||||
and, if so, what the answer was.
|
||||
|
||||
Another off-the-wall idea -- let's just teach Subversion to do
|
||||
this calculation as part of a libsvn_repos API.
|
||||
|
||||
|
||||
BIN
notes/logo/viewvc-logo.odg
Normal file
BIN
notes/logo/viewvc-logo.odg
Normal file
Binary file not shown.
BIN
notes/logo/viewvc-logo.pdf
Normal file
BIN
notes/logo/viewvc-logo.pdf
Normal file
Binary file not shown.
2
notes/logo/viewvc-logo.svg
Normal file
2
notes/logo/viewvc-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 10 KiB |
@@ -24,10 +24,12 @@ numbers, and not literal):
|
||||
Commit any modifications. NOTE: This step should not be necessary
|
||||
for patch releases.
|
||||
|
||||
3. Verify that copyright years are correct in both the LICENSE.html
|
||||
3. Verify that copyright years are correct in both the license-1.html
|
||||
file and the source code.
|
||||
|
||||
4. Update and commit the 'CHANGES' file.
|
||||
4. Update and commit the 'CHANGES' file, using any available crystal
|
||||
balls or other forward-looking devices to take a stab at the
|
||||
release date.
|
||||
|
||||
5. Test, test, test! There is no automatic testsuite available. So
|
||||
just run with permuting different `viewvc.conf' settings... and
|
||||
@@ -38,48 +40,86 @@ numbers, and not literal):
|
||||
should exactly reflect what you wish to distribute and dub "the
|
||||
release".
|
||||
|
||||
7. Edit the file 'lib/viewvc.py' and remove the "-dev" suffix from
|
||||
7. Update your release branch working copy to HEAD.
|
||||
|
||||
svn up
|
||||
|
||||
8. Edit the file 'lib/viewvc.py' and remove the "-dev" suffix from
|
||||
__version__. The remainder should be of the form "X.Y.Z", where X,
|
||||
Y, and Z are positive integers. Do NOT commit this change.
|
||||
Y, and Z are positive integers.
|
||||
|
||||
8. Update your working copy to HEAD, and tag the release:
|
||||
*** Do NOT commit this change. ***
|
||||
|
||||
svn update
|
||||
svn cp -m "Tag the X.Y.Z final release." . \
|
||||
http://viewvc.tigris.org/svn/viewvc/tags/X.Y.Z
|
||||
9. "Peg" the contributed templates externals definition to the
|
||||
current HEAD revision:
|
||||
|
||||
9. Go into an empty directory and run the 'make-release' script:
|
||||
svn pedit svn:externals .
|
||||
|
||||
(squeeze "-rBASE_REV", where BASE_REV is the current HEAD revision
|
||||
number, between 'templates-contrib' and the target URL).
|
||||
|
||||
tools/make-release viewvc-X.Y.Z X.Y.Z
|
||||
*** Do NOT commit this change. ***
|
||||
|
||||
10. Verify the archive files:
|
||||
10. Tag the release:
|
||||
|
||||
svn cp -m "Tag the X.Y.Z final release." . ^/tags/X.Y.Z
|
||||
|
||||
This will create a copy of the release branch, plus your local
|
||||
modifications to the svn:externals property and lib/viewvc.py
|
||||
file, to the tag location.
|
||||
|
||||
11. Revert the changes in your working copy.
|
||||
|
||||
svn revert -R .
|
||||
|
||||
12. Go into an empty directory and run the 'make-release' script:
|
||||
|
||||
tools/make-release viewvc-X.Y.Z tags/X.Y.Z
|
||||
|
||||
13. Verify the archive files:
|
||||
|
||||
- do they have a LICENSE.html file?
|
||||
- do they have necessary include documentation?
|
||||
- do they *not* have unnecessary stuff?
|
||||
- do they install and work correctly?
|
||||
|
||||
11. Upload the created archive files (tar.gz and zip) into the Files
|
||||
14. Upload the created archive files (tar.gz and zip) into the Files
|
||||
and Documents section of the Tigris.org project, and modify the
|
||||
CHECKSUMS document there accordingly. Also, drop a copy of the
|
||||
archive files into the root directory of the viewvc.org website
|
||||
(unversioned).
|
||||
CHECKSUMS document there accordingly:
|
||||
|
||||
12. Update the websites (both the viewvc.org/ and www/ ones) to refer
|
||||
to the new release files.
|
||||
http://viewvc.tigris.org/servlets/ProjectDocumentList?folderID=6004
|
||||
|
||||
13. Edit the file 'lib/viewvc.py' again, re-adding the "-dev" suffix
|
||||
and incrementing the patch number assigned to the __version__
|
||||
variable, and commit:
|
||||
Also, drop a copy of the archive files into the root directory of
|
||||
the viewvc.org website (unversioned).
|
||||
|
||||
15. Update the Tigris.org website (^/trunk/www/index.html) to refer to
|
||||
the new release files and commit.
|
||||
|
||||
svn ci -m "Bump latest advertised release."
|
||||
|
||||
16. Back on the release branch, edit the file 'lib/viewvc.py' again,
|
||||
incrementing the patch number assigned to the __version__
|
||||
variable. Add a new empty block in the branch's CHANGES file.
|
||||
Commit your changes:
|
||||
|
||||
svn ci -m "Begin a new release cycle."
|
||||
|
||||
14. Edit the Issue Tracker configuration options, adding a new Version
|
||||
17. Edit the Issue Tracker configuration options, adding a new Version
|
||||
for the just-released one, and a new Milestone for the next patch
|
||||
(and possibly, minor or major) release. (For the Milestone sort
|
||||
key, use a packed integer XXYYZZ: 1.0.3 == 10003, 2.11.4 == 21104.)
|
||||
|
||||
15. Write an announcement explaining all the cool new features and
|
||||
post it to the announce@ list, to the project's News area, and to
|
||||
other places interested in this sort of stuff, such as Freshmeat
|
||||
(http://www.freshmeat.net).
|
||||
http://viewvc.tigris.org/issues/editversions.cgi?component=viewvc&action=add
|
||||
http://viewvc.tigris.org/issues/editmilestones.cgi?component=viewvc&action=add
|
||||
|
||||
18. Send to the announce@ list a message explaining all the cool new
|
||||
features.
|
||||
|
||||
http://viewvc.tigris.org/ds/viewForumSummary.do?dsForumId=4253
|
||||
|
||||
19. Post a new release notification at Freecode.
|
||||
|
||||
https://freecode.com/projects/viewvc/releases/new
|
||||
|
||||
20. Merge CHANGES for this release into the CHANGES file for newer
|
||||
release lines and commit.
|
||||
|
||||
78
notes/vclib-enhancements.txt
Normal file
78
notes/vclib-enhancements.txt
Normal file
@@ -0,0 +1,78 @@
|
||||
The following is an email from a developer who was integrating bzr
|
||||
into ViewVC in which he shares some thoughts on how to further
|
||||
abstract the version control system interactions into first-class APIs
|
||||
in the vclib module.
|
||||
|
||||
Subject: Re: [ViewCVS-dev] difflib module
|
||||
Date: Wed, 1 Jun 2005 16:59:10 -0800
|
||||
From: "Johan Rydberg" <jrydberg@gnu.org>
|
||||
To: "Michael Pilato" <cmpilato@collab.net>
|
||||
Cc: <viewcvs-dev@lyra.org>
|
||||
|
||||
"C. Michael Pilato" <cmpilato@collab.net> writes:
|
||||
|
||||
>> I've tried to minimize the changes to the viewcvs.py, but of course
|
||||
>> there are a few places where some things has to be altered.
|
||||
>
|
||||
> Well, if along the way, you have ideas about how to further abstract
|
||||
> stuff into the vclib/ modules, please post.
|
||||
|
||||
I came up with a few as off now;
|
||||
|
||||
* Generalize revision counting; svn starts from 0, bzr starts from 1.
|
||||
Can be done by a constant; request.repos.MIN_REVNO. For CVS I'm not
|
||||
sure exactly what should be done. Right now this is only used in
|
||||
view_revision_svn, so it is not a problem in the short term.
|
||||
|
||||
* Generalize view_diff;
|
||||
|
||||
* Have a repo-method diff(file1, rev1, file2, rev2, args) that returns
|
||||
(date1, date2, fp). Means human_readbale_diff and raw_diff does not
|
||||
have to parse dates. Good for VCS that does not have the date in
|
||||
the diff. [### DONE ###]
|
||||
|
||||
* I'm not sure you should require GNU diff. Some VCS may use own
|
||||
diff mechanisms (bzr uses difflib, _or_ GNU diff when needed.
|
||||
Monotone uses its own, IIRC.)
|
||||
|
||||
* Generalize view_revision ;
|
||||
|
||||
* Have a method, revision_info(), which returns (date, author, msg,
|
||||
changes) much like vclib.svn.get_revision_info. The CVS version
|
||||
can raise a ViewCVSException. [### DONE ###]
|
||||
|
||||
* Establish a convention for renamed/copied files; current should
|
||||
work good enough (change.base_path, change.base_rev) but action
|
||||
string must be same for both svn and others.
|
||||
|
||||
* request.repos.rev (or .revision) should give the current revision
|
||||
number of the repo. No need for this (from view_directory):
|
||||
|
||||
if request.roottype == 'svn':
|
||||
revision = str(vclib.svn.created_rev(request.repos, request.where))
|
||||
|
||||
If svn needs the full name of the repo, why not give it when the
|
||||
repo is created?
|
||||
|
||||
* request.repos.youngest vs vclib.svn.get_youngest_revision(REPO)
|
||||
|
||||
* More object oriented;
|
||||
|
||||
* The subversion backend is not really object oriented. viewcfg.py uses
|
||||
a lot function from vclib.svn, which could instead be methods of the
|
||||
Repository class. Example:
|
||||
|
||||
diffobj = vclib.svn.do_diff(request.repos, p1, int(rev1),
|
||||
p2, int(rev2), args)
|
||||
|
||||
This should be a method of the repository;
|
||||
|
||||
diffobj = request.repos.do_diff(p1, rev1, ...)
|
||||
|
||||
I have identified the following functions;
|
||||
|
||||
- vclib.svn.created_rev
|
||||
- vclib.svn.get_youngest_revision
|
||||
- vclib.svn.date_from_rev
|
||||
- vclib.svn.do_diff
|
||||
- vclib.svn.get_revision_info
|
||||
@@ -1,43 +0,0 @@
|
||||
[# setup page definitions][define page_title]Annotation of /[where][end][define help_href][docroot]/help_rootview.html[end][# end][include "include/header.ezt" "annotate"]
|
||||
[include "include/file_header.ezt"]
|
||||
|
||||
<hr />
|
||||
|
||||
<p>
|
||||
Revision [if-any revision_href]<a href="[revision_href]"><strong>[rev]</strong></a>[else]<strong>[rev]</strong>[end] -
|
||||
(<a href="[view_href]"><strong>view</strong></a>)
|
||||
(<a href="[download_href]"><strong>download</strong></a>)
|
||||
[if-any download_text_href](<a href="[download_text_href]"><strong>as text</strong></a>)[end]
|
||||
[if-any orig_path]
|
||||
<br />Original Path: <a href="[orig_href]"><em>[orig_path]</em></a>
|
||||
[end]
|
||||
</p>
|
||||
|
||||
|
||||
[define class1]vc_row_even[end]
|
||||
[define class2]vc_row_odd[end]
|
||||
[define last_rev]0[end]
|
||||
[define rowclass][class1][end]
|
||||
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
[for lines]
|
||||
[is lines.rev last_rev]
|
||||
[else]
|
||||
[is rowclass class1]
|
||||
[define rowclass][class2][end]
|
||||
[else]
|
||||
[define rowclass][class1][end]
|
||||
[end]
|
||||
[end]
|
||||
|
||||
<tr class="[rowclass]" id="l[lines.line_number]">
|
||||
<td class="vc_blame_line">[lines.line_number] :</td>
|
||||
<td class="vc_blame_author">[is lines.rev last_rev] [else][lines.author][end]</td>
|
||||
<td class="vc_blame_rev">[is lines.rev last_rev] [else][if-any lines.diff_url]<a href="[lines.diff_url]">[end][lines.rev][if-any lines.diff_url]</a>[end][end]</td>
|
||||
<td class="vc_blame_text">[lines.text]</td>
|
||||
</tr>
|
||||
[define last_rev][lines.rev][end]
|
||||
[end]
|
||||
</table>
|
||||
|
||||
[include "include/footer.ezt"]
|
||||
@@ -12,19 +12,24 @@
|
||||
<pre class="vc_raw_diff">[raw_diff]</pre>
|
||||
[end]
|
||||
|
||||
[define left_view_href][if-any left.prefer_markup][left.view_href][else][if-any left.download_href][left.download_href][end][end][end]
|
||||
[define right_view_href][if-any right.prefer_markup][right.view_href][else][if-any right.download_href][right.download_href][end][end][end]
|
||||
|
||||
[if-any changes]
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr class="vc_diff_header">
|
||||
<th style="width:6%;"></th>
|
||||
<th style="width:47%; vertical-align:top;">
|
||||
[is path_left path_right][else][path_left][end]
|
||||
revision [rev_left], [date_left]
|
||||
[if-any tag_left]<br />Tag: [tag_left][end]
|
||||
[is left.path right.path][else][left.path][end]
|
||||
revision [if-any left_view_href]<a href="[left_view_href]">[end][left.rev][if-any left_view_href]</a>[end][if-any left.author] by <em>[left.author]</em>[end],
|
||||
[left.date]
|
||||
[if-any left.tag]<br />Tag: [left.tag][end]
|
||||
</th>
|
||||
<th style="width:47%; vertical-align:top;">
|
||||
[is path_left path_right][else][path_right][end]
|
||||
revision [rev_right], [date_right]
|
||||
[if-any tag_right]<br />Tag: [tag_right][end]
|
||||
[is left.path right.path][else][right.path][end]
|
||||
revision [if-any right_view_href]<a href="[right_view_href]">[end][right.rev][if-any right_view_href]</a>[end][if-any right.author] by <em>[right.author]</em>[end],
|
||||
[right.date]
|
||||
[if-any right.tag]<br />Tag: [right.tag][end]
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
@@ -44,7 +49,7 @@
|
||||
[else]
|
||||
[is changes.type "add"]
|
||||
<tr>
|
||||
<td id="l[changes.line_number]">[if-any annotate_href]<a href="[annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
|
||||
<td class="vc_diff_line_number" id="l[changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
|
||||
<td class="vc_diff_empty"> </td>
|
||||
<td class="vc_diff_add"> [changes.right]</td>
|
||||
</tr>
|
||||
@@ -59,7 +64,7 @@
|
||||
[is changes.type "change"]
|
||||
<tr>
|
||||
[if-any changes.have_right]
|
||||
<td id="l[changes.line_number]">[if-any annotate_href]<a href="[annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
|
||||
<td class="vc_diff_line_number" id="l[changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
|
||||
[else]
|
||||
<td></td>
|
||||
[end]
|
||||
@@ -107,7 +112,7 @@
|
||||
</tr>
|
||||
[else]
|
||||
<tr>
|
||||
<td id="l[changes.line_number]">[if-any annotate_href]<a href="[annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
|
||||
<td class="vc_diff_line_number" id="l[changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
|
||||
<td class="vc_diff_nochange"> [changes.left]</td>
|
||||
<td class="vc_diff_nochange"> [changes.right]</td>
|
||||
</tr>
|
||||
@@ -129,12 +134,12 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
[is path_left path_right][else][path_left][end]
|
||||
Revision [rev_left]
|
||||
[is left.path right.path][else][left.path][end]
|
||||
Revision [left.rev]
|
||||
</th>
|
||||
<th colspan="2">
|
||||
[is path_left path_right][else][path_right][end]
|
||||
Revision [rev_right]
|
||||
[is left.path right.path][else][right.path][end]
|
||||
Revision [right.rev]
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -160,8 +165,8 @@
|
||||
<table class="vc_idiff">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>r[rev_left]</th>
|
||||
<th>r[rev_right]</th>
|
||||
<th>r[left.rev]</th>
|
||||
<th>r[right.rev]</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -191,10 +196,11 @@
|
||||
<td>
|
||||
<form method="get" action="[diff_format_action]">
|
||||
<div>
|
||||
[diff_format_hidden_values]
|
||||
[for diff_format_hidden_values]<input type="hidden" name="[diff_format_hidden_values.name]" value="[diff_format_hidden_values.value]"/>[end]
|
||||
<select name="diff_format" onchange="submit()">
|
||||
<option value="h" [is diff_format "h"]selected="selected"[end]>Colored Diff</option>
|
||||
<option value="l" [is diff_format "l"]selected="selected"[end]>Long Colored Diff</option>
|
||||
<option value="f" [is diff_format "f"]selected="selected"[end]>Full Colored Diff</option>
|
||||
<option value="u" [is diff_format "u"]selected="selected"[end]>Unidiff</option>
|
||||
<option value="c" [is diff_format "c"]selected="selected"[end]>Context Diff</option>
|
||||
<option value="s" [is diff_format "s"]selected="selected"[end]>Side by Side</option>
|
||||
@@ -212,7 +218,7 @@
|
||||
<td>Legend:<br />
|
||||
<table cellspacing="0" cellpadding="1">
|
||||
<tr>
|
||||
<td style="text-align:center;" class="vc_diff_remove">Removed from v.[rev_left]</td>
|
||||
<td style="text-align:center;" class="vc_diff_remove">Removed from v.[left.rev]</td>
|
||||
<td class="vc_diff_empty"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -220,7 +226,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="vc_diff_empty"> </td>
|
||||
<td style="text-align:center;" class="vc_diff_add">Added in v.[rev_right]</td>
|
||||
<td style="text-align:center;" class="vc_diff_add">Added in v.[right.rev]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
[include "include/dir_header.ezt"]
|
||||
|
||||
<table cellspacing="1" cellpadding="2">
|
||||
<table cellspacing="1" cellpadding="2" class="fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="vc_header[is sortby "file"]_sort[end]" colspan="2">
|
||||
<a href="[sortby_file_href]#dirlist">File
|
||||
<th style="width: 200px" class="vc_header[is sortby "file"]_sort[end]">
|
||||
[if-any sortby_file_href]<a href="[sortby_file_href]#dirlist">File</a>[else]File[end]
|
||||
[is sortby "file"]
|
||||
<img class="vc_sortarrow" alt="[is sortdir "down"](rev)[end]"
|
||||
width="13" height="13"
|
||||
src="[docroot]/images/[is sortdir "up"]up[else]down[end].png" />
|
||||
[end]
|
||||
</a>
|
||||
</th>
|
||||
<th style="width: 96px" class="vc_header"></th>
|
||||
[if-any sortby_rev_href]
|
||||
<th class="vc_header[is sortby "rev"]_sort[end]">
|
||||
<a href="[sortby_rev_href]#dirlist">Last Change
|
||||
<a href="[sortby_rev_href]#dirlist">Last Change</a>
|
||||
[is sortby "rev"]
|
||||
<img class="vc_sortarrow" alt="[is sortdir "down"](rev)[end]"
|
||||
width="13" height="13"
|
||||
src="[docroot]/images/[is sortdir "up"]up[else]down[end].png" />
|
||||
[end]
|
||||
</a>
|
||||
[else]
|
||||
<th class="vc_header[is sortby "date"]_sort[end]">
|
||||
[if-any sortby_date_href]<a href="[sortby_date_href]#dirlist">Last Change</a>[else]Last Change[end]
|
||||
[is sortby "date"]
|
||||
<img class="vc_sortarrow" alt="[is sortdir "down"](date)[end]"
|
||||
width="13" height="13"
|
||||
src="[docroot]/images/[is sortdir "up"]up[else]down[end].png" />
|
||||
[end]
|
||||
[end]
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -27,79 +36,92 @@
|
||||
<tbody>
|
||||
[if-any up_href]
|
||||
<tr class="vc_row_odd">
|
||||
<td>
|
||||
<td style="width: 200px">
|
||||
<a href="[up_href]">
|
||||
<img src="[docroot]/images/back_small.png" alt="" width="16" height="16"
|
||||
<img src="[docroot]/images/back_small.png" alt="" class="vc_icon"
|
||||
/> Parent Directory</a>
|
||||
</td>
|
||||
<td> </td>
|
||||
<td style="width: 96px; font-size: 0;"></td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
[end]
|
||||
[for entries]
|
||||
<tr class="vc_row_[if-index entries even]even[else]odd[end]">
|
||||
<td>
|
||||
<td style="width: 200px">
|
||||
<a name="[entries.anchor]" href="[is entries.pathtype "dir"][entries.view_href][else][if-any entries.prefer_markup][entries.view_href][else][entries.download_href][end][end]" title="[is entries.pathtype "dir"]View Directory Contents[else][if-any entries.prefer_markup]View[else]Download[end] File Contents[end]">
|
||||
<img src="[docroot]/images/[is entries.pathtype "dir"]dir[else][is entries.state "dead"]broken[else]text[end][end].png" alt="" width="16" height="16" />
|
||||
<img src="[docroot]/images/[is entries.pathtype "dir"]dir[else][is entries.state "dead"]broken[else]text[end][end].png" alt="" class="vc_icon" />
|
||||
[entries.name][is entries.pathtype "dir"]/[end]</a>
|
||||
[if-any entries.lockinfo]<img src="[docroot]/images/lock.png" alt="locked" class="vc_icon" title="Locked by [entries.lockinfo]" />[end]
|
||||
[is entries.state "dead"](dead)[end]
|
||||
</td>
|
||||
|
||||
[if-any entries.errors]
|
||||
<td colspan="2">[for entries.errors]<em>[entries.errors]</em>[end]</td>
|
||||
[else]
|
||||
<td style="width:1%; white-space: nowrap">
|
||||
[define view_icon_link][end]
|
||||
[define graph_icon_link][end]
|
||||
[define download_icon_link][end]
|
||||
[define annotate_icon_link][end]
|
||||
[define log_icon_link][if-any entries.log_href]<a
|
||||
href="[entries.log_href]"
|
||||
title="View Log"><img
|
||||
src="[docroot]/images/log.png"
|
||||
alt="View Log"
|
||||
class="vc_icon" /></a>[end][end]
|
||||
|
||||
[# Icon column. We might want to add more icons like a tarball
|
||||
# icon for directories or a diff to previous icon for files. ]
|
||||
|
||||
[if-any entries.log_href]
|
||||
<a href="[entries.log_href]"><img
|
||||
src="[docroot]/images/log.png"
|
||||
alt="View Log" width="16" height="16" /></a>
|
||||
[is entries.pathtype "dir"]
|
||||
[is roottype "cvs"]
|
||||
[# no point in showing icon when there's only one to choose from]
|
||||
[else]
|
||||
[define view_icon_link]<a
|
||||
href="[entries.view_href]"
|
||||
title="View Directory Listing"><img
|
||||
src="[docroot]/images/list.png"
|
||||
alt="View Directory Listing"
|
||||
class="vc_icon" /></a>[end]
|
||||
[end]
|
||||
[end]
|
||||
|
||||
[is entries.pathtype "dir"]
|
||||
[is roottype "cvs"]
|
||||
[# no point in showing icon when there's only one to choose from]
|
||||
[else]
|
||||
<a href="[entries.view_href]"><img
|
||||
src="[docroot]/images/list.png"
|
||||
alt="View Directory Listing" width="16" height="16" /></a>
|
||||
[end]
|
||||
[end]
|
||||
[is entries.pathtype "file"]
|
||||
[define view_icon_link][if-any entries.view_href]<a
|
||||
href="[entries.view_href]"
|
||||
title="View File"><img
|
||||
src="[docroot]/images/view.png"
|
||||
alt="View File"
|
||||
class="vc_icon" /></a>[end][end]
|
||||
|
||||
[is entries.pathtype "file"]
|
||||
[if-any entries.graph_href]
|
||||
<a href="[entries.graph_href]"
|
||||
[define graph_icon_link][if-any entries.graph_href]<a
|
||||
href="[entries.graph_href]"
|
||||
title="View Revision Graph"><img
|
||||
src="[docroot]/images/cvsgraph_16x16.png"
|
||||
alt="View Revision Graph" width="16" height="16" />
|
||||
</a>
|
||||
[end]
|
||||
src="[docroot]/images/cvsgraph_16x16.png"
|
||||
alt="View Revision Graph"
|
||||
class="vc_icon" /></a>[end][end]
|
||||
|
||||
[if-any entries.view_href]
|
||||
<a href="[entries.view_href]"><img
|
||||
src="[docroot]/images/view.png"
|
||||
alt="View File" width="16" height="16" /></a>
|
||||
[end]
|
||||
[define download_icon_link][if-any entries.download_href]<a
|
||||
href="[entries.download_href]"
|
||||
title="Download File"><img
|
||||
src="[docroot]/images/download.png"
|
||||
alt="Download File"
|
||||
class="vc_icon" /></a>[end][end]
|
||||
|
||||
[if-any entries.download_href]
|
||||
<a href="[entries.download_href]"><img
|
||||
src="[docroot]/images/download.png"
|
||||
alt="Download File" width="16" height="16" /></a>
|
||||
[end]
|
||||
[define annotate_icon_link][if-any entries.annotate_href]<a
|
||||
href="[entries.annotate_href]"
|
||||
title="Annotate File"><img
|
||||
src="[docroot]/images/annotate.png"
|
||||
alt="Annotate File"
|
||||
class="vc_icon" /></a>[end][end]
|
||||
[end]
|
||||
|
||||
<td style="width: 96px"
|
||||
>[# Icon column. We might want to add more icons like a tarball
|
||||
# icon for directories or a diff to previous icon for files.
|
||||
# Make sure this sucker has no whitespace in it, or the fixed
|
||||
# widthness of will suffer for large font sizes
|
||||
][log_icon_link][view_icon_link][graph_icon_link][download_icon_link][annotate_icon_link]</td>
|
||||
|
||||
[if-any entries.annotate_href]
|
||||
<a href="[entries.annotate_href]"><img
|
||||
src="[docroot]/images/annotate.png"
|
||||
alt="Annotate File" width="16" height="16" /></a>
|
||||
[end]
|
||||
[end]
|
||||
</td>
|
||||
<td>
|
||||
[if-any entries.rev]
|
||||
<strong>[if-any entries.revision_href]<a href="[entries.revision_href]">[entries.rev]</a>[else][entries.rev][end]</strong>
|
||||
<strong>[if-any entries.revision_href]<a href="[entries.revision_href]" title="Revision [entries.rev]">[entries.rev]</a>[else][entries.rev][end]</strong>
|
||||
([entries.ago] ago)
|
||||
by <em>[entries.author]</em>:
|
||||
[entries.log]
|
||||
|
||||
@@ -4,50 +4,45 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="vc_header[is sortby "file"]_sort[end]" colspan="2">
|
||||
<a href="[sortby_file_href]#dirlist">File
|
||||
[if-any sortby_file_href]<a href="[sortby_file_href]#dirlist">File</a>[else]File[end]
|
||||
[is sortby "file"]
|
||||
<img class="vc_sortarrow" alt="[is sortdir "down"](rev)[end]"
|
||||
width="13" height="13"
|
||||
src="[docroot]/images/[is sortdir "up"]up[else]down[end].png" />
|
||||
[end]
|
||||
</a>
|
||||
</th>
|
||||
<th class="vc_header[is sortby "rev"]_sort[end]">
|
||||
<a href="[sortby_rev_href]#dirlist">Rev.
|
||||
[if-any sortby_rev_href]<a href="[sortby_rev_href]#dirlist">Rev.</a>[else]Rev.[end]
|
||||
[is sortby "rev"]
|
||||
<img class="vc_sortarrow" alt="[is sortdir "down"](rev)[end]"
|
||||
width="13" height="13"
|
||||
src="[docroot]/images/[is sortdir "up"]up[else]down[end].png" />
|
||||
[end]
|
||||
</a>
|
||||
</th>
|
||||
<th class="vc_header[is sortby "date"]_sort[end]">
|
||||
<a href="[sortby_date_href]#dirlist">Age
|
||||
[if-any sortby_date_href]<a href="[sortby_date_href]#dirlist">Age</a>[else]Age[end]
|
||||
[is sortby "date"]
|
||||
<img class="vc_sortarrow" alt="[is sortdir "down"](rev)[end]"
|
||||
width="13" height="13"
|
||||
src="[docroot]/images/[is sortdir "up"]up[else]down[end].png" />
|
||||
[end]
|
||||
</a>
|
||||
</th>
|
||||
<th class="vc_header[is sortby "author"]_sort[end]">
|
||||
<a href="[sortby_author_href]#dirlist">Author
|
||||
[if-any sortby_author_href]<a href="[sortby_author_href]#dirlist">Author</a>[else]Author[end]
|
||||
[is sortby "author"]
|
||||
<img class="vc_sortarrow" alt="[is sortdir "down"](rev)[end]"
|
||||
width="13" height="13"
|
||||
src="[docroot]/images/[is sortdir "up"]up[else]down[end].png" />
|
||||
[end]
|
||||
</a>
|
||||
</th>
|
||||
[is cfg.options.show_logs "1"]
|
||||
<th class="vc_header[is sortby "log"]_sort[end]">
|
||||
<a href="[sortby_log_href]#dirlist">Last log entry
|
||||
[if-any sortby_log_href]<a href="[sortby_log_href]#dirlist">Last log entry</a>[else]Last log entry[end]
|
||||
[is sortby "log"]
|
||||
<img class="vc_sortarrow" alt="[is sortdir "down"](rev)[end]"
|
||||
width="13" height="13"
|
||||
src="[docroot]/images/[is sortdir "up"]up[else]down[end].png" />
|
||||
[end]
|
||||
</a>
|
||||
</th>
|
||||
[end]
|
||||
</tr>
|
||||
@@ -58,7 +53,7 @@
|
||||
<tr class="vc_row_odd">
|
||||
<td colspan="2">
|
||||
<a href="[up_href]">
|
||||
<img src="[docroot]/images/back_small.png" alt="" width="16" height="16"
|
||||
<img src="[docroot]/images/back_small.png" alt="" class="vc_icon"
|
||||
/> Parent Directory</a>
|
||||
</td>
|
||||
<td> </td>
|
||||
@@ -77,7 +72,7 @@
|
||||
[else]
|
||||
<a name="[entries.anchor]" href="[entries.log_href]" title="View file revision log">
|
||||
[end]
|
||||
<img src="[docroot]/images/[is entries.pathtype "dir"]dir[else][is entries.state "dead"]broken[else]text[end][end].png" alt="" width="16" height="16" />
|
||||
<img src="[docroot]/images/[is entries.pathtype "dir"]dir[else][is entries.state "dead"]broken[else]text[end][end].png" alt="" class="vc_icon" />
|
||||
[entries.name][is entries.pathtype "dir"]/[end]</a>
|
||||
[is entries.state "dead"](dead)[end]
|
||||
</td>
|
||||
@@ -85,7 +80,7 @@
|
||||
<td style="width:1%"><a href="[entries.graph_href]"
|
||||
title="View Revision Graph"><img
|
||||
src="[docroot]/images/cvsgraph_16x16.png"
|
||||
alt="View Revision Graph" width="16" height="16" />
|
||||
alt="View Revision Graph" class="vc_icon" />
|
||||
</a></td>
|
||||
[end]
|
||||
[if-any entries.errors]
|
||||
@@ -96,13 +91,16 @@
|
||||
[is entries.pathtype "dir"]
|
||||
<td> [if-any entries.rev]<a href="[entries.log_href]" title="View directory revision log"><strong>[entries.rev]</strong></a>[end]</td>
|
||||
[else]
|
||||
<td> [if-any entries.rev]<a href="[if-any entries.prefer_markup][entries.view_href][else][entries.download_href][end]" title="[if-any entries.prefer_markup]View[else]Download[end] file contents"><strong>[entries.rev]</strong></a>[end]</td>
|
||||
[define rev_href][if-any entries.prefer_markup][entries.view_href][else][if-any entries.download_href][entries.download_href][end][end][end]
|
||||
<td style="white-space: nowrap;"> [if-any entries.rev][if-any rev_href]<a href="[rev_href]" title="[if-any entries.prefer_markup]View[else]Download[end] file contents">[end]<strong>[entries.rev]</strong>[if-any rev_href]</a>[end][end]
|
||||
[if-any entries.lockinfo]<img src="[docroot]/images/lock.png" alt="locked" class="vc_icon" title="Locked by [entries.lockinfo]" />[end]
|
||||
</td>
|
||||
[end]
|
||||
<td> [entries.ago]</td>
|
||||
<td> [entries.author]</td>
|
||||
[is cfg.options.show_logs "1"]
|
||||
[if-any entries.log]
|
||||
<td> [entries.log][is entries.pathtype "dir"][is roottype "cvs"]
|
||||
[if-any entries.short_log]
|
||||
<td> [entries.short_log][is entries.pathtype "dir"][is roottype "cvs"]
|
||||
<em>(from [entries.log_file]/[entries.log_rev])</em>[end][end]</td>
|
||||
[else]
|
||||
<td> </td>
|
||||
|
||||
@@ -4,31 +4,23 @@
|
||||
<head>
|
||||
<title>ViewVC Help: Directory View</title>
|
||||
<link rel="stylesheet" href="help.css" type="text/css" />
|
||||
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div><a href="http://viewvc.org/index.html"><img src="images/viewvc-logo.png" alt="ViewVC logotype" /></a></div>
|
||||
<table>
|
||||
<col class="menu" />
|
||||
<col />
|
||||
<tr>
|
||||
<td><a href="http://viewvc.org/index.html"><img
|
||||
src="images/logo.png" alt="ViewVC logotype" /></a>
|
||||
</td>
|
||||
<td>
|
||||
<h1>ViewVC Help: Directory View</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td>
|
||||
<h3>Help</h3>
|
||||
<a href="help_rootview.html">General</a><br />
|
||||
<strong>Directory View</strong><br />
|
||||
<a href="help_log.html">Log View</a><br />
|
||||
<a href="help_query.html">Query Database</a><br />
|
||||
</td><td>
|
||||
|
||||
<h3>Internet</h3>
|
||||
<a href="http://viewvc.org/index.html">Home</a><br />
|
||||
<a href="http://viewvc.org/upgrading.html">Upgrading</a><br />
|
||||
<a href="http://viewvc.org/contributing.html">Contributing</a><br />
|
||||
<a href="http://viewvc.org/license-1.html">License</a><br />
|
||||
</td><td colspan="2">
|
||||
<h1>ViewVC Help: Directory View</h1>
|
||||
|
||||
<p>The directory listing view should be a familiar sight to any
|
||||
computer user. It shows the path of the current directory being viewed
|
||||
|
||||
@@ -4,32 +4,24 @@
|
||||
<head>
|
||||
<title>ViewVC Help: Log View</title>
|
||||
<link rel="stylesheet" href="help.css" type="text/css" />
|
||||
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div><a href="http://viewvc.org/index.html"><img src="images/viewvc-logo.png" alt="ViewVC logotype" /></a></div>
|
||||
<table>
|
||||
<col class="menu" />
|
||||
<col />
|
||||
<tr>
|
||||
<td><a href="http://viewvc.org/index.html"><img
|
||||
src="images/logo.png" alt="ViewVC logotype" /></a>
|
||||
</td>
|
||||
<td>
|
||||
<h1>ViewVC Help: Log View</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td>
|
||||
<h3>Help</h3>
|
||||
<a href="help_rootview.html">General</a><br />
|
||||
<a href="help_dirview.html">Directory View</a><br />
|
||||
<strong>Log View</strong><br />
|
||||
<a href="help_query.html">Query Database</a><br />
|
||||
</td><td>
|
||||
|
||||
<h1>ViewVC Help: Log View</h1>
|
||||
|
||||
<h3>Internet</h3>
|
||||
<a href="http://viewvc.org/index.html">Home</a><br />
|
||||
<a href="http://viewvc.org/upgrading.html">Upgrading</a><br />
|
||||
<a href="http://viewvc.org/contributing.html">Contributing</a><br />
|
||||
<a href="http://viewvc.org/license-1.html">License</a><br />
|
||||
</td><td colspan="2">
|
||||
<p>
|
||||
The log view displays the revision history of the selected source
|
||||
file or directory. For each revision the following information is
|
||||
|
||||
@@ -4,64 +4,59 @@
|
||||
<head>
|
||||
<title>ViewVC Help: Query The Commit Database</title>
|
||||
<link rel="stylesheet" href="help.css" type="text/css" />
|
||||
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
<table>
|
||||
<col class="menu" />
|
||||
<col />
|
||||
<tr>
|
||||
<td><a href=".."><img
|
||||
src="images/logo.png" alt="ViewVC logotype" /></a>
|
||||
</td>
|
||||
<td><h1>ViewVC Help: Query The Commit Database</h1></td>
|
||||
</tr>
|
||||
<tr><td>
|
||||
<h3>Other Help:</h3>
|
||||
<a href="help_rootview.html">General</a><br />
|
||||
<a href="help_dirview.html">Directory View</a><br />
|
||||
<a href="help_log.html">Classic Log View</a><br />
|
||||
<a href="help_logtable.html">Alternative Log View</a><br />
|
||||
<strong>Query Database</strong>
|
||||
|
||||
<h3>Internet</h3>
|
||||
<a href="http://viewvc.org/index.html">Home</a><br />
|
||||
<a href="http://viewvc.org/upgrading.html">Upgrading</a><br />
|
||||
<a href="http://viewvc.org/contributing.html">Contributing</a><br />
|
||||
<a href="http://viewvc.org/license-1.html">License</a><br />
|
||||
</td><td colspan="2">
|
||||
|
||||
<p>
|
||||
Select your parameters for querying the CVS commit database in the
|
||||
form at the top of the page. You
|
||||
can search for multiple matches by typing a comma-seperated list
|
||||
into the text fields. Regular expressions, and wildcards are also
|
||||
supported. Blank text input fields are treated as wildcards.
|
||||
</p>
|
||||
<p>
|
||||
Any of the text entry fields can take a comma-seperated list of
|
||||
search arguments. For example, to search for all commits from
|
||||
authors <em>jpaint</em> and <em>gstein</em>, just type: <code>jpaint,
|
||||
gstein</code> in the <em>Author</em> input box. If you are searching
|
||||
for items containing spaces or quotes, you will need to quote your
|
||||
request. For example, the same search above with quotes is:
|
||||
<code>"jpaint", "gstein"</code>.
|
||||
</p>
|
||||
<p>
|
||||
Wildcard and regular expression searches are entered in a similar
|
||||
way to the quoted requests. You must quote any wildcard or
|
||||
regular expression request, and a command character preceeds the
|
||||
first quote. The command character <code>l</code>(lowercase L) is for wildcard
|
||||
searches, and the wildcard character is a percent (<code>%</code>). The
|
||||
command character for regular expressions is <code>r</code>, and is
|
||||
passed directly to MySQL, so you'll need to refer to the MySQL
|
||||
manual for the exact regex syntax. It is very similar to Perl. A
|
||||
wildard search for all files with a <em>.py</em> extention is:
|
||||
<code>l"%.py"</code> in the <em>File</em> input box. The same search done
|
||||
with a regular expression is: <code>r".*\.py"</code>.
|
||||
</p>
|
||||
<p>
|
||||
All search types can be mixed, as long as they are seperated by
|
||||
commas.
|
||||
</p>
|
||||
</td></tr></table>
|
||||
</body></html>
|
||||
<div><a href="http://viewvc.org/index.html"><img src="images/viewvc-logo.png" alt="ViewVC logotype" /></a></div>
|
||||
<table>
|
||||
<col class="menu" />
|
||||
<col />
|
||||
<tr><td>
|
||||
<h3>Help:</h3>
|
||||
<a href="help_rootview.html">General</a><br />
|
||||
<a href="help_dirview.html">Directory View</a><br />
|
||||
<a href="help_log.html">Log View</a><br />
|
||||
<strong>Query Database</strong>
|
||||
</td><td>
|
||||
|
||||
<h1>ViewVC Help: Query The Commit Database</h1>
|
||||
|
||||
<p>
|
||||
Select your parameters for querying the CVS commit database in the
|
||||
form at the top of the page. You
|
||||
can search for multiple matches by typing a comma-seperated list
|
||||
into the text fields. Regular expressions, and wildcards are also
|
||||
supported. Blank text input fields are treated as wildcards.
|
||||
</p>
|
||||
<p>
|
||||
Any of the text entry fields can take a comma-seperated list of
|
||||
search arguments. For example, to search for all commits from
|
||||
authors <em>jpaint</em> and <em>gstein</em>, just type: <code>jpaint,
|
||||
gstein</code> in the <em>Author</em> input box. If you are searching
|
||||
for items containing spaces or quotes, you will need to quote your
|
||||
request. For example, the same search above with quotes is:
|
||||
<code>"jpaint", "gstein"</code>.
|
||||
</p>
|
||||
<p>
|
||||
Wildcard and regular expression searches are entered in a similar
|
||||
way to the quoted requests. You must quote any wildcard or
|
||||
regular expression request, and a command character preceeds the
|
||||
first quote. The command character <code>l</code>(lowercase L) is for wildcard
|
||||
searches, and the wildcard character is a percent (<code>%</code>). The
|
||||
command character for regular expressions is <code>r</code>, and is
|
||||
passed directly to MySQL, so you'll need to refer to the MySQL
|
||||
manual for the exact regex syntax. It is very similar to Perl. A
|
||||
wildard search for all files with a <em>.py</em> extention is:
|
||||
<code>l"%.py"</code> in the <em>File</em> input box. The same search done
|
||||
with a regular expression is: <code>r".*\.py"</code>.
|
||||
</p>
|
||||
<p>
|
||||
All search types can be mixed, as long as they are seperated by
|
||||
commas.
|
||||
</p>
|
||||
</td></tr></table>
|
||||
<hr />
|
||||
<address><a href="mailto:users@viewvc.tigris.org">ViewVC Users Mailinglist</a></address>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,164 +4,152 @@
|
||||
<head>
|
||||
<title>ViewVC Help: General</title>
|
||||
<link rel="stylesheet" href="help.css" type="text/css" />
|
||||
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div><a href="http://viewvc.org/index.html"><img src="images/viewvc-logo.png" alt="ViewVC logotype" /></a></div>
|
||||
<table>
|
||||
<col class="menu" />
|
||||
<col />
|
||||
<tr>
|
||||
<td><a href=".."><img
|
||||
src="images/logo.png" alt="ViewVC logotype" /></a>
|
||||
</td>
|
||||
<td>
|
||||
<h1>ViewVC Help: General</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td>
|
||||
<h3>Help</h3>
|
||||
<strong>General</strong><br />
|
||||
<a href="help_dirview.html">Directory View</a><br />
|
||||
<a href="help_log.html">Log View</a><br />
|
||||
<a href="help_query.html">Query Database</a><br />
|
||||
</td><td>
|
||||
|
||||
<h3>Internet</h3>
|
||||
<a href="http://viewvc.org/index.html">Home</a><br />
|
||||
<a href="http://viewvc.org/upgrading.html">Upgrading</a><br />
|
||||
<a href="http://viewvc.org/contributing.html">Contributing</a><br />
|
||||
<a href="http://viewvc.org/license-1.html">License</a><br />
|
||||
</td><td colspan="2">
|
||||
|
||||
<p><em>ViewVC</em> is a WWW interface for CVS and Subversion
|
||||
repositories. It allows you to browse the files and directories in a
|
||||
repository while showing you metadata from the repository history: log
|
||||
messages, modification dates, author names, revision numbers, copy
|
||||
history, and so on. It provides several different views of repository
|
||||
data to help you find the information you are looking for:</p>
|
||||
|
||||
<ul>
|
||||
<li><a name="view-dir" href="help_dirview.html"><strong>Directory
|
||||
View</strong></a> - Shows a list of files and subdirectories in a
|
||||
directory of the repository, along with metadata like author names and
|
||||
log entries.</li>
|
||||
|
||||
<li><a name="view-log" href="help_log.html"><strong>Log
|
||||
View</strong></a> - Shows a revision by revision list of all the
|
||||
changes that have made to a file or directory in the repository, with
|
||||
metadata and links to views of each revision.</li>
|
||||
|
||||
<li><a name="view-markup"><strong>File Contents View (Markup
|
||||
View)</strong></a> - Shows the contents of a file at a particular
|
||||
revision, with revision information at the top of the page. File
|
||||
revisions which are GIF, PNG, or JPEG images are displayed inline on
|
||||
the page. Other file types are displayed as marked up text. The markup
|
||||
may be limited to turning URLs and email addresses into links, or
|
||||
configured to show colorized source code.</li>
|
||||
|
||||
<li><a name="view-checkout"><strong>File Download (Checkout
|
||||
View)</strong></a> - Retrieves the unaltered contents of a file
|
||||
revision. Browsers may try to display the file, or just save it to
|
||||
disk.</li>
|
||||
|
||||
<li><a name="view-annotate"><strong>File Annotate View</strong></a> -
|
||||
Shows the contents of a file revision and breaks it down line by line,
|
||||
showing the revision number where each one was last modified, along
|
||||
with links and other information. <em>This view is disabled in some
|
||||
ViewVC configurations</em></li>
|
||||
|
||||
<li><a name="view-diff"><strong>File Diff View</strong></a> - Shows
|
||||
the changes made between two revisions of a file</li>
|
||||
|
||||
<li><a name="view-tarball"><strong>Directory Tarball View</strong> -
|
||||
Retrieves a gzipped tar archive containing the contents of a
|
||||
directory.<em>This view is disabled in the default ViewVC
|
||||
configuration.</em></li>
|
||||
|
||||
<li><a name="view-query"><strong>Directory Query View</strong></a> -
|
||||
Shows information about changes made to all subdirectories and files
|
||||
under a parent directory, sorted and filtered by criteria you specify.
|
||||
<em>This view is disabled in the default ViewVC configuration.</em>
|
||||
</li>
|
||||
|
||||
<li><a name="view-rev"><strong>Revision View</strong> - Shows
|
||||
information about a revision including log message, author, and a list
|
||||
of changed paths. <em>For Subversion repositories only.</em></li>
|
||||
|
||||
<li><a name="view-graph"><strong>Graph View</strong></a> - Shows a
|
||||
graphical representation of a file's revisions and branches complete
|
||||
with tag and author names and links to markup and diff pages.
|
||||
<em>For CVS repositories only, and disabled in the default
|
||||
configuration.</em></li>
|
||||
</ul>
|
||||
|
||||
<h3><a name="multiple-repositories">Multiple Repositories</a></h3>
|
||||
|
||||
<p>A single installation of ViewVC is often used to provide access to
|
||||
more than one repository. In these installations, ViewVC shows a
|
||||
<em>Project Root</em> drop down box in the top right corner of every
|
||||
generated page to allow for quick access to any repository.</p>
|
||||
|
||||
<h3><a name="sticky-revision-tag">Sticky Revision and Tag</a></h3>
|
||||
|
||||
<p>By default, ViewVC will show the files and directories and revisions
|
||||
that currently exist in the repository. But it's also possible to browse
|
||||
the contents of a repository at a point in its past history by choosing
|
||||
a "sticky tag" (in CVS) or a "sticky revision" (in Subversion) from the
|
||||
forms at the top of directory and log pages. They're called sticky
|
||||
because once they're chosen, they stick around when you navigate to
|
||||
other pages, until you reset them. When they're set, directory and log
|
||||
pages only show revisions preceding the specified point in history. In
|
||||
CVS, when a tag refers to a branch or a revision on a branch, only
|
||||
revisions from the branch history are shown, including branch points and
|
||||
their preceding revisions.</p>
|
||||
|
||||
<h3><a name="dead-files">Dead Files</a></h3>
|
||||
|
||||
<p>In CVS directory listings, ViewVC can optionally display dead files.
|
||||
Dead files are files which used to be in a directory but are currently
|
||||
deleted, or files which just don't exist in the currently selected
|
||||
<a href="#sticky-revision-tag">sticky tag</a>. Dead files cannot be
|
||||
shown in Subversion repositories. The only way to see a deleted file in
|
||||
a Subversion directory is to navigate to a sticky revision where the
|
||||
file previously existed.</p>
|
||||
|
||||
<h3><a name="artificial-tags">Artificial Tags</a></h3>
|
||||
|
||||
<p>In CVS Repositories, ViewVC adds artificial tags <em>HEAD</em> and
|
||||
<em>MAIN</em> to tag listings and accepts them in place of revision
|
||||
numbers and real tag names in all URLs. <em>MAIN</em> acts like a branch
|
||||
tag pointing at the default branch, while <em>HEAD</em> acts like a
|
||||
revision tag pointing to the latest revision on the default branch. The
|
||||
default branch is usually just the trunk, but may be set to other
|
||||
branches inside individual repository files. CVS will always check out
|
||||
revisions from a file's default branch when no other branch is specified
|
||||
on the command line.</p>
|
||||
|
||||
<h3><a name="more-information">More Information</a></h3>
|
||||
|
||||
<p>More information about <em>ViewVC</em> is available from
|
||||
<a href="http://viewvc.org/">viewvc.org</a>.
|
||||
See the links below for guides to CVS and Subversion</p>
|
||||
|
||||
<h4>Documentation about CVS</h4>
|
||||
<blockquote>
|
||||
<p>
|
||||
<a href="http://cvsbook.red-bean.com/"><em>Open Source
|
||||
Development with CVS</em></a><br />
|
||||
<a href="http://www.loria.fr/~molli/cvs/doc/cvs_toc.html">CVS
|
||||
User's Guide</a><br />
|
||||
<a href="http://cellworks.washington.edu/pub/docs/cvs/tutorial/cvs_tutorial_1.html">Another CVS tutorial</a><br />
|
||||
<a href="http://www.csc.calpoly.edu/~dbutler/tutorials/winter96/cvs/">Yet another CVS tutorial (a little old, but nice)</a><br />
|
||||
<a href="http://www.cs.utah.edu/dept/old/texinfo/cvs/FAQ.txt">An old but very useful FAQ about CVS</a>
|
||||
</p>
|
||||
</blockquote>
|
||||
|
||||
<h4>Documentation about Subversion</h3>
|
||||
<blockquote>
|
||||
<p>
|
||||
<a href="http://svnbook.red-bean.com/"><em>Version Control with
|
||||
Subversion</em></a><br />
|
||||
</p>
|
||||
</blockquote>
|
||||
<h1>ViewVC Help: General</h1>
|
||||
|
||||
<p><em>ViewVC</em> is a WWW interface for CVS and Subversion
|
||||
repositories. It allows you to browse the files and directories in a
|
||||
repository while showing you metadata from the repository history: log
|
||||
messages, modification dates, author names, revision numbers, copy
|
||||
history, and so on. It provides several different views of repository
|
||||
data to help you find the information you are looking for:</p>
|
||||
|
||||
<ul>
|
||||
<li><a name="multiple-repositories"><strong>Root Listing
|
||||
View</strong></a> - Show a list of repositories configured for
|
||||
display in ViewVC.</li>
|
||||
|
||||
<li><a name="view-dir" href="help_dirview.html"><strong>Directory
|
||||
View</strong></a> - Shows a list of files and subdirectories in a
|
||||
directory of the repository, along with metadata like author names and
|
||||
log entries.</li>
|
||||
|
||||
<li><a name="view-log" href="help_log.html"><strong>Log
|
||||
View</strong></a> - Shows a revision by revision list of all the
|
||||
changes that have made to a file or directory in the repository, with
|
||||
metadata and links to views of each revision.</li>
|
||||
|
||||
<li><a name="view-checkout"><strong>File Download (Checkout
|
||||
View)</strong></a> - Retrieves the unaltered contents of a file
|
||||
revision. Browsers may try to display the file, or just save it
|
||||
to disk. <em>This view is disabled in the default ViewVC
|
||||
configuration.</em></li>
|
||||
|
||||
<li><a name="view-annotate"><a name="view-markup"><strong>File
|
||||
Contents View</strong></a></a> - Shows the contents of a file at
|
||||
a particular revision, with revision information at the top of
|
||||
the page. File revisions which are GIF, PNG, or JPEG images are
|
||||
displayed inline on the page. Other file types are displayed as
|
||||
marked up text. The markup may be limited to turning URLs and
|
||||
email addresses into links, or configured to show colorized
|
||||
source code. This view can optionally show line-based
|
||||
annotation data for the file, containing the revision number
|
||||
where each line was last modified, along with links and other
|
||||
information. <em>This view is disabled in some ViewVC
|
||||
configurations.</em></li>
|
||||
|
||||
<li><a name="view-diff"><strong>File Diff View</strong></a> - Shows
|
||||
the changes made between two revisions of a file</li>
|
||||
|
||||
<li><a name="view-tarball"><strong>Directory Tarball View</strong> -
|
||||
Retrieves a gzipped tar archive containing the contents of a
|
||||
directory.<em>This view is disabled in the default ViewVC
|
||||
configuration.</em></li>
|
||||
|
||||
<li><a name="view-query"><strong>Directory Query View</strong></a> -
|
||||
Shows information about changes made to all subdirectories and files
|
||||
under a parent directory, sorted and filtered by criteria you specify.
|
||||
<em>This view is disabled in the default ViewVC configuration.</em>
|
||||
</li>
|
||||
|
||||
<li><a name="view-rev"><strong>Revision View</strong> - Shows
|
||||
information about a revision including log message, author, and a list
|
||||
of changed paths. <em>For Subversion repositories only.</em></li>
|
||||
|
||||
<li><a name="view-graph"><strong>Graph View</strong></a> - Shows a
|
||||
graphical representation of a file's revisions and branches complete
|
||||
with tag and author names and links to markup and diff pages.
|
||||
<em>For CVS repositories only, and disabled in the default
|
||||
configuration.</em></li>
|
||||
</ul>
|
||||
|
||||
<h3><a name="sticky-revision-tag">Sticky Revision and Tag</a></h3>
|
||||
|
||||
<p>By default, ViewVC will show the files and directories and revisions
|
||||
that currently exist in the repository. But it's also possible to browse
|
||||
the contents of a repository at a point in its past history by choosing
|
||||
a "sticky tag" (in CVS) or a "sticky revision" (in Subversion) from the
|
||||
forms at the top of directory and log pages. They're called sticky
|
||||
because once they're chosen, they stick around when you navigate to
|
||||
other pages, until you reset them. When they're set, directory and log
|
||||
pages only show revisions preceding the specified point in history. In
|
||||
CVS, when a tag refers to a branch or a revision on a branch, only
|
||||
revisions from the branch history are shown, including branch points and
|
||||
their preceding revisions.</p>
|
||||
|
||||
<h3><a name="dead-files">Dead Files</a></h3>
|
||||
|
||||
<p>In CVS directory listings, ViewVC can optionally display dead files.
|
||||
Dead files are files which used to be in a directory but are currently
|
||||
deleted, or files which just don't exist in the currently selected
|
||||
<a href="#sticky-revision-tag">sticky tag</a>. Dead files cannot be
|
||||
shown in Subversion repositories. The only way to see a deleted file in
|
||||
a Subversion directory is to navigate to a sticky revision where the
|
||||
file previously existed.</p>
|
||||
|
||||
<h3><a name="artificial-tags">Artificial Tags</a></h3>
|
||||
|
||||
<p>In CVS Repositories, ViewVC adds artificial tags <em>HEAD</em> and
|
||||
<em>MAIN</em> to tag listings and accepts them in place of revision
|
||||
numbers and real tag names in all URLs. <em>MAIN</em> acts like a branch
|
||||
tag pointing at the default branch, while <em>HEAD</em> acts like a
|
||||
revision tag pointing to the latest revision on the default branch. The
|
||||
default branch is usually just the trunk, but may be set to other
|
||||
branches inside individual repository files. CVS will always check out
|
||||
revisions from a file's default branch when no other branch is specified
|
||||
on the command line.</p>
|
||||
|
||||
<h3><a name="more-information">More Information</a></h3>
|
||||
|
||||
<p>More information about <em>ViewVC</em> is available from
|
||||
<a href="http://viewvc.org/">viewvc.org</a>.
|
||||
See the links below for guides to CVS and Subversion</p>
|
||||
|
||||
<h4>Documentation about CVS</h4>
|
||||
<blockquote>
|
||||
<p>
|
||||
<a href="http://cvsbook.red-bean.com/"><em>Open Source
|
||||
Development with CVS</em></a><br />
|
||||
<a href="http://www.loria.fr/~molli/cvs/doc/cvs_toc.html">CVS
|
||||
User's Guide</a><br />
|
||||
<a href="http://cellworks.washington.edu/pub/docs/cvs/tutorial/cvs_tutorial_1.html">Another CVS tutorial</a><br />
|
||||
<a href="http://www.csc.calpoly.edu/~dbutler/tutorials/winter96/cvs/">Yet another CVS tutorial (a little old, but nice)</a><br />
|
||||
<a href="http://www.cs.utah.edu/dept/old/texinfo/cvs/FAQ.txt">An old but very useful FAQ about CVS</a>
|
||||
</p>
|
||||
</blockquote>
|
||||
|
||||
<h4>Documentation about Subversion</h3>
|
||||
<blockquote>
|
||||
<p>
|
||||
<a href="http://svnbook.red-bean.com/"><em>Version Control with
|
||||
Subversion</em></a><br />
|
||||
</p>
|
||||
</blockquote>
|
||||
</td></tr></table>
|
||||
<hr />
|
||||
<address><a href="mailto:users@viewvc.tigris.org">ViewVC Users Mailinglist</a></address>
|
||||
|
||||
BIN
templates/docroot/images/favicon.ico
Normal file
BIN
templates/docroot/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
templates/docroot/images/lock.png
Normal file
BIN
templates/docroot/images/lock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 303 B |
Binary file not shown.
|
Before Width: | Height: | Size: 5.9 KiB |
BIN
templates/docroot/images/viewvc-logo.png
Normal file
BIN
templates/docroot/images/viewvc-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
@@ -6,6 +6,7 @@
|
||||
html, body {
|
||||
color: #000000;
|
||||
background-color: #ffffff;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
a:link { color: #0000ff; }
|
||||
@@ -21,12 +22,36 @@ table {
|
||||
table.auto {
|
||||
width: auto;
|
||||
}
|
||||
table.fixed {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
table.fixed td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
tr, td, th { vertical-align: top; }
|
||||
th { white-space: nowrap; }
|
||||
form { margin: 0; }
|
||||
|
||||
/** Navigation Headers ***/
|
||||
|
||||
/*** Icons ***/
|
||||
.vc_icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
|
||||
/*** Navigation Headers ***/
|
||||
.vc_navheader {
|
||||
background-color: #cccccc;
|
||||
padding: .25em;
|
||||
}
|
||||
.vc_navheader .pathdiv {
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +72,10 @@ form { margin: 0; }
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.vc_row_odd {
|
||||
background-color: #ccccee;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.vc_row_special {
|
||||
background-color: #ffff7f;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,44 +90,102 @@ form { margin: 0; }
|
||||
}
|
||||
|
||||
|
||||
/*** Markup Summary Header ***/
|
||||
/*** Properties Listing ***/
|
||||
.vc_properties {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
|
||||
/*** File Content Markup Styles ***/
|
||||
.vc_summary {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
/*** Highlight Markup Styles ***/
|
||||
#vc_markup .num { color: #000000; }
|
||||
#vc_markup .esc { color: #bd8d8b; }
|
||||
#vc_markup .str { color: #bd8d8b; }
|
||||
#vc_markup .dstr { color: #bd8d8b; }
|
||||
#vc_markup .slc { color: #ac2020; font-style: italic; }
|
||||
#vc_markup .com { color: #ac2020; font-style: italic; }
|
||||
#vc_markup .dir { color: #000000; }
|
||||
#vc_markup .sym { color: #000000; }
|
||||
#vc_markup .line { color: #555555; }
|
||||
#vc_markup .kwa { color: #9c20ee; font-weight: bold; }
|
||||
#vc_markup .kwb { color: #208920; }
|
||||
#vc_markup .kwc { color: #0000ff; }
|
||||
#vc_markup .kwd { color: #404040; }
|
||||
|
||||
/*** Py2html Markup Styles ***/
|
||||
#vc_markup .PY_STRING { color: #bd8d8b; }
|
||||
#vc_markup .PY_COMMENT { color: #ac2020; font-style: italic; }
|
||||
#vc_markup .PY_KEYWORD { color: #9c20ee; font-weight: bold; }
|
||||
#vc_markup .PY_IDENTIFIER { color: #404040; }
|
||||
|
||||
/*** Line numbers outputted by highlight colorizer ***/
|
||||
.line {
|
||||
border-right-width: 1px;
|
||||
#vc_file td {
|
||||
border-right-style: solid;
|
||||
border-right-color: #505050;
|
||||
padding: 1px;
|
||||
background-color: #eeeeee;
|
||||
color: #505050;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
padding: 1px 5px;
|
||||
}
|
||||
.vc_file_line_number {
|
||||
border-right-width: 1px;
|
||||
background-color: #eeeeee;
|
||||
color: #505050;
|
||||
text-align: right;
|
||||
}
|
||||
.vc_file_line_author, .vc_file_line_rev {
|
||||
border-right-width: 1px;
|
||||
text-align: right;
|
||||
}
|
||||
.vc_file_line_text {
|
||||
border-right-width: 0px;
|
||||
background-color: white;
|
||||
font-family: monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
width: 100%;
|
||||
}
|
||||
.pygments-c { color: #408080; font-style: italic } /* Comment */
|
||||
.pygments-err { border: 1px solid #FF0000 } /* Error */
|
||||
.pygments-k { color: #008000; font-weight: bold } /* Keyword */
|
||||
.pygments-o { color: #666666 } /* Operator */
|
||||
.pygments-cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
||||
.pygments-cp { color: #BC7A00 } /* Comment.Preproc */
|
||||
.pygments-c1 { color: #408080; font-style: italic } /* Comment.Single */
|
||||
.pygments-cs { color: #408080; font-style: italic } /* Comment.Special */
|
||||
.pygments-gd { color: #A00000 } /* Generic.Deleted */
|
||||
.pygments-ge { font-style: italic } /* Generic.Emph */
|
||||
.pygments-gr { color: #FF0000 } /* Generic.Error */
|
||||
.pygments-gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.pygments-gi { color: #00A000 } /* Generic.Inserted */
|
||||
.pygments-go { color: #808080 } /* Generic.Output */
|
||||
.pygments-gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||
.pygments-gs { font-weight: bold } /* Generic.Strong */
|
||||
.pygments-gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.pygments-gt { color: #0040D0 } /* Generic.Traceback */
|
||||
.pygments-kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
||||
.pygments-kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
||||
.pygments-kp { color: #008000 } /* Keyword.Pseudo */
|
||||
.pygments-kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
|
||||
.pygments-kt { color: #B00040 } /* Keyword.Type */
|
||||
.pygments-m { color: #666666 } /* Literal.Number */
|
||||
.pygments-s { color: #BA2121 } /* Literal.String */
|
||||
.pygments-na { color: #7D9029 } /* Name.Attribute */
|
||||
.pygments-nb { color: #008000 } /* Name.Builtin */
|
||||
.pygments-nc { color: #0000FF; font-weight: bold } /* Name.Class */
|
||||
.pygments-no { color: #880000 } /* Name.Constant */
|
||||
.pygments-nd { color: #AA22FF } /* Name.Decorator */
|
||||
.pygments-ni { color: #999999; font-weight: bold } /* Name.Entity */
|
||||
.pygments-ne { color: #D2413A; font-weight: bold } /* Name.Exception */
|
||||
.pygments-nf { color: #0000FF } /* Name.Function */
|
||||
.pygments-nl { color: #A0A000 } /* Name.Label */
|
||||
.pygments-nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
||||
.pygments-nt { color: #008000; font-weight: bold } /* Name.Tag */
|
||||
.pygments-nv { color: #19177C } /* Name.Variable */
|
||||
.pygments-ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
||||
.pygments-w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.pygments-mf { color: #666666 } /* Literal.Number.Float */
|
||||
.pygments-mh { color: #666666 } /* Literal.Number.Hex */
|
||||
.pygments-mi { color: #666666 } /* Literal.Number.Integer */
|
||||
.pygments-mo { color: #666666 } /* Literal.Number.Oct */
|
||||
.pygments-sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||
.pygments-sc { color: #BA2121 } /* Literal.String.Char */
|
||||
.pygments-sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
|
||||
.pygments-s2 { color: #BA2121 } /* Literal.String.Double */
|
||||
.pygments-se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
|
||||
.pygments-sh { color: #BA2121 } /* Literal.String.Heredoc */
|
||||
.pygments-si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
|
||||
.pygments-sx { color: #008000 } /* Literal.String.Other */
|
||||
.pygments-sr { color: #BB6688 } /* Literal.String.Regex */
|
||||
.pygments-s1 { color: #BA2121 } /* Literal.String.Single */
|
||||
.pygments-ss { color: #19177C } /* Literal.String.Symbol */
|
||||
.pygments-bp { color: #008000 } /* Name.Builtin.Pseudo */
|
||||
.pygments-vc { color: #19177C } /* Name.Variable.Class */
|
||||
.pygments-vg { color: #19177C } /* Name.Variable.Global */
|
||||
.pygments-vi { color: #19177C } /* Name.Variable.Instance */
|
||||
.pygments-il { color: #666666 } /* Literal.Number.Integer.Long */
|
||||
|
||||
|
||||
/*** Diff Styles ***/
|
||||
.vc_diff_header {
|
||||
@@ -140,13 +226,15 @@ form { margin: 0; }
|
||||
font-family: sans-serif;
|
||||
font-size: smaller;
|
||||
}
|
||||
.vc_diff_line_number {
|
||||
}
|
||||
.vc_raw_diff {
|
||||
background-color: #cccccc;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
/*** Intraline Diff Styles ***/
|
||||
|
||||
/*** Intraline Diff Styles ***/
|
||||
.vc_idiff_add {
|
||||
background-color: #aaffaa;
|
||||
}
|
||||
@@ -159,7 +247,6 @@ form { margin: 0; }
|
||||
.vc_idiff_empty {
|
||||
background-color:#e0e0e0;
|
||||
}
|
||||
|
||||
table.vc_idiff col.content {
|
||||
width: 50%;
|
||||
}
|
||||
@@ -177,23 +264,19 @@ table.vc_idiff tbody th {
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
/*** Annotate Styles ***/
|
||||
.vc_blame_line, .vc_blame_author, .vc_blame_rev {
|
||||
font-family: monospace;
|
||||
font-size: smaller;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
padding-right: 1em;
|
||||
}
|
||||
.vc_blame_text {
|
||||
font-family: monospace;
|
||||
font-size: smaller;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*** Query Form ***/
|
||||
.vc_query_form {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
|
||||
/*** Warning! ***/
|
||||
.vc_warning {
|
||||
border-width: 1px 2px 2px 2px;
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
background-color: red;
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
123
templates/file.ezt
Normal file
123
templates/file.ezt
Normal file
@@ -0,0 +1,123 @@
|
||||
[# ------------------------------------------------------------------------- ]
|
||||
[# CUSTOMIZE ME: To avoid displaying "binary garbage" -- the contents of ]
|
||||
[# files with non-human-readable file formats -- change the value of the ]
|
||||
[# hide_binary_garbage variable below to 1. ]
|
||||
[# ------------------------------------------------------------------------- ]
|
||||
|
||||
[define hide_binary_garbage]0[end]
|
||||
|
||||
[# ------------------------------------------------------------------------- ]
|
||||
|
||||
[# setup page definitions]
|
||||
[define page_title]Contents of /[where][end]
|
||||
[define help_href][docroot]/help_rootview.html[end]
|
||||
[# end]
|
||||
|
||||
[include "include/header.ezt" "markup"]
|
||||
[include "include/file_header.ezt"]
|
||||
<hr />
|
||||
<div class="vc_summary">
|
||||
Revision [if-any revision_href]<a href="[revision_href]"><strong>[rev]</strong></a>[else]<strong>[rev]</strong>[end] -
|
||||
([is annotation "annotated"]<a href="[view_href]"><strong>hide annotations</strong></a>[else]<a href="[annotate_href]"><strong>show annotations</strong></a>[end])
|
||||
[if-any download_href](<a href="[download_href]"><strong>download</strong></a>)[end]
|
||||
[if-any download_text_href](<a href="[download_text_href]"><strong>as text</strong></a>)[end]
|
||||
|
||||
[if-any vendor_branch] <em>(vendor branch)</em>[end]
|
||||
<br /><em>[if-any date][date][else](unknown date)[end]</em>
|
||||
[if-any ago]([ago] ago)[end]
|
||||
by <em>[if-any author][author][else](unknown author)[end]</em>
|
||||
[if-any orig_path]
|
||||
<br />Original Path: <a href="[orig_href]"><em>[orig_path]</em></a>
|
||||
[end]
|
||||
|
||||
[if-any branches]
|
||||
<br />Branch: <strong>[branches]</strong>
|
||||
[end]
|
||||
[if-any tags]
|
||||
<br />CVS Tags: <strong>[tags]</strong>
|
||||
[end]
|
||||
[if-any branch_points]
|
||||
<br />Branch point for: <strong>[branch_points]</strong>
|
||||
[end]
|
||||
[is roottype "cvs"]
|
||||
[if-any changed]
|
||||
<br />Changes since <strong>[prev]: [changed] lines</strong>
|
||||
[end]
|
||||
[end]
|
||||
[if-any mime_type]
|
||||
<br />File MIME type: [mime_type]
|
||||
[end]
|
||||
[is roottype "svn"][if-any size]
|
||||
<br />File size: [size] byte(s)
|
||||
[end][end]
|
||||
[if-any lockinfo]
|
||||
<br />Lock status: <img src="[docroot]/images/lock.png" alt="Locked" width="16" height="16" /> [lockinfo]
|
||||
[end]
|
||||
[is annotation "binary"]
|
||||
<br /><strong>Unable to calculate annotation data on binary file contents.</strong>
|
||||
[end]
|
||||
[is annotation "error"]
|
||||
<br /><strong>Error occurred while calculating annotation data.</strong>
|
||||
[end]
|
||||
[is state "dead"]
|
||||
<br /><strong><em>FILE REMOVED</em></strong>
|
||||
[end]
|
||||
[if-any log]
|
||||
<pre class="vc_log">[log]</pre>
|
||||
[end]
|
||||
</div>
|
||||
|
||||
[if-any prefer_markup][define hide_binary_garbage]0[end][end]
|
||||
[if-any image_src_href][define hide_binary_garbage]0[end][end]
|
||||
|
||||
[is hide_binary_garbage "1"]
|
||||
<p><strong>This file's contents are not viewable.
|
||||
[if-any download_href]Please <a href="[download_href]">download</a>
|
||||
this version of the file in order to view it.[end]</strong></p>
|
||||
[else]
|
||||
|
||||
[define last_rev]0[end]
|
||||
[define rowclass]vc_row_even[end]
|
||||
|
||||
[if-any lines]
|
||||
|
||||
<div id="vc_file">
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
[for lines]
|
||||
[is lines.rev last_rev]
|
||||
[else]
|
||||
[is lines.rev rev]
|
||||
[define rowclass]vc_row_special[end]
|
||||
[else]
|
||||
[is rowclass "vc_row_even"]
|
||||
[define rowclass]vc_row_odd[end]
|
||||
[else]
|
||||
[define rowclass]vc_row_even[end]
|
||||
[end]
|
||||
[end]
|
||||
[end]
|
||||
|
||||
<tr class="[rowclass]" id="l[lines.line_number]">
|
||||
<td class="vc_file_line_number">[lines.line_number]</td>
|
||||
[is annotation "annotated"]
|
||||
<td class="vc_file_line_author">[is lines.rev last_rev] [else][lines.author][end]</td>
|
||||
<td class="vc_file_line_rev">[is lines.rev last_rev] [else][if-any lines.diff_href]<a href="[lines.diff_href]">[end][lines.rev][if-any lines.diff_href]</a>[end][end]</td>
|
||||
[end]
|
||||
<td class="vc_file_line_text">[lines.text]</td>
|
||||
</tr>
|
||||
[define last_rev][lines.rev][end]
|
||||
[end]
|
||||
</table>
|
||||
</div>
|
||||
|
||||
[else]
|
||||
[if-any image_src_href]
|
||||
<div id="vc_file_image">
|
||||
<img src="[image_src_href]" alt="" />
|
||||
</div>
|
||||
[end]
|
||||
[end]
|
||||
[end]
|
||||
|
||||
[include "include/props.ezt"]
|
||||
[include "include/footer.ezt"]
|
||||
@@ -15,7 +15,7 @@
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
[diff_select_hidden_values]
|
||||
[for diff_select_hidden_values]<input type="hidden" name="[diff_select_hidden_values.name]" value="[diff_select_hidden_values.value]"/>[end]
|
||||
Diffs between
|
||||
[if-any tags]
|
||||
<select name="r1">
|
||||
@@ -55,6 +55,7 @@
|
||||
<select name="diff_format" onchange="submit()">
|
||||
<option value="h" [is diff_format "h"]selected="selected"[end]>Colored Diff</option>
|
||||
<option value="l" [is diff_format "l"]selected="selected"[end]>Long Colored Diff</option>
|
||||
<option value="f" [is diff_format "f"]selected="selected"[end]>Full Colored Diff</option>
|
||||
<option value="u" [is diff_format "u"]selected="selected"[end]>Unidiff</option>
|
||||
<option value="c" [is diff_format "c"]selected="selected"[end]>Context Diff</option>
|
||||
<option value="s" [is diff_format "s"]selected="selected"[end]>Side by Side</option>
|
||||
|
||||
@@ -1,38 +1,9 @@
|
||||
[if-any search_re_form]
|
||||
<hr />
|
||||
[# this table holds the selectors on the left, and reset on the right ]
|
||||
<table class="auto">
|
||||
<tr>
|
||||
<td>Show files containing the regular expression:</td>
|
||||
<td>
|
||||
<form method="get" action="[search_re_action]">
|
||||
<div>
|
||||
[search_re_hidden_values]
|
||||
<input type="text" name="search" value="[search_re]" />
|
||||
<input type="submit" value="Show" />
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
[if-any search_re]
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
<form method="get" action="[search_tag_action]">
|
||||
<div>
|
||||
[search_tag_hidden_values]
|
||||
<input type="submit" value="Show all files" />
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
[end]
|
||||
</table>
|
||||
[end]
|
||||
|
||||
[# if you want to disable tarball generation remove the following: ]
|
||||
[if-any tarball_href]
|
||||
<p style="margin:0;"><a href="[tarball_href]">Download GNU tarball</a></p>
|
||||
<hr/>
|
||||
<p style="margin:0;"><a href="[tarball_href]">Download GNU tarball</a></p>
|
||||
[end]
|
||||
|
||||
[include "props.ezt"]
|
||||
[include "footer.ezt"]
|
||||
|
||||
|
||||
@@ -24,15 +24,34 @@
|
||||
[is roottype "svn"]
|
||||
<tr>
|
||||
<td>Directory revision:</td>
|
||||
<td><a href="[tree_rev_href]">[tree_rev]</a>[if-any youngest_rev] (of <a href="[youngest_rev_href]">[youngest_rev]</a>)[end]</td>
|
||||
<td><a href="[tree_rev_href]" title="Revision [tree_rev]">[tree_rev]</a>[if-any youngest_rev] (of <a href="[youngest_rev_href]" title="Revision [youngest_rev]">[youngest_rev]</a>)[end]</td>
|
||||
</tr>
|
||||
[end]
|
||||
<tr>
|
||||
<td>Sticky [is roottype "cvs"]Tag[else]Revision[end]:</td>
|
||||
<td>[include "pathrev_form.ezt"]</td>
|
||||
</tr>
|
||||
[if-any search_re]
|
||||
<tr><td>Current search:</td><td><strong>[search_re]</strong></td></tr>
|
||||
|
||||
[if-any search_re_action]
|
||||
<tr>
|
||||
<td>Filter files by content:</td>
|
||||
<td><form method="get" action="[search_re_action]" style="display: inline;">
|
||||
<div style="display: inline;">
|
||||
[for search_re_hidden_values]<input type="hidden" name="[search_re_hidden_values.name]" value="[search_re_hidden_values.value]"/>[end]
|
||||
<input type="text" name="search" value="[search_re]" />
|
||||
<input type="submit" value="Search Regexp" />
|
||||
</div>
|
||||
</form>
|
||||
[if-any search_re]
|
||||
<form method="get" action="[search_re_action]" style="display: inline;">
|
||||
<div style="display: inline;">
|
||||
[for search_re_hidden_values]<input type="hidden" name="[search_re_hidden_values.name]" value="[search_re_hidden_values.value]"/>[end]
|
||||
<input type="submit" value="Show All Files" />
|
||||
</div>
|
||||
</form>
|
||||
[end]
|
||||
</td>
|
||||
</tr>
|
||||
[end]
|
||||
|
||||
[if-any queryform_href]
|
||||
@@ -41,24 +60,25 @@
|
||||
<td><a href="[queryform_href]">Query revision history</a></td>
|
||||
</tr>
|
||||
[end]
|
||||
|
||||
</table>
|
||||
[is cfg.options.use_pagesize "0"]
|
||||
|
||||
[is picklist_len "0"]
|
||||
[else]
|
||||
[is picklist_len "1"]
|
||||
[else]
|
||||
[is picklist_len "1"]
|
||||
[else]
|
||||
<form method="get" action="[dir_paging_action]">
|
||||
[dir_paging_hidden_values]
|
||||
<input type="submit" value="Go to:" />
|
||||
<select name="dir_pagestart" onchange="submit()">
|
||||
[for picklist]
|
||||
<option [is picklist.count dir_pagestart]selected[end] value="[picklist.count]">Page [picklist.page]: [picklist.start] to [picklist.end]</option>
|
||||
[end]
|
||||
</select>
|
||||
</form>
|
||||
[end]
|
||||
<form method="get" action="[dir_paging_action]">
|
||||
[for dir_paging_hidden_values]<input type="hidden" name="[dir_paging_hidden_values.name]" value="[dir_paging_hidden_values.value]"/>[end]
|
||||
<input type="submit" value="Go to:" />
|
||||
<select name="dir_pagestart" onchange="submit()">
|
||||
[for picklist]
|
||||
<option [is picklist.count dir_pagestart]selected[end] value="[picklist.count]">Page [picklist.page]: [picklist.start] to [picklist.end]</option>
|
||||
[end]
|
||||
</select>
|
||||
</form>
|
||||
[end]
|
||||
[end]
|
||||
|
||||
<p><a name="dirlist"></a></p>
|
||||
|
||||
<hr />
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<p style="margin:0;">
|
||||
[is pathtype "file"]
|
||||
<a href="[up_href]"><img src="[docroot]/images/back_small.png" width="16" height="16" alt="Parent Directory" /> Parent Directory</a>
|
||||
<a href="[up_href]"><img src="[docroot]/images/back_small.png" class="vc_icon" alt="Parent Directory" /> Parent Directory</a>
|
||||
[if-any log_href]
|
||||
| <a href="[log_href][if-any log_href_rev]#rev[log_href_rev][end]"><img src="[docroot]/images/log.png" width="16" height="16" alt="Revision Log" /> Revision Log</a>
|
||||
| <a href="[log_href]"><img src="[docroot]/images/log.png" class="vc_icon" alt="Revision Log" /> Revision Log</a>
|
||||
[end]
|
||||
[if-any graph_href]
|
||||
| <a href="[graph_href]"><img src="[docroot]/images/cvsgraph_16x16.png" width="16" height="16" alt="View Revision Graph" /> Revision Graph</a>
|
||||
| <a href="[graph_href]"><img src="[docroot]/images/cvsgraph_16x16.png" class="vc_icon" alt="View Revision Graph" /> Revision Graph</a>
|
||||
[end]
|
||||
[is view "diff"]
|
||||
| <a href="[patch_href]"><img src="[docroot]/images/diff.png" width="16" height="16" alt="View Patch" /> Patch</a>
|
||||
| <a href="[patch_href]"><img src="[docroot]/images/diff.png" class="vc_icon" alt="View Patch" /> Patch</a>
|
||||
[end]
|
||||
[else]
|
||||
<a href="[view_href]"><img src="[docroot]/images/dir.png" width="16" height="16" alt="View Directory Listing" /> Directory Listing</a>
|
||||
<a href="[view_href]"><img src="[docroot]/images/dir.png" class="vc_icon" alt="View Directory Listing" /> Directory Listing</a>
|
||||
[end]
|
||||
</p>
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><address>[cfg.general.address]</address></td>
|
||||
<td style="text-align: right;"><strong><a href="[help_href]">ViewVC Help</a></strong></td>
|
||||
<td>[if-any cfg.general.address]<address><a href="mailto:[cfg.general.address]">[cfg.general.address]</a></address>[else] [end]</td>
|
||||
<td style="text-align: right;">[if-any help_href]<strong><a href="[help_href]">ViewVC Help</a></strong>[else] [end]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Powered by <a href="http://viewvc.tigris.org/">ViewVC [vsn]</a></td>
|
||||
<td style="text-align: right;">[if-any rss_href]<a href="[rss_href]" title="RSS 2.0 feed"><img src="[docroot]/images/feed-icon-16x16.jpg" alt="RSS 2.0 feed"/></a>[else] [end]</td>
|
||||
<td style="text-align: right;">[if-any rss_href]<a href="[rss_href]" title="RSS 2.0 feed"><img src="[docroot]/images/feed-icon-16x16.jpg" class="vc_icon" alt="RSS 2.0 feed" /></a>[else] [end]</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -5,71 +5,19 @@
|
||||
<head>
|
||||
<title>[if-any rootname][[][rootname]][else]ViewVC[end] [page_title]</title>
|
||||
<meta name="generator" content="ViewVC [vsn]" />
|
||||
<link rel="shortcut icon" href="[docroot]/images/favicon.ico" />
|
||||
<link rel="stylesheet" href="[docroot]/styles.css" type="text/css" />
|
||||
[if-any rss_href]<link rel="alternate" type="application/rss+xml" title="RSS [[][rootname]][where]" href="[rss_href]" />[end]
|
||||
</head>
|
||||
<body>
|
||||
<div class="vc_navheader">
|
||||
[if-any roots]
|
||||
<form method="get" action="[change_root_action]">
|
||||
[end]
|
||||
<table style="padding:0.1em;">
|
||||
<tr>
|
||||
<td>
|
||||
[if-any nav_path]<strong>
|
||||
[for nav_path]
|
||||
[if-any nav_path.href]<a href="[nav_path.href]">[end]
|
||||
[if-index nav_path first]
|
||||
[[][nav_path.name]][else]
|
||||
[nav_path.name][end][if-any nav_path.href]</a>[end]
|
||||
[if-index nav_path last][else]/[end]
|
||||
[end]
|
||||
</strong>
|
||||
[end]
|
||||
</td>
|
||||
<td style="text-align:right;">
|
||||
[if-any roots]
|
||||
[change_root_hidden_values]
|
||||
<strong>Repository:</strong>
|
||||
<select name="root" onchange="submit()">
|
||||
[define cvs_root_options][end]
|
||||
[define svn_root_options][end]
|
||||
<option value="*viewroots*"[is view "roots"] selected="selected"[else][end]>Repository Listing</option>
|
||||
[for roots]
|
||||
[define root_option][end]
|
||||
[is roots.name rootname]
|
||||
[define root_option]<option selected="selected">[roots.name]</option>[end]
|
||||
[else]
|
||||
[define root_option]<option>[roots.name]</option>[end]
|
||||
[end]
|
||||
[is roots.type "cvs"]
|
||||
[define cvs_root_options][cvs_root_options][root_option][end]
|
||||
[else]
|
||||
[is roots.type "svn"]
|
||||
[define svn_root_options][svn_root_options][root_option][end]
|
||||
[end]
|
||||
[end]
|
||||
[end]
|
||||
[is cvs_root_options ""][else]
|
||||
<optgroup label="CVS Repositories">[cvs_root_options]</optgroup>
|
||||
[end]
|
||||
[is svn_root_options ""][else]
|
||||
<optgroup label="Subversion Repositories">[svn_root_options]</optgroup>
|
||||
[end]
|
||||
</select>
|
||||
<input type="submit" value="Go" />
|
||||
[else]
|
||||
|
||||
[end]
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
[if-any roots]
|
||||
</form>
|
||||
[end]
|
||||
<table><tr>
|
||||
<td><strong>[if-any roots_href]<a href="[roots_href]"><span class="pathdiv">/</span></a>[else]<span class="pathdiv">/</span>[end][if-any nav_path][for nav_path][if-any nav_path.href]<a href="[nav_path.href]">[end][if-index nav_path first][[][nav_path.name]][else][nav_path.name][end][if-any nav_path.href]</a>[end][if-index nav_path last][else]<span class="pathdiv">/</span>[end][end][end]</strong></td>
|
||||
<td style="text-align: right;">[if-any username]Logged in as: <strong>[username]</strong>[end]</td>
|
||||
</tr></table>
|
||||
</div>
|
||||
|
||||
<div style="float: right; padding: 5px;"><a href="http://www.viewvc.org/"><img src="[docroot]/images/logo.png" alt="ViewVC logotype" width="128" height="48" /></a></div>
|
||||
<div style="float: right; padding: 5px;"><a href="http://www.viewvc.org/" title="ViewVC Home"><img src="[docroot]/images/viewvc-logo.png" alt="ViewVC logotype" width="240" height="70" /></a></div>
|
||||
<h1>[page_title]</h1>
|
||||
|
||||
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
[end]
|
||||
|
||||
[is pathtype "file"]
|
||||
[if-any view_href]
|
||||
[if-any head_view_href]
|
||||
<tr>
|
||||
<td>Links to HEAD:</td>
|
||||
<td>
|
||||
(<a href="[view_href]">view</a>)
|
||||
[if-any download_href](<a href="[download_href]">download</a>)[end]
|
||||
[if-any download_text_href](<a href="[download_text_href]">as text</a>)[end]
|
||||
[if-any annotate_href](<a href="[annotate_href]">annotate</a>)[end]
|
||||
(<a href="[head_view_href]">view</a>)
|
||||
[if-any head_download_href](<a href="[head_download_href]">download</a>)[end]
|
||||
[if-any head_download_text_href](<a href="[head_download_text_href]">as text</a>)[end]
|
||||
[if-any head_annotate_href](<a href="[head_annotate_href]">annotate</a>)[end]
|
||||
</td>
|
||||
</tr>
|
||||
[end]
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
[is cfg.options.use_pagesize "0"]
|
||||
[is picklist_len "0"]
|
||||
[else]
|
||||
[is picklist_len "1"]
|
||||
[else]
|
||||
<hr />
|
||||
<form method="get" action="[log_paging_action]">
|
||||
[log_paging_hidden_values]
|
||||
[for log_paging_hidden_values]<input type="hidden" name="[log_paging_hidden_values.name]" value="[log_paging_hidden_values.value]"/>[end]
|
||||
<input type="submit" value="Go to:">
|
||||
<select name="log_pagestart" onchange="submit()">
|
||||
[for picklist]
|
||||
[if-any picklist.more]
|
||||
<option [is picklist.count log_pagestart]selected[end] value="[picklist.count]">Page [picklist.page]: [picklist.start] ...</option>
|
||||
[else]
|
||||
<option [is picklist.count log_pagestart]selected[end] value="[picklist.count]">Page [picklist.page]: [picklist.start] - [picklist.end]</option>
|
||||
[end]
|
||||
[end]
|
||||
</select>
|
||||
</form>
|
||||
[end]
|
||||
[end]
|
||||
|
||||
|
||||
[end]
|
||||
@@ -1,6 +1,6 @@
|
||||
<form method="get" action="[pathrev_action]" style="display: inline">
|
||||
<div style="display: inline">
|
||||
[pathrev_hidden_values]
|
||||
[for pathrev_hidden_values]<input type="hidden" name="[pathrev_hidden_values.name]" value="[pathrev_hidden_values.value]"/>[end]
|
||||
[is roottype "cvs"]
|
||||
[define pathrev_selected][pathrev][end]
|
||||
<select name="pathrev" onchange="submit()">
|
||||
@@ -41,7 +41,7 @@
|
||||
[if-any pathrev]
|
||||
<form method="get" action="[pathrev_clear_action]" style="display: inline">
|
||||
<div style="display: inline">
|
||||
[pathrev_clear_hidden_values]
|
||||
[for pathrev_clear_hidden_values]<input type="hidden" name="[pathrev_clear_hidden_values.name]" value="[pathrev_clear_hidden_values.value]"/>[end]
|
||||
[if-any lastrev]
|
||||
[is pathrev lastrev][else]<input type="submit" value="Set to [lastrev]" />[end]
|
||||
(<i>Current path doesn't exist after revision <strong>[lastrev]</strong></i>)
|
||||
|
||||
26
templates/include/props.ezt
Normal file
26
templates/include/props.ezt
Normal file
@@ -0,0 +1,26 @@
|
||||
[if-any properties]
|
||||
<hr/>
|
||||
<div class="vc_properties">
|
||||
<h2>Properties</h2>
|
||||
<table cellspacing="1" cellpadding="2" class="auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="vc_header_sort">Name</th>
|
||||
<th class="vc_header">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
[for properties]
|
||||
<tr class="vc_row_[if-index properties even]even[else]odd[end]">
|
||||
<td><strong>[properties.name]</strong></td>
|
||||
[if-any properties.undisplayable]
|
||||
<td><em>Property value is undisplayable.</em></td>
|
||||
[else]
|
||||
<td style="white-space: pre;">[properties.value]</td>
|
||||
[end]
|
||||
</tr>
|
||||
[end]
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
[end]
|
||||
@@ -1,8 +1,10 @@
|
||||
[is roottype "svn"]
|
||||
[else]
|
||||
<form method="get" action="[logsort_action]">
|
||||
<div>
|
||||
<hr />
|
||||
<a name="logsort"></a>
|
||||
[logsort_hidden_values]
|
||||
[for logsort_hidden_values]<input type="hidden" name="[logsort_hidden_values.name]" value="[logsort_hidden_values.value]"/>[end]
|
||||
Sort log by:
|
||||
<select name="logsort" onchange="submit()">
|
||||
<option value="cvs" [is logsort "cvs"]selected="selected"[end]>Not sorted</option>
|
||||
@@ -12,3 +14,4 @@
|
||||
<input type="submit" value=" Sort " />
|
||||
</div>
|
||||
</form>
|
||||
[end]
|
||||
@@ -6,6 +6,7 @@
|
||||
[for entries]
|
||||
[if-index entries first][define first_revision][entries.rev][end][end]
|
||||
[if-index entries last][define last_revision][entries.rev][end][end]
|
||||
|
||||
<div>
|
||||
<hr />
|
||||
|
||||
@@ -19,10 +20,12 @@
|
||||
[end]
|
||||
|
||||
Revision [is roottype "svn"]<a href="[entries.revision_href]"><strong>[entries.rev]</strong></a>[else]<strong>[entries.rev]</strong>[end] -
|
||||
[is pathtype "file"]
|
||||
(<a href="[entries.view_href]">view</a>)
|
||||
[else]
|
||||
<a href="[entries.view_href]">Directory Listing</a>
|
||||
[if-any entries.view_href]
|
||||
[is pathtype "file"]
|
||||
(<a href="[entries.view_href]">view</a>)
|
||||
[else]
|
||||
<a href="[entries.view_href]">Directory Listing</a>
|
||||
[end]
|
||||
[end]
|
||||
[if-any entries.download_href](<a href="[entries.download_href]">download</a>)[end]
|
||||
[if-any entries.download_text_href](<a href="[entries.download_text_href]">as text</a>)[end]
|
||||
@@ -48,7 +51,9 @@
|
||||
[if-index entries last]Added[else]Modified[end]
|
||||
[end]
|
||||
|
||||
<em>[entries.date]</em> ([entries.ago] ago) by <em>[entries.author]</em>
|
||||
<em>[if-any entries.date][entries.date][else](unknown date)[end]</em>
|
||||
[if-any entries.ago]([entries.ago] ago)[end]
|
||||
by <em>[if-any entries.author][entries.author][else](unknown author)[end]</em>
|
||||
|
||||
[if-any entries.orig_path]
|
||||
<br />Original Path: <a href="[entries.orig_href]"><em>[entries.orig_path]</em></a>
|
||||
@@ -93,6 +98,10 @@
|
||||
[end]
|
||||
[end]
|
||||
|
||||
[if-any entries.lockinfo]
|
||||
<br />Lock status: <img src="[docroot]/images/lock.png" alt="Locked" width="16" height="16" /> [entries.lockinfo]
|
||||
[end]
|
||||
|
||||
[is entries.state "dead"]
|
||||
<br /><strong><em>FILE REMOVED</em></strong>
|
||||
[else]
|
||||
|
||||
@@ -34,10 +34,12 @@
|
||||
|
||||
[# Tasks column]
|
||||
<td>
|
||||
[is pathtype "file"]
|
||||
<a href="[entries.view_href]"><strong>View</strong></a><br />
|
||||
[else]
|
||||
<a href="[entries.view_href]"><strong>Directory Listing</strong></a><br />
|
||||
[if-any entries.view_href]
|
||||
[is pathtype "file"]
|
||||
<a href="[entries.view_href]"><strong>View</strong></a><br />
|
||||
[else]
|
||||
<a href="[entries.view_href]"><strong>Directory Listing</strong></a><br />
|
||||
[end]
|
||||
[end]
|
||||
[if-any entries.download_href]<a href="[entries.download_href]"><strong>Download</strong></a><br />[end]
|
||||
[if-any entries.download_text_href]<a href="[entries.download_text_href]"><strong>As text</strong></a><br />[end]
|
||||
@@ -108,7 +110,7 @@
|
||||
[# Tags ]
|
||||
[if-any entries.tags]
|
||||
<form method=get action="[pathrev_action]" >
|
||||
[pathrev_hidden_values]
|
||||
[for pathrev_hidden_values]<input type="hidden" name="[pathrev_hidden_values.name]" value="[pathrev_hidden_values.value]"/>[end]
|
||||
<select name="pathrev" onChange="submit()">
|
||||
<option value="" [is pathrev ""]selected[end]>Show all tags</option>
|
||||
[for entries.tags]
|
||||
@@ -126,7 +128,8 @@
|
||||
[is roottype "svn"]
|
||||
[if-index entries last]Added[else]Modified[end]
|
||||
[end]
|
||||
[entries.ago] ago<br /><em>[entries.date]</em>
|
||||
[if-any entries.ago][entries.ago] ago<br />[end]
|
||||
[if-any entries.date]<em>[entries.date]</em>[end]
|
||||
[is roottype "cvs"]
|
||||
[if-any entries.prev]
|
||||
[if-any entries.changed]
|
||||
@@ -144,9 +147,14 @@
|
||||
</tr>
|
||||
<tr class="vc_row_[if-index entries even]even[else]odd[end]">
|
||||
<td colspan=5>
|
||||
|
||||
[if-any entries.lockinfo]
|
||||
<strong>Lock status</strong>: <img src="[docroot]/images/lock.png" alt="Locked" width="16" height="16" /> [entries.lockinfo]<br />
|
||||
[end]
|
||||
|
||||
[is roottype "svn"]
|
||||
[if-any entries.orig_path]
|
||||
Original Path: <a href="[entries.orig_href]"><em>[entries.orig_path]</em></a><br />
|
||||
<strong>Original Path</strong>: <a href="[entries.orig_href]"><em>[entries.orig_path]</em></a><br />
|
||||
[end]
|
||||
|
||||
[if-any entries.size]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user