Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df339937f7 | ||
|
|
6bada56b51 | ||
|
|
883a31e6ed | ||
|
|
e1be6057b9 | ||
|
|
8bc836053b | ||
|
|
365e9eb651 | ||
|
|
839735de76 | ||
|
|
d4ddc6f907 | ||
|
|
65456364c7 | ||
|
|
8d4974787c | ||
|
|
839067e342 | ||
|
|
e5552b5111 | ||
|
|
20c1fc528b | ||
|
|
ceb17d1d20 | ||
|
|
1303633d45 | ||
|
|
23b5c0b0b7 | ||
|
|
baedf8bf14 | ||
|
|
ea0c0ef83b | ||
|
|
9cfce67e5d | ||
|
|
3dff17e2a7 | ||
|
|
8c888a567c | ||
|
|
df3a725d94 | ||
|
|
e593b390f2 | ||
|
|
20707501a1 | ||
|
|
1588c43186 | ||
|
|
4ddebdbe9d | ||
|
|
a42c40db09 | ||
|
|
43b255e0b7 | ||
|
|
fcb3231ba0 | ||
|
|
28dbff13f8 | ||
|
|
7499d1c1f6 | ||
|
|
d1e5445d82 | ||
|
|
9518515adb | ||
|
|
d4960d7e29 | ||
|
|
e3255b6651 | ||
|
|
244d5da838 | ||
|
|
c5a480733f | ||
|
|
5c92c7af4f | ||
|
|
8dc4556f95 | ||
|
|
d1c86ecf10 | ||
|
|
e9e5a65fb0 | ||
|
|
829b7f8c24 | ||
|
|
8e851e582b | ||
|
|
06fa17dc72 | ||
|
|
d99130aa84 | ||
|
|
215ae0ef5b | ||
|
|
02e7b86582 | ||
|
|
2499192f7c | ||
|
|
ffe1691820 | ||
|
|
bc1db33b38 | ||
|
|
2e574f82ff | ||
|
|
946f9891bc | ||
|
|
8f8d39e075 | ||
|
|
a9cc95c1bc | ||
|
|
4aee59c60c | ||
|
|
f6c3cd30e2 | ||
|
|
5fded7d5f8 | ||
|
|
cfcb60ea85 | ||
|
|
ae19f8ca7f | ||
|
|
66539850bd | ||
|
|
64a554ecef | ||
|
|
f35fa7e288 | ||
|
|
cde7b1e6ef | ||
|
|
6aaad58f64 | ||
|
|
7849a83bb1 | ||
|
|
0a429d9098 | ||
|
|
49422f5f17 | ||
|
|
494a4cedd6 | ||
|
|
a5528a8e1d | ||
|
|
50b5abdccf | ||
|
|
5e416ed523 | ||
|
|
607c755d79 | ||
|
|
662ef03f9f | ||
|
|
79dc6a7c79 | ||
|
|
64d5cc74b9 | ||
|
|
824f604814 | ||
|
|
10135bc0c7 | ||
|
|
b0f36b7458 | ||
|
|
2b3dc1f862 | ||
|
|
653cb57084 | ||
|
|
40cb20bef0 | ||
|
|
9d172acde3 | ||
|
|
41b4d618f5 | ||
|
|
5a73d32298 | ||
|
|
040b5bff6a | ||
|
|
d0db763fb3 | ||
|
|
ca965f9154 | ||
|
|
ed276302aa | ||
|
|
4d2f283783 | ||
|
|
335125d2bc | ||
|
|
021892b68b | ||
|
|
c291f82054 | ||
|
|
a5a7bc702c | ||
|
|
a2cc39ef5b | ||
|
|
0577622570 | ||
|
|
b87b81ce5a | ||
|
|
f8decacb36 | ||
|
|
9be68a937f | ||
|
|
5b6a369834 | ||
|
|
5eaef7d297 | ||
|
|
e1e68a1279 | ||
|
|
9fffc28d34 | ||
|
|
61cf00167d | ||
|
|
d9ce092839 | ||
|
|
16ab711746 | ||
|
|
756d372646 | ||
|
|
ca8fb6e77b | ||
|
|
43b2655835 | ||
|
|
4eb5a868cd | ||
|
|
58ca064814 | ||
|
|
43d4a79170 | ||
|
|
17ad34e455 | ||
|
|
0ed0711977 | ||
|
|
a18961f51d | ||
|
|
26b26b7059 | ||
|
|
27fdb43ced | ||
|
|
66d5937d23 | ||
|
|
5763ce77f0 | ||
|
|
fd07da80d4 | ||
|
|
17e91cc027 | ||
|
|
96aa745c96 | ||
|
|
2b8fc80ec2 | ||
|
|
a4de84fda7 | ||
|
|
cae8704e09 | ||
|
|
d3aa1f1b7b | ||
|
|
f7bbd7e3bc | ||
|
|
541f061ad9 | ||
|
|
b953490d57 | ||
|
|
177c1167df | ||
|
|
b0589e11ea | ||
|
|
606f135670 | ||
|
|
6939cb71af | ||
|
|
6b072767f2 | ||
|
|
d211d55fbe | ||
|
|
03ab7169d4 | ||
|
|
eb12e90516 | ||
|
|
573f0e6cbd | ||
|
|
37e7ef0659 | ||
|
|
99e6bd2c0b | ||
|
|
226be2e5da | ||
|
|
30e7cada55 | ||
|
|
c17187528e | ||
|
|
c4eebda10d | ||
|
|
60a888826c | ||
|
|
c27488d3ff | ||
|
|
a9f9ce2f1d | ||
|
|
f60f03a3c8 | ||
|
|
e6b874bfe3 | ||
|
|
11f1ee5ad5 | ||
|
|
46b9617705 | ||
|
|
5fa239359a | ||
|
|
0e7de977ea | ||
|
|
efa719451c | ||
|
|
0feda99acd | ||
|
|
3ed3b6e31b | ||
|
|
c4cdcdb7b5 | ||
|
|
5381dec8c0 | ||
|
|
a098be0814 | ||
|
|
aadc1acd79 | ||
|
|
dda8093171 | ||
|
|
fa27e5bdf8 | ||
|
|
479732ce3f | ||
|
|
53f3847529 | ||
|
|
8e22859df9 | ||
|
|
95bbba6315 | ||
|
|
0658b1477e | ||
|
|
b4690e4cf8 | ||
|
|
74fc70c16c | ||
|
|
0569081815 | ||
|
|
b137ac0ae1 | ||
|
|
c5827f1efd | ||
|
|
2840343cea | ||
|
|
b41904baf9 | ||
|
|
3e9f1613d1 | ||
|
|
00a72f22f7 | ||
|
|
aa2006777f | ||
|
|
0a6288aadc | ||
|
|
4ce1fca204 | ||
|
|
62bd2f5255 | ||
|
|
a57333116b | ||
|
|
4af7a6538a | ||
|
|
9fe67894af | ||
|
|
98a5533174 | ||
|
|
a30b627be4 | ||
|
|
cedb6e461e | ||
|
|
5e0ebbba3d | ||
|
|
c232364b2a | ||
|
|
f5a08c6e6b | ||
|
|
d81efa06e5 | ||
|
|
9c8b495d98 | ||
|
|
f951a3d306 | ||
|
|
aebec48195 | ||
|
|
1a82ee4db3 | ||
|
|
f6fff51d7d | ||
|
|
e33bd3cc15 | ||
|
|
7671ad28dd | ||
|
|
0e90c26e2e | ||
|
|
5ca21751e0 | ||
|
|
0388df418c | ||
|
|
ecaf80a17a | ||
|
|
06f8519e19 | ||
|
|
18c6d7ff69 | ||
|
|
01de62f578 | ||
|
|
28c696380a | ||
|
|
a187d3a8dd | ||
|
|
45ce3b4a09 | ||
|
|
7e721943c0 | ||
|
|
71b8d795d2 | ||
|
|
2d3ce6be44 | ||
|
|
7efb6ea23d | ||
|
|
cc45442862 | ||
|
|
4824c48b8d | ||
|
|
ba60e451d9 | ||
|
|
c918b77fd7 | ||
|
|
2f30c346b3 | ||
|
|
5d4d43dcff | ||
|
|
3e96c6db2b | ||
|
|
120b4edf0f | ||
|
|
d0388c6e65 | ||
|
|
39302a13ec | ||
|
|
e281c208ab | ||
|
|
46f7530225 | ||
|
|
b367a90a94 | ||
|
|
a9356a41a3 | ||
|
|
dfa8841fd8 | ||
|
|
3a459d32e3 | ||
|
|
996189613f | ||
|
|
039b4b2ed6 | ||
|
|
ca9dfb8e3e | ||
|
|
844f5c1350 | ||
|
|
79cf462dbe | ||
|
|
d8525e47b9 | ||
|
|
dbfd93a5c3 | ||
|
|
1cd1672276 | ||
|
|
44fc1dbf29 | ||
|
|
d4be2558ba | ||
|
|
22bdc9d1aa | ||
|
|
6da5dc666f | ||
|
|
6b40eed965 | ||
|
|
3d14ec1a36 | ||
|
|
3bbc82dd6e | ||
|
|
c4d05efdfe | ||
|
|
cff51b2ff2 | ||
|
|
9ebdfd942f | ||
|
|
9218e16272 | ||
|
|
7ff1bbe966 | ||
|
|
3f136a496b | ||
|
|
3d6e63b403 | ||
|
|
134c70f756 | ||
|
|
7e7abcd058 | ||
|
|
df904f0a18 | ||
|
|
246060e74f | ||
|
|
43aacbc260 | ||
|
|
37b5ed08ef | ||
|
|
3b936753fc | ||
|
|
746dcea928 | ||
|
|
62c60587e6 | ||
|
|
eb98e69a8c | ||
|
|
094b3c5279 | ||
|
|
c85dc03e72 | ||
|
|
eb3a1adb50 | ||
|
|
da8eb59f66 | ||
|
|
63fb34ce4f | ||
|
|
81eb60dd97 | ||
|
|
727c8df183 | ||
|
|
9b07159568 | ||
|
|
3faaa3da0e | ||
|
|
7379783ec7 | ||
|
|
1de012f8b7 | ||
|
|
538a3b1583 | ||
|
|
6c3913aaef | ||
|
|
010440a47c | ||
|
|
aadbc8541a | ||
|
|
7b557b2fa2 | ||
|
|
a723a81079 | ||
|
|
59f9a4639a | ||
|
|
2455bb831a | ||
|
|
23832e6c5a | ||
|
|
6324036319 | ||
|
|
fdbce9b00d | ||
|
|
b08e125c87 | ||
|
|
dca01509c5 | ||
|
|
e373d27e75 | ||
|
|
33fdd38b11 | ||
|
|
352767fa3a | ||
|
|
5ab9d12f90 | ||
|
|
5d8dcf9e18 | ||
|
|
de751a993e | ||
|
|
c667500d30 | ||
|
|
ad6e151d48 | ||
|
|
450b92715b | ||
|
|
1bc259730f | ||
|
|
35291f00eb | ||
|
|
baad1d4496 | ||
|
|
1580666545 | ||
|
|
c19f2fdd4e | ||
|
|
8124f27e19 | ||
|
|
f849eee3c7 | ||
|
|
62359adc34 | ||
|
|
e1175c3761 | ||
|
|
066fd281db | ||
|
|
2012d6c8d3 | ||
|
|
7101cb8303 | ||
|
|
560ae38f68 | ||
|
|
75e7315cb8 | ||
|
|
2fbccecc0d | ||
|
|
3361761bc3 | ||
|
|
1645870bb4 | ||
|
|
ada59fa952 | ||
|
|
2989422aee | ||
|
|
8c1d1c8f05 | ||
|
|
7cf0246a14 | ||
|
|
623b809b8a | ||
|
|
9b666a5e03 | ||
|
|
1db197ca4d | ||
|
|
73a7191758 | ||
|
|
00f6b77ee1 | ||
|
|
bc5940bd36 | ||
|
|
6a09349645 | ||
|
|
297ad386e3 | ||
|
|
920f736b6e | ||
|
|
3ee70d6800 | ||
|
|
2975050c94 | ||
|
|
a2e46a327f | ||
|
|
e823d670cc | ||
|
|
bd7f59bc05 | ||
|
|
55a71ee4ab | ||
|
|
cec9bbaef0 | ||
|
|
ab29466607 | ||
|
|
36fece9243 | ||
|
|
e15237e62b | ||
|
|
2ae4777638 | ||
|
|
ee150af446 | ||
|
|
d32d2bea41 | ||
|
|
f54b479f47 | ||
|
|
106cddcef6 | ||
|
|
5260e0f067 | ||
|
|
5af01cdbbf | ||
|
|
792148e069 | ||
|
|
f9bb2c1407 | ||
|
|
68e464c8f0 | ||
|
|
814227f984 | ||
|
|
84e1922b4d | ||
|
|
107b336826 | ||
|
|
61cc4575a6 | ||
|
|
8fb8a27fa0 | ||
|
|
091ffcd496 | ||
|
|
986e205e76 | ||
|
|
d6f9095b98 | ||
|
|
b1dad205d7 | ||
|
|
44b816381a | ||
|
|
1736682a77 | ||
|
|
d393307727 | ||
|
|
dbb89e54fe | ||
|
|
18c14746dc | ||
|
|
1aaeccd369 | ||
|
|
16e5126461 | ||
|
|
6aa01795e0 | ||
|
|
18100eb677 | ||
|
|
a75b9e111d | ||
|
|
05364663e4 | ||
|
|
5dc0868487 | ||
|
|
5905459466 | ||
|
|
ee38c44863 | ||
|
|
cd5c1bb039 | ||
|
|
cdc2011134 | ||
|
|
5e19e3ecb1 | ||
|
|
ec271b19bb | ||
|
|
9683fa918b | ||
|
|
50edfcf994 | ||
|
|
61ae73fc2e | ||
|
|
e67e142e8f | ||
|
|
121f0aee66 | ||
|
|
29a5e71745 | ||
|
|
09e851a987 | ||
|
|
3a064b687f | ||
|
|
85a4908e96 | ||
|
|
3f88b33d9b | ||
|
|
edbab74c55 | ||
|
|
8906ada850 | ||
|
|
c1a6023566 | ||
|
|
c06f7dc755 | ||
|
|
6b6c0c310c | ||
|
|
6f80a9f4a8 | ||
|
|
c4becc0683 | ||
|
|
60c679a23f | ||
|
|
6bf3db4449 | ||
|
|
0383e34bdf | ||
|
|
8a022389c1 | ||
|
|
5173faed40 | ||
|
|
10e7da10c9 | ||
|
|
dd6bb1acb2 | ||
|
|
cb382e4357 | ||
|
|
fb294f3a9b | ||
|
|
b14ff6c261 | ||
|
|
2bbe2c2537 | ||
|
|
dada04c548 | ||
|
|
7154f5e16c | ||
|
|
35ea2cefdf | ||
|
|
a5756bf09b | ||
|
|
760a6a72cd | ||
|
|
c1dc4181bf | ||
|
|
0276f1b000 | ||
|
|
73acd20d23 | ||
|
|
901a8a2a57 | ||
|
|
995877a653 | ||
|
|
2bdae67221 | ||
|
|
802e0811dd | ||
|
|
1243ab61a2 | ||
|
|
1cc942976f | ||
|
|
e5b35c1af5 | ||
|
|
aadd26c00a | ||
|
|
c4355b28e2 | ||
|
|
7e970792a4 | ||
|
|
34762da4fa | ||
|
|
27b0eadcde | ||
|
|
effbfb6270 | ||
|
|
171594a61a | ||
|
|
b53e69823f | ||
|
|
87d0848d38 | ||
|
|
203d59b115 | ||
|
|
19bdbed35a | ||
|
|
da991ab186 | ||
|
|
a6a73211cc | ||
|
|
6beea81452 | ||
|
|
186b1f15f2 | ||
|
|
3d57a83450 | ||
|
|
ea8ab2dc11 | ||
|
|
ae51a72acb | ||
|
|
c46c1a81b9 | ||
|
|
980d874889 | ||
|
|
96f4423b31 | ||
|
|
15b3d95ca6 | ||
|
|
5d00b99974 | ||
|
|
3dce4c9732 | ||
|
|
6c47625e08 | ||
|
|
2ea82700aa | ||
|
|
0259c4ca0a | ||
|
|
ac21b569d3 | ||
|
|
754acf8a4b | ||
|
|
271fd9a199 | ||
|
|
904b126eae | ||
|
|
89f0036448 | ||
|
|
b381361430 | ||
|
|
1d028ba3cc | ||
|
|
864af552bb | ||
|
|
e691073611 | ||
|
|
aba3869285 | ||
|
|
378a08c165 | ||
|
|
bb108320c7 | ||
|
|
b2ec47f2f0 | ||
|
|
90271f4af7 | ||
|
|
16676b6f1e | ||
|
|
feb81bf0f4 | ||
|
|
e95d37b3e1 | ||
|
|
4460ab0c55 | ||
|
|
1591217955 | ||
|
|
168c7aa7e9 | ||
|
|
752367ee92 | ||
|
|
876b5d2694 | ||
|
|
c91b6ae972 | ||
|
|
d889bc25e6 | ||
|
|
a4bd13e365 | ||
|
|
d2e8706845 | ||
|
|
2835cf60ab | ||
|
|
8bba79138d | ||
|
|
c744c25bc6 | ||
|
|
925d0ff765 | ||
|
|
923e431098 | ||
|
|
7e47d660f6 | ||
|
|
e73210682c | ||
|
|
794efbc3be | ||
|
|
818edc00d7 | ||
|
|
6e5c7d65fa | ||
|
|
bd86c6f1e2 | ||
|
|
b7525b6072 | ||
|
|
f0f8ff7e20 | ||
|
|
76e47a0d2f | ||
|
|
dd17e89996 | ||
|
|
fd8706cd24 | ||
|
|
eaef138da2 | ||
|
|
014e0e7b62 | ||
|
|
1f08e64fc4 | ||
|
|
88ae28e22f | ||
|
|
7b8d379d45 | ||
|
|
16c7f267d8 | ||
|
|
52b3a0c6e0 | ||
|
|
a09a151147 | ||
|
|
e3d9abaded | ||
|
|
49107e6dfd | ||
|
|
8c0df5e3b8 | ||
|
|
8234444bd7 | ||
|
|
29ab67ee1b | ||
|
|
8b0dfd84c4 | ||
|
|
58dfe801d3 | ||
|
|
ab164ab3c3 | ||
|
|
d08f9d07de | ||
|
|
31266c7ee2 | ||
|
|
1fefb8ac54 | ||
|
|
e9eb3aafa8 | ||
|
|
dfe9ddc264 | ||
|
|
dea0d057cd | ||
|
|
5d08751089 | ||
|
|
71da727f0f | ||
|
|
4fd4187ede | ||
|
|
24f74bf25c | ||
|
|
719d6dbb59 | ||
|
|
ce671cc42d | ||
|
|
0ce2adafb0 | ||
|
|
90e2540f50 | ||
|
|
63f20c460d | ||
|
|
98225274d2 | ||
|
|
7a644902fd | ||
|
|
1a989e31b8 | ||
|
|
594c4838c6 | ||
|
|
25044f6138 | ||
|
|
562e3b06a8 | ||
|
|
41e2f581e6 | ||
|
|
d4408343aa | ||
|
|
c3db49ca10 | ||
|
|
90596ebd62 | ||
|
|
60cf6d0c4b | ||
|
|
6ef1c4d4d7 | ||
|
|
a98a95542e | ||
|
|
8803186970 | ||
|
|
84e74ae26f | ||
|
|
83d8a45bee | ||
|
|
e26ceb332a | ||
|
|
2b03b346e3 | ||
|
|
8418a06ff1 | ||
|
|
604eb05352 | ||
|
|
0f69c32b3e | ||
|
|
7a72bb3377 | ||
|
|
e61d64f258 | ||
|
|
575b268ac8 | ||
|
|
fa0c67d2f0 | ||
|
|
f25844ecaa | ||
|
|
a514ae4095 | ||
|
|
246097d986 | ||
|
|
a8fe8f498e | ||
|
|
8a52a8acff | ||
|
|
1d22e4edc8 | ||
|
|
3d8c79b070 | ||
|
|
5218878567 | ||
|
|
faa3eb34cf | ||
|
|
3393af2f19 | ||
|
|
b8335b0dbb | ||
|
|
b2dac151b7 | ||
|
|
6c60f630dc | ||
|
|
0cfa2788a0 | ||
|
|
ea135d02b4 | ||
|
|
31fab34793 | ||
|
|
2e4793bccb | ||
|
|
e34b28902f | ||
|
|
415f4c87a9 | ||
|
|
0c61bd36ec | ||
|
|
f1a8070e23 | ||
|
|
9bd2cb059b | ||
|
|
26074b70ea | ||
|
|
863005d1ba | ||
|
|
4085f6c9ce | ||
|
|
79c5c9a32f | ||
|
|
25ae621d01 | ||
|
|
5bb03ec8ba | ||
|
|
a54a407762 | ||
|
|
c5b30e3593 | ||
|
|
c96e0c1e14 | ||
|
|
25abaf436a | ||
|
|
579360d384 | ||
|
|
a3921f7c21 | ||
|
|
db70e338b8 | ||
|
|
19db380842 | ||
|
|
ff10d82de1 | ||
|
|
ee2d00ffc6 | ||
|
|
e2526f60a4 | ||
|
|
0b2c607df2 | ||
|
|
ea498ede49 | ||
|
|
6ad4a0db46 | ||
|
|
ab8796fc34 | ||
|
|
5d113a0614 | ||
|
|
15ca99f677 | ||
|
|
ae1aacc692 | ||
|
|
548f011930 | ||
|
|
3d97704688 | ||
|
|
cf5fdeb998 | ||
|
|
5eed0c51cd | ||
|
|
226a5ab1c2 | ||
|
|
c7799976e1 | ||
|
|
9e06f3ec05 | ||
|
|
607ea6ddc8 | ||
|
|
607a4f4d96 | ||
|
|
0816b99cfb | ||
|
|
08dea92585 | ||
|
|
8fbd9e31f5 | ||
|
|
53e98570c4 | ||
|
|
69c900775b | ||
|
|
933d2f8a0a | ||
|
|
71d59c23dc | ||
|
|
675674cc67 | ||
|
|
80205ea16f | ||
|
|
d129d25466 | ||
|
|
7c693f91d3 | ||
|
|
e268f4e926 | ||
|
|
8dc7188d47 | ||
|
|
1182bb8217 | ||
|
|
4d352f58b4 | ||
|
|
cf0f4be5d1 | ||
|
|
85307ee9a1 | ||
|
|
d42b9dcafc | ||
|
|
aa29b28853 | ||
|
|
e4b8dde05b | ||
|
|
ba564772c6 | ||
|
|
9b156099c6 | ||
|
|
0668b4a380 | ||
|
|
7458da3455 | ||
|
|
208960acc7 | ||
|
|
accff05a48 | ||
|
|
a11af49e60 | ||
|
|
75185df70f | ||
|
|
6b5ce5f4bc | ||
|
|
87c984e371 | ||
|
|
548198abc6 | ||
|
|
464d651068 | ||
|
|
dd945441ed | ||
|
|
7d3d60322b | ||
|
|
8388f7e4c6 | ||
|
|
d50ea82596 | ||
|
|
bab6aa62bf | ||
|
|
3f0e8bbdc9 | ||
|
|
177eed86f3 | ||
|
|
3beabc69da | ||
|
|
325857d317 | ||
|
|
8525f78db2 | ||
|
|
1034abdfeb | ||
|
|
042dee98d4 | ||
|
|
fb3bd7ea6b | ||
|
|
4a926060c5 | ||
|
|
6ba1873a29 | ||
|
|
c67f76f202 | ||
|
|
0d82f79d86 | ||
|
|
3a7f61bfc3 | ||
|
|
ec45080d36 | ||
|
|
038cc52d14 | ||
|
|
89b04a638a | ||
|
|
31d1ce81fd | ||
|
|
f859aba466 | ||
|
|
7fe339e954 | ||
|
|
964d6eebc4 | ||
|
|
d0f1ab5fe5 | ||
|
|
7e5d58893c | ||
|
|
d34ae77e80 | ||
|
|
9bdc952237 | ||
|
|
94ce5dd92c | ||
|
|
d42fd033ef | ||
|
|
d4d68604f5 | ||
|
|
6466941893 | ||
|
|
3b562335ae | ||
|
|
fb3445a09f | ||
|
|
fc1bdccb9f | ||
|
|
7cdcc37bd4 | ||
|
|
9139be9be9 | ||
|
|
9552c0f853 | ||
|
|
d2e0a7e499 | ||
|
|
7b9adcee4b | ||
|
|
d63a636d79 | ||
|
|
a583324ff1 | ||
|
|
c611996663 | ||
|
|
b82ca69d69 | ||
|
|
81d40cbdac | ||
|
|
a3d0080bb3 | ||
|
|
d803b497cc | ||
|
|
ffaf968bf2 | ||
|
|
7a23888cb6 | ||
|
|
3439b0e6ce | ||
|
|
828c715d36 | ||
|
|
e9463b2307 | ||
|
|
618cf878a4 | ||
|
|
3a9f4b92f9 | ||
|
|
2f15ffa27b | ||
|
|
b69bdc4868 | ||
|
|
0567515572 | ||
|
|
8c6478b5f3 | ||
|
|
8bea3ce158 | ||
|
|
a48717e129 | ||
|
|
5eae81ce4d | ||
|
|
a902886dfe | ||
|
|
e62d16db14 | ||
|
|
5d7cb0fa39 | ||
|
|
6af5d67683 | ||
|
|
dbfa6ac0c2 | ||
|
|
f1a7d53f78 | ||
|
|
0592f94328 | ||
|
|
91196651d2 | ||
|
|
5455f1e682 | ||
|
|
8178833b4c | ||
|
|
096cd5c8be | ||
|
|
c3995e7399 | ||
|
|
1b7198c8ce | ||
|
|
0162327e3f | ||
|
|
9b332ca960 | ||
|
|
9e2e175e94 | ||
|
|
31980b4ab4 | ||
|
|
40d52deba1 | ||
|
|
8e0ff16340 | ||
|
|
20b7cf4302 | ||
|
|
86e8abbba8 | ||
|
|
c79c25d960 | ||
|
|
7007ce8126 | ||
|
|
712c5066f3 | ||
|
|
f323a00905 | ||
|
|
63a9ab32e2 | ||
|
|
08dbc128ce | ||
|
|
a20c7f5083 | ||
|
|
5cefaa08f0 | ||
|
|
31a79a9840 | ||
|
|
60b29e6d68 | ||
|
|
42c2db8d17 | ||
|
|
d6a84ac8bd | ||
|
|
d2d70ff708 | ||
|
|
7a1760e0f8 | ||
|
|
cb070daf2c | ||
|
|
8b08a4dea6 | ||
|
|
320df6bd0a | ||
|
|
99d0bb064e | ||
|
|
74c2ee9738 | ||
|
|
4dfbf66e83 | ||
|
|
57850301f9 | ||
|
|
de32d16dc7 | ||
|
|
e2cf5f0a53 | ||
|
|
53870294b6 | ||
|
|
cc0b70bfa3 | ||
|
|
188bba9f0b | ||
|
|
3c5463efb5 | ||
|
|
c6d6c32edf | ||
|
|
73d784605a | ||
|
|
43a3379bb9 | ||
|
|
596a587847 | ||
|
|
848623ca90 | ||
|
|
d35f6087a2 | ||
|
|
c84566c7bf | ||
|
|
d33c10a3b4 | ||
|
|
889b1dd559 | ||
|
|
c78a04f37d | ||
|
|
b3b84194b2 | ||
|
|
a39ba8d9af | ||
|
|
52dbb4e3bf | ||
|
|
f6869a59c0 | ||
|
|
599750b4ff | ||
|
|
1b8e0e9b7d | ||
|
|
9d11d1f545 | ||
|
|
0607bce439 | ||
|
|
087498ac49 | ||
|
|
6e8f4ab6ff | ||
|
|
17477a2609 | ||
|
|
6e97c7f2e0 | ||
|
|
49bf148b53 | ||
|
|
171dd0c62a | ||
|
|
e3d861df3f | ||
|
|
1f1420ab6d | ||
|
|
6cc9b54f54 | ||
|
|
039267c574 | ||
|
|
2af18c8742 | ||
|
|
942b59fe5b | ||
|
|
20bd747b51 | ||
|
|
27042f7061 | ||
|
|
545cc79f54 | ||
|
|
1deadb4bd8 | ||
|
|
14ce205c7f | ||
|
|
47303810ed | ||
|
|
4a205d6a98 | ||
|
|
3a4f350444 | ||
|
|
ffdb24164c | ||
|
|
bd0d3efc44 | ||
|
|
fe53a54d7f | ||
|
|
8485311a4f | ||
|
|
f85b184043 | ||
|
|
9296c491e8 | ||
|
|
39d7157abe | ||
|
|
b5856d7e02 | ||
|
|
870f240873 | ||
|
|
b20e3e6f4c | ||
|
|
9b15b1a083 | ||
|
|
36af28aff6 | ||
|
|
83748ea84a | ||
|
|
465005278b | ||
|
|
446a0bdb47 | ||
|
|
c4cc7474aa | ||
|
|
95b976dd4c | ||
|
|
e391197488 | ||
|
|
8dbcb13185 | ||
|
|
d48857d8e8 | ||
|
|
923349b5f1 | ||
|
|
7f97d2fe74 | ||
|
|
c374442e83 | ||
|
|
a82bb01e79 | ||
|
|
04a5337b1d | ||
|
|
9567ded3d0 | ||
|
|
c9dee3d3f1 | ||
|
|
8ad47e16a5 | ||
|
|
da680f1231 | ||
|
|
c2d466b355 | ||
|
|
01647f232a | ||
|
|
48f93bd93e | ||
|
|
d6c423f67c | ||
|
|
1a674917a3 | ||
|
|
b0c8e06ff6 | ||
|
|
58e8c86553 | ||
|
|
04de42d4f7 | ||
|
|
6ed329a709 | ||
|
|
0bc7d6bd1e | ||
|
|
e6a4610c44 | ||
|
|
1bb2483039 | ||
|
|
55b453a896 | ||
|
|
57f48dcb64 | ||
|
|
6169bb004d | ||
|
|
da1aedd515 | ||
|
|
fff8c628a5 | ||
|
|
993eb06c6e | ||
|
|
04e1db4b94 | ||
|
|
00f48f852e | ||
|
|
97e4c09491 | ||
|
|
d96358fbff | ||
|
|
62a0ac9aed | ||
|
|
47fc0ac961 | ||
|
|
05b803b9d6 | ||
|
|
d31feba27a | ||
|
|
a88d2fcd7c | ||
|
|
80578687ff | ||
|
|
88c11a401d | ||
|
|
daf633418a | ||
|
|
1806dbad74 | ||
|
|
dd13071bd3 | ||
|
|
659b8d8032 | ||
|
|
ca528f7b08 | ||
|
|
1c687fb56e | ||
|
|
64533fc958 | ||
|
|
a33a6cc23e | ||
|
|
42f9da1f6c | ||
|
|
cc6be1306c | ||
|
|
fd1c14c8fd | ||
|
|
95e1de309e | ||
|
|
84e05c1ecd | ||
|
|
b0a5a9a5a2 | ||
|
|
a71eb88f68 | ||
|
|
23a15165fa | ||
|
|
c2f2d3daaa | ||
|
|
7d28ffb92f | ||
|
|
e552f0c49c | ||
|
|
95e2280c66 | ||
|
|
362586ce16 | ||
|
|
540b4226f0 | ||
|
|
21b3b0efd0 | ||
|
|
211e3ccb86 | ||
|
|
8eb1ce6f73 | ||
|
|
50b82b127e | ||
|
|
003e3081d3 | ||
|
|
fd851a6bc9 | ||
|
|
d52775e800 | ||
|
|
c1ac1474ed | ||
|
|
ad3437453c |
+8
-14
@@ -1,27 +1,21 @@
|
||||
*.deb
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.rpm
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
.noseids
|
||||
/git-cola.app
|
||||
/build
|
||||
/build-stamp
|
||||
/ChangeLog
|
||||
/cola.tuxfamily.org
|
||||
/cola/builtin_version.py
|
||||
/config.mak
|
||||
/configure-stamp
|
||||
/dist
|
||||
/debian/*.log
|
||||
/debian/files
|
||||
/debian/bld
|
||||
/debian/cola
|
||||
/installroot
|
||||
/.lock-wscript
|
||||
/Meta
|
||||
/patches
|
||||
/releases
|
||||
/share/locale
|
||||
/test/tmp
|
||||
/tags
|
||||
.DS_Store
|
||||
.noseids
|
||||
*.py[co]
|
||||
*.qm
|
||||
*.swp
|
||||
*~
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
Copyright (C) 2009 David Aguilar (davvid@gmail.com) and contributors
|
||||
Copyright (C) 2007, 2008, 2009, 2010, 2011
|
||||
David Aguilar <davvid@gmail.com> and contributors
|
||||
|
||||
@@ -7,7 +7,7 @@ ACTIVE INGREDIENTS
|
||||
Python 2.4 or newer
|
||||
~ http://python.org/
|
||||
|
||||
PyQt 4.3 or newer
|
||||
PyQt 4.1 or newer
|
||||
~ http://www.riverbankcomputing.co.uk/software/pyqt/download
|
||||
|
||||
|
||||
@@ -27,23 +27,48 @@ If you want to do a global install you can do
|
||||
|
||||
# make prefix=/usr install
|
||||
|
||||
The "install" target will by default include copies of
|
||||
simplejson and jsonpickle. If your system already has these
|
||||
modules installed then you can specify standalone=true when running "make":
|
||||
|
||||
$ make prefix=/usr standalone=true install
|
||||
|
||||
|
||||
You don't need to install cola to run it.
|
||||
git-cola is designed to run directly out of its source tree.
|
||||
|
||||
$ make
|
||||
$ bin/git-cola
|
||||
$ bin/git-dag
|
||||
|
||||
|
||||
NOW AVAILABLE TO RESIDENTS OF REDMOND, WA
|
||||
# the only cola made with real sugar, not high fructose corn syrup
|
||||
MAC OS X INSTALLTION INSTRUCTIONS
|
||||
|
||||
Whether you install cola yourself with "make install" or
|
||||
use the "git-cola.app" bundle, you will need to install
|
||||
Qt and PyQt separately.
|
||||
|
||||
The easiest way to do this is to use brew.
|
||||
|
||||
~ http://mxcl.github.com/homebrew/
|
||||
$ brew install git-cola
|
||||
|
||||
Once brew has installed git-cola (and its dependencies) you use
|
||||
git-cola.app or install from source using "make install".
|
||||
Installing these packages also gives you a PyQt development
|
||||
environment which can be used for building your own applications
|
||||
or hacking on cola itself.
|
||||
|
||||
|
||||
WINDOWS INSTALLATION
|
||||
|
||||
Download the latest stable Git, Python 2.6, and Py26-PyQt4 installers
|
||||
http://code.google.com/p/msysgit/
|
||||
http://python.org/download/
|
||||
http://www.riverbankcomputing.co.uk/software/pyqt/download
|
||||
|
||||
Download the latest Cola-[version].exe installer
|
||||
http://cola.tuxfamily.org/releases/win32/
|
||||
Download the latest git-cola-[version].exe Windows installer
|
||||
https://github.com/git-cola/git-cola/downloads
|
||||
|
||||
Once it's installed you can run it from the Start menu or
|
||||
from a Git Bash by typing 'cola'.
|
||||
@@ -55,23 +80,6 @@ tree by using the 'win32/cola' shell script.
|
||||
$ win32/cola
|
||||
|
||||
Note: win32/cola assumes you've installed Python in /c/Python26 or /c/Python25.
|
||||
Adjust accordingly. You can also add python to your path and simply bin/git-cola.
|
||||
Adjust accordingly.
|
||||
|
||||
|
||||
ABOUT THE VARIOUS COLA FLAVORS
|
||||
|
||||
A cola-*-src tarball does not include any generated files --
|
||||
it is purely a "source" release and includes everything needed
|
||||
to generate a cola release. It is basically an export of git-cola.git.
|
||||
~ http://cola.tuxfamily.org/releases/.
|
||||
|
||||
A cola-*-release tarball contains all of the generated gui files and is
|
||||
designed to be run as-is. These are provided for convenience so that
|
||||
users are not required to have the PyQt4 development tools installed in
|
||||
order to use cola.
|
||||
~ http://cola.tuxfamily.org/releases/release/.
|
||||
|
||||
A cola-*-win32 tarball contains extra scripts for use on Windows.
|
||||
It's easier to just use the .exe installer these days, but if you're
|
||||
paranoid and would rather just download a tarball then that's cool too.
|
||||
~ http://cola.tuxfamily.org/releases/win32/.
|
||||
You can also add python to your path and run bin/git-cola directly.
|
||||
|
||||
+14
-16
@@ -1,42 +1,40 @@
|
||||
include bin/git-cola
|
||||
include bin/difftool/*
|
||||
include cola/models/*.py
|
||||
include bin/git-dag
|
||||
include cola/*.py
|
||||
include cola/dag/*.py
|
||||
include cola/classic/*.py
|
||||
include cola/controllers/*.py
|
||||
include cola/main/*.py
|
||||
include cola/merge/*.py
|
||||
include cola/models/*.py
|
||||
include cola/prefs/*.py
|
||||
include cola/stash/*.py
|
||||
include cola/views/*.py
|
||||
include cola/widgets/*.py
|
||||
include darwin/*
|
||||
include jsonpickle/LICENSE
|
||||
include jsonpickle/*.py
|
||||
include jsonpickle/tests/*.py
|
||||
include simplejson/LICENSE
|
||||
include simplejson/*.py
|
||||
include simplejson/tests/*.py
|
||||
include test/*.py
|
||||
include test/Makefile
|
||||
include README
|
||||
include ChangeLog
|
||||
include Makefile
|
||||
include INSTALL
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include po/*.po
|
||||
include share/applications/*.desktop
|
||||
include share/doc/git-cola/.gitignore
|
||||
include share/doc/git-cola/*.rst
|
||||
include share/doc/git-cola/relnotes/*.rst
|
||||
include share/doc/git-cola/models/*.rst
|
||||
include share/doc/git-cola/controllers/*.rst
|
||||
include share/doc/git-cola/views/*.rst
|
||||
include share/doc/git-cola/images/*.png
|
||||
include share/doc/git-cola/_build/.gitignore
|
||||
include share/doc/git-cola/_static/.gitignore
|
||||
include share/doc/git-cola/_static/*.png
|
||||
include share/doc/git-cola/_templates/.gitignore
|
||||
include share/doc/git-cola/*.txt
|
||||
include share/doc/git-cola/*.conf
|
||||
include share/doc/git-cola/Makefile
|
||||
include share/doc/git-cola/Makefile.asciidoc
|
||||
include share/doc/git-cola/build-docdep.perl
|
||||
include share/doc/git-cola/callouts.xsl
|
||||
include share/git-cola/bin/*
|
||||
include share/git-cola/qm/.gitignore
|
||||
include share/git-cola/po/*.po
|
||||
include share/git-cola/icons/*.png
|
||||
include share/git-cola/styles/*.qss
|
||||
include share/git-cola/styles/images/*.png
|
||||
include win32/*
|
||||
|
||||
@@ -1,45 +1,66 @@
|
||||
prefix ?= $(HOME)
|
||||
DESTDIR ?= /
|
||||
PYTHON ?= python
|
||||
PYTHON_VER ?= $(shell $(PYTHON) -c 'import platform; print platform.python_version()[:3]')
|
||||
PYTHON_SITE ?= $(DESTDIR)$(prefix)/lib/python$(PYTHON_VER)/site-packages
|
||||
COLA_VER ?= $(shell git describe --abbrev=4 --match='v*.*')
|
||||
APP ?= git-cola.app
|
||||
APPZIP ?= $(shell darwin/name-tarball.py)
|
||||
# The default target of this Makefile is...
|
||||
all::
|
||||
|
||||
all:
|
||||
# The external commands used by this Makefile are...
|
||||
GIT = git
|
||||
NOSETESTS = nosetests
|
||||
PYTHON = python
|
||||
TAR = tar
|
||||
|
||||
# These values can be overridden on the command-line or via config.mak
|
||||
prefix = $(HOME)
|
||||
bindir = $(prefix)/bin
|
||||
# DESTDIR =
|
||||
|
||||
cola_app = $(CURDIR)/git-cola.app
|
||||
cola_version = $(shell env TERM=dummy $(PYTHON) cola/version.py)
|
||||
cola_dist := cola-$(cola_version)
|
||||
|
||||
python_path = $(CURDIR):$(CURDIR)/thirdparty:$(PYTHONPATH)
|
||||
python_version = $(shell env TERM=dummy $(PYTHON) -c 'import distutils.sysconfig as sc; print(sc.get_python_version())')
|
||||
python_site := $(prefix)/lib*/python$(python_version)/site-packages
|
||||
|
||||
test_flags =
|
||||
all_test_flags = --with-doctest $(test_flags)
|
||||
|
||||
# User customizations
|
||||
-include config.mak
|
||||
|
||||
ifdef standalone
|
||||
standalone_args ?= --standalone
|
||||
endif
|
||||
|
||||
|
||||
all::
|
||||
$(PYTHON) setup.py build
|
||||
|
||||
darwin: all
|
||||
$(PYTHON) darwin/py2app-setup.py py2app
|
||||
|
||||
$(APP): darwin
|
||||
rm -rf $(APP)
|
||||
mv dist/$(APP) $(CURDIR)
|
||||
find $(APP) -name '*_debug*' | xargs rm -f
|
||||
tar cjf $(APPZIP) $(APP)
|
||||
|
||||
install:
|
||||
$(PYTHON) setup.py install \
|
||||
--quiet \
|
||||
install: all
|
||||
$(PYTHON) setup.py --quiet install \
|
||||
$(standalone_args) \
|
||||
--prefix=$(DESTDIR)$(prefix) \
|
||||
--install-scripts=$(DESTDIR)$(bindir) \
|
||||
--force && \
|
||||
rm -f $(PYTHON_SITE)/git_cola* && \
|
||||
(test -d $(PYTHON_SITE) && rmdir -p $(PYTHON_SITE) 2>/dev/null || true) && \
|
||||
(cd $(DESTDIR)$(prefix)/bin && \
|
||||
((! test -e cola && ln -s git-cola cola) || true))
|
||||
rm -f $(DESTDIR)$(python_site)/git_cola*
|
||||
rmdir -p $(DESTDIR)$(python_site) 2>/dev/null || true
|
||||
(cd $(DESTDIR)$(bindir) && \
|
||||
! test -e cola && ln -s git-cola cola) || true
|
||||
|
||||
# Maintainer's dist target
|
||||
dist:
|
||||
$(GIT) archive --format=tar --prefix=$(cola_dist)/ HEAD^{tree} | \
|
||||
gzip -f -9 - >$(cola_dist).tar.gz
|
||||
|
||||
doc:
|
||||
$(MAKE) -C share/doc/git-cola all
|
||||
$(MAKE) -C share/doc/git-cola prefix=$(prefix) all
|
||||
|
||||
html:
|
||||
$(MAKE) -C share/doc/git-cola html
|
||||
$(MAKE) -C share/doc/git-cola prefix=$(prefix) html
|
||||
|
||||
install-doc:
|
||||
$(MAKE) -C share/doc/git-cola install
|
||||
$(MAKE) -C share/doc/git-cola prefix=$(prefix) install
|
||||
|
||||
install-html:
|
||||
$(MAKE) -C share/doc/git-cola install-html
|
||||
$(MAKE) -C share/doc/git-cola prefix=$(prefix) install-html
|
||||
|
||||
uninstall:
|
||||
rm -rf $(DESTDIR)$(prefix)/bin/git-cola \
|
||||
@@ -48,26 +69,39 @@ uninstall:
|
||||
$(DESTDIR)$(prefix)/share/git-cola \
|
||||
$(DESTDIR)$(prefix)/share/doc/git-cola
|
||||
|
||||
test_flags ?=
|
||||
all_test_flags ?= --with-doctest $(test_flags)
|
||||
|
||||
test:
|
||||
@env PYTHONPATH="$(CURDIR)":"$(PYTHONPATH)" \
|
||||
nosetests $(all_test_flags)
|
||||
test: all
|
||||
@env PYTHONPATH=$(python_path) \
|
||||
$(NOSETESTS) $(all_test_flags)
|
||||
|
||||
coverage:
|
||||
@env PYTHONPATH=$(CURDIR):$(PYTHONPATH) \
|
||||
nosetests $(all_test_flags) \
|
||||
--with-coverage --cover-package=cola
|
||||
@env PYTHONPATH=$(python_path) \
|
||||
$(NOSETESTS) --with-coverage --cover-package=cola $(all_test_flags)
|
||||
|
||||
clean:
|
||||
$(MAKE) -C share/doc/git-cola clean
|
||||
find . -name .noseids -print0 | xargs -0 rm -f
|
||||
find . -name '*.py[co]' -print0 | xargs -0 rm -f
|
||||
find share -name '*.qm' -print0 | xargs -0 rm -f
|
||||
rm -rf cola/builtin_version.* build dist tmp tags
|
||||
rm -rf build dist tmp tags git-cola.app
|
||||
rm -rf share/locale
|
||||
|
||||
tags:
|
||||
ctags cola/*.py cola/*/*.py
|
||||
find . -name '*.py' -print0 | xargs -0 ctags -f tags
|
||||
|
||||
.PHONY: all install doc install-doc install-html test clean darwin git-cola.app
|
||||
pot:
|
||||
$(PYTHON) setup.py build_pot -N -d .
|
||||
|
||||
mo:
|
||||
$(PYTHON) setup.py build_mo -f
|
||||
|
||||
git-cola.app:
|
||||
mkdir -p $(cola_app)/Contents/MacOS
|
||||
cp darwin/git-cola $(cola_app)/Contents/MacOS
|
||||
cp darwin/Info.plist darwin/PkgInfo $(cola_app)/Contents
|
||||
$(MAKE) prefix=$(cola_app)/Contents/Resources install install-doc
|
||||
cp darwin/git-cola.icns $(cola_app)/Contents/Resources
|
||||
|
||||
app-tarball: git-cola.app
|
||||
$(TAR) czf git-cola-$(cola_version).app.tar.gz $(cola_app)
|
||||
|
||||
.PHONY: all install doc install-doc install-html test clean tags
|
||||
.PHONY: git-cola.app app-tarball
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
git-cola: a highly caffeinated git gui
|
||||
git-cola: A highly caffeinated git GUI
|
||||
|
||||
cola is a sweet, carbonated git gui known for its
|
||||
sugary flavour and caffeine-inspired features.
|
||||
git-cola is a simple, powerful, and feature-rich git GUI.
|
||||
|
||||
|
||||
Copyright (C) 2009 David Aguilar and contributors
|
||||
Copyright (C) 2007-2012, David Aguilar and contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -19,40 +17,33 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
REQUIREMENTS
|
||||
------------
|
||||
~ git version 1.6.3 or newer
|
||||
~ Python 2.4 or newer
|
||||
~ PyQt 4.4 or newer
|
||||
|
||||
INSTALLATION
|
||||
FORK
|
||||
----
|
||||
$ git clone git://github.com/git-cola/git-cola.git
|
||||
$ xdg-open https://github.com/git-cola/git-cola
|
||||
|
||||
See the 'INSTALL' file for more details.
|
||||
DOWNLOAD
|
||||
--------
|
||||
~ http://git-cola.github.com/
|
||||
|
||||
DISCUSS
|
||||
-------
|
||||
~ http://groups.google.com/group/git-cola/
|
||||
|
||||
THE WEBSITE
|
||||
|
||||
Visit the git-cola home page:
|
||||
~ http://cola.tuxfamily.org/
|
||||
|
||||
|
||||
THE MAILING LIST
|
||||
|
||||
Visit us on google groups:
|
||||
~ http://groups.google.com/group/git-cola/
|
||||
|
||||
Feel free to join, suggest features, and submit patches with
|
||||
git-send-mail.
|
||||
|
||||
|
||||
THE BUG TRACKER
|
||||
|
||||
We use the github issue tracker. It's simple and it works.
|
||||
~ http://github.com/davvid/git-cola/issues
|
||||
|
||||
We do prefer to discuss things on the mailing list, though,
|
||||
so don't be shy.
|
||||
Join, suggest features, and send patches with 'git send-mail'.
|
||||
|
||||
SUPPORT
|
||||
-------
|
||||
~ http://github.com/git-cola/git-cola/issues
|
||||
|
||||
ACKNOWLEDGEMENTS
|
||||
|
||||
git-cola includes a copy of the following modules:
|
||||
|
||||
GitPython ~ http://gitorious.org/projects/git-python/
|
||||
jsonpickle ~ http://code.google.com/p/jsonpickle/
|
||||
----------------
|
||||
git-cola includes the following modules:
|
||||
jsonpickle ~ https://github.com/jsonpickle/jsonpickle
|
||||
simplejson ~ http://code.google.com/p/simplejson/
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
git-difftool originated in git-cola.
|
||||
|
||||
It first appeared in git.git's contrib/ area in January 2009
|
||||
after it was rewritten as a GIT_EXTERNAL_DIFF compatible script.
|
||||
|
||||
git-difftool became an official git command in April 2009 with the
|
||||
release of git 1.6.3. setup.py checks for git 1.6.3 and installs
|
||||
git-difftool when an older git version is detected.
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
# Copyright (c) 2009 David Aguilar
|
||||
#
|
||||
# This is a wrapper around the GIT_EXTERNAL_DIFF-compatible
|
||||
# git-difftool--helper script.
|
||||
#
|
||||
# This script exports GIT_EXTERNAL_DIFF and GIT_PAGER for use by git.
|
||||
# GIT_DIFFTOOL_NO_PROMPT, GIT_DIFFTOOL_PROMPT, and GIT_DIFF_TOOL
|
||||
# are exported for use by git-difftool--helper.
|
||||
#
|
||||
# Any arguments that are unknown to this script are forwarded to 'git diff'.
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use Cwd qw(abs_path);
|
||||
use File::Basename qw(dirname);
|
||||
|
||||
my $DIR = abs_path(dirname($0));
|
||||
|
||||
|
||||
sub usage
|
||||
{
|
||||
print << 'USAGE';
|
||||
usage: git difftool [--tool=<tool>] [-y|--no-prompt] ["git diff" options]
|
||||
USAGE
|
||||
exit 1;
|
||||
}
|
||||
|
||||
sub setup_environment
|
||||
{
|
||||
$ENV{PATH} = "$DIR:$ENV{PATH}";
|
||||
$ENV{GIT_PAGER} = '';
|
||||
$ENV{GIT_EXTERNAL_DIFF} = 'git-difftool--helper';
|
||||
}
|
||||
|
||||
sub exe
|
||||
{
|
||||
my $exe = shift;
|
||||
if ($^O eq 'MSWin32' || $^O eq 'msys') {
|
||||
return "$exe.exe";
|
||||
}
|
||||
return $exe;
|
||||
}
|
||||
|
||||
sub generate_command
|
||||
{
|
||||
my @command = (exe('git'), 'diff');
|
||||
my $skip_next = 0;
|
||||
my $idx = -1;
|
||||
for my $arg (@ARGV) {
|
||||
$idx++;
|
||||
if ($skip_next) {
|
||||
$skip_next = 0;
|
||||
next;
|
||||
}
|
||||
if ($arg eq '-t' || $arg eq '--tool') {
|
||||
usage() if $#ARGV <= $idx;
|
||||
$ENV{GIT_DIFF_TOOL} = $ARGV[$idx + 1];
|
||||
$skip_next = 1;
|
||||
next;
|
||||
}
|
||||
if ($arg =~ /^--tool=/) {
|
||||
$ENV{GIT_DIFF_TOOL} = substr($arg, 7);
|
||||
next;
|
||||
}
|
||||
if ($arg eq '-y' || $arg eq '--no-prompt') {
|
||||
$ENV{GIT_DIFFTOOL_NO_PROMPT} = 'true';
|
||||
delete $ENV{GIT_DIFFTOOL_PROMPT};
|
||||
next;
|
||||
}
|
||||
if ($arg eq '--prompt') {
|
||||
$ENV{GIT_DIFFTOOL_PROMPT} = 'true';
|
||||
delete $ENV{GIT_DIFFTOOL_NO_PROMPT};
|
||||
next;
|
||||
}
|
||||
if ($arg eq '-h' || $arg eq '--help') {
|
||||
usage();
|
||||
}
|
||||
push @command, $arg;
|
||||
}
|
||||
return @command
|
||||
}
|
||||
|
||||
setup_environment();
|
||||
exec(generate_command());
|
||||
@@ -1,227 +0,0 @@
|
||||
#!/bin/sh
|
||||
# git-difftool--helper is a GIT_EXTERNAL_DIFF-compatible diff tool launcher.
|
||||
# This script is typically launched by using the 'git difftool'
|
||||
# convenience command.
|
||||
#
|
||||
# Copyright (c) 2009 David Aguilar
|
||||
|
||||
# difftool.prompt controls the default prompt/no-prompt behavior
|
||||
# and is overridden with $GIT_DIFFTOOL*_PROMPT.
|
||||
should_prompt () {
|
||||
prompt=$(git config --bool difftool.prompt || echo true)
|
||||
if test "$prompt" = true; then
|
||||
test -z "$GIT_DIFFTOOL_NO_PROMPT"
|
||||
else
|
||||
test -n "$GIT_DIFFTOOL_PROMPT"
|
||||
fi
|
||||
}
|
||||
|
||||
# This function prepares temporary files and launches the appropriate
|
||||
# merge tool.
|
||||
launch_merge_tool () {
|
||||
# Merged is the filename as it appears in the work tree
|
||||
# Local is the contents of a/filename
|
||||
# Remote is the contents of b/filename
|
||||
# Custom merge tool commands might use $BASE so we provide it
|
||||
MERGED="$1"
|
||||
LOCAL="$2"
|
||||
REMOTE="$3"
|
||||
BASE="$1"
|
||||
|
||||
# $LOCAL and $REMOTE are temporary files so prompt
|
||||
# the user with the real $MERGED name before launching $merge_tool.
|
||||
if should_prompt; then
|
||||
printf "\nViewing: '$MERGED'\n"
|
||||
printf "Hit return to launch '%s': " "$merge_tool"
|
||||
read ans
|
||||
fi
|
||||
|
||||
# Run the appropriate merge tool command
|
||||
case "$merge_tool" in
|
||||
kdiff3)
|
||||
basename=$(basename "$MERGED")
|
||||
"$merge_tool_path" --auto \
|
||||
--L1 "$basename (A)" \
|
||||
--L2 "$basename (B)" \
|
||||
"$LOCAL" "$REMOTE" \
|
||||
> /dev/null 2>&1
|
||||
;;
|
||||
|
||||
kompare)
|
||||
"$merge_tool_path" "$LOCAL" "$REMOTE"
|
||||
;;
|
||||
|
||||
tkdiff)
|
||||
"$merge_tool_path" "$LOCAL" "$REMOTE"
|
||||
;;
|
||||
|
||||
meld)
|
||||
"$merge_tool_path" "$LOCAL" "$REMOTE"
|
||||
;;
|
||||
|
||||
diffuse)
|
||||
"$merge_tool_path" "$LOCAL" "$REMOTE" | cat
|
||||
;;
|
||||
|
||||
vimdiff)
|
||||
"$merge_tool_path" -d -c "wincmd l" "$LOCAL" "$REMOTE"
|
||||
;;
|
||||
|
||||
gvimdiff)
|
||||
"$merge_tool_path" -d -c "wincmd l" -f "$LOCAL" "$REMOTE"
|
||||
;;
|
||||
|
||||
xxdiff)
|
||||
"$merge_tool_path" \
|
||||
-R 'Accel.Search: "Ctrl+F"' \
|
||||
-R 'Accel.SearchForward: "Ctrl-G"' \
|
||||
"$LOCAL" "$REMOTE"
|
||||
;;
|
||||
|
||||
opendiff)
|
||||
"$merge_tool_path" "$LOCAL" "$REMOTE" | cat
|
||||
;;
|
||||
|
||||
ecmerge)
|
||||
"$merge_tool_path" "$LOCAL" "$REMOTE" \
|
||||
--default --mode=merge2 --to="$MERGED"
|
||||
;;
|
||||
|
||||
emerge)
|
||||
"$merge_tool_path" -f emerge-files-command \
|
||||
"$LOCAL" "$REMOTE" "$(basename "$MERGED")"
|
||||
;;
|
||||
|
||||
*)
|
||||
if test -n "$merge_tool_cmd"; then
|
||||
( eval $merge_tool_cmd )
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Verifies that (difftool|mergetool).<tool>.cmd exists
|
||||
valid_custom_tool() {
|
||||
merge_tool_cmd="$(git config difftool.$1.cmd)"
|
||||
test -z "$merge_tool_cmd" &&
|
||||
merge_tool_cmd="$(git config mergetool.$1.cmd)"
|
||||
test -n "$merge_tool_cmd"
|
||||
}
|
||||
|
||||
# Verifies that the chosen merge tool is properly setup.
|
||||
# Built-in merge tools are always valid.
|
||||
valid_tool() {
|
||||
case "$1" in
|
||||
kdiff3 | kompare | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | ecmerge)
|
||||
;; # happy
|
||||
*)
|
||||
if ! valid_custom_tool "$1"
|
||||
then
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Sets up the merge_tool_path variable.
|
||||
# This handles the difftool.<tool>.path configuration.
|
||||
# This also falls back to mergetool defaults.
|
||||
init_merge_tool_path() {
|
||||
merge_tool_path=$(git config difftool."$1".path)
|
||||
test -z "$merge_tool_path" &&
|
||||
merge_tool_path=$(git config mergetool."$1".path)
|
||||
if test -z "$merge_tool_path"; then
|
||||
case "$1" in
|
||||
vimdiff)
|
||||
merge_tool_path=vim
|
||||
;;
|
||||
gvimdiff)
|
||||
merge_tool_path=gvim
|
||||
;;
|
||||
emerge)
|
||||
merge_tool_path=emacs
|
||||
;;
|
||||
*)
|
||||
merge_tool_path="$1"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
# Allow GIT_DIFF_TOOL and GIT_MERGE_TOOL to provide default values
|
||||
test -n "$GIT_MERGE_TOOL" && merge_tool="$GIT_MERGE_TOOL"
|
||||
test -n "$GIT_DIFF_TOOL" && merge_tool="$GIT_DIFF_TOOL"
|
||||
|
||||
# If merge tool was not specified then use the diff.tool
|
||||
# configuration variable. If that's invalid then reset merge_tool.
|
||||
# Fallback to merge.tool.
|
||||
if test -z "$merge_tool"; then
|
||||
merge_tool=$(git config diff.tool)
|
||||
test -z "$merge_tool" &&
|
||||
merge_tool=$(git config merge.tool)
|
||||
if test -n "$merge_tool" && ! valid_tool "$merge_tool"; then
|
||||
echo >&2 "git config option diff.tool set to unknown tool: $merge_tool"
|
||||
echo >&2 "Resetting to default..."
|
||||
unset merge_tool
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try to guess an appropriate merge tool if no tool has been set.
|
||||
if test -z "$merge_tool"; then
|
||||
# We have a $DISPLAY so try some common UNIX merge tools
|
||||
if test -n "$DISPLAY"; then
|
||||
# If gnome then prefer meld, otherwise, prefer kdiff3 or kompare
|
||||
if test -n "$GNOME_DESKTOP_SESSION_ID" ; then
|
||||
merge_tool_candidates="meld kdiff3 kompare tkdiff xxdiff gvimdiff diffuse"
|
||||
else
|
||||
merge_tool_candidates="kdiff3 kompare tkdiff xxdiff meld gvimdiff diffuse"
|
||||
fi
|
||||
fi
|
||||
if echo "${VISUAL:-$EDITOR}" | grep 'emacs' > /dev/null 2>&1; then
|
||||
# $EDITOR is emacs so add emerge as a candidate
|
||||
merge_tool_candidates="$merge_tool_candidates emerge opendiff vimdiff"
|
||||
elif echo "${VISUAL:-$EDITOR}" | grep 'vim' > /dev/null 2>&1; then
|
||||
# $EDITOR is vim so add vimdiff as a candidate
|
||||
merge_tool_candidates="$merge_tool_candidates vimdiff opendiff emerge"
|
||||
else
|
||||
merge_tool_candidates="$merge_tool_candidates opendiff emerge vimdiff"
|
||||
fi
|
||||
echo "merge tool candidates: $merge_tool_candidates"
|
||||
|
||||
# Loop over each candidate and stop when a valid merge tool is found.
|
||||
for i in $merge_tool_candidates
|
||||
do
|
||||
init_merge_tool_path $i
|
||||
if type "$merge_tool_path" > /dev/null 2>&1; then
|
||||
merge_tool=$i
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if test -z "$merge_tool" ; then
|
||||
echo "No known merge resolution program available."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
else
|
||||
# A merge tool has been set, so verify that it's valid.
|
||||
if ! valid_tool "$merge_tool"; then
|
||||
echo >&2 "Unknown merge tool $merge_tool"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
init_merge_tool_path "$merge_tool"
|
||||
|
||||
if test -z "$merge_tool_cmd" && ! type "$merge_tool_path" > /dev/null 2>&1; then
|
||||
echo "The merge tool $merge_tool is not available as '$merge_tool_path'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# Launch the merge tool on each path provided by 'git diff'
|
||||
while test $# -gt 6
|
||||
do
|
||||
launch_merge_tool "$1" "$2" "$5"
|
||||
shift 7
|
||||
done
|
||||
+15
-24
@@ -4,7 +4,8 @@
|
||||
"""
|
||||
|
||||
__copyright__ = """
|
||||
Copyright (C) 2007, 2008, 2009 David Aguilar <davvid@gmail.com> and contributors
|
||||
Copyright (C) 2007, 2008, 2009, 2010,
|
||||
David Aguilar <davvid@gmail.com> and contributors
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License version 2 as
|
||||
@@ -15,9 +16,6 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -27,28 +25,21 @@ def cola_init():
|
||||
"""Provides access to the cola modules"""
|
||||
# Try to detect where it is run from and set prefix and the search path.
|
||||
# It is assumed that the user installed Cola using the --prefix= option
|
||||
prefix = os.path.dirname(os.path.abspath(__file__))
|
||||
if 'Contents/Resources' not in __file__:
|
||||
prefix = os.path.dirname(prefix)
|
||||
|
||||
# Look for modules in the source and install trees
|
||||
paths_to_try = [prefix, os.path.join(prefix, 'share', 'git-cola', 'lib')]
|
||||
actual_paths = [p for p in paths_to_try if os.path.exists(p)]
|
||||
sys.path = actual_paths + sys.path
|
||||
|
||||
import cola.resources
|
||||
|
||||
# Setup the path so that git finds us when we run 'git cola'
|
||||
path_entries = os.environ.get('PATH').split(os.pathsep)
|
||||
bindir = os.path.dirname(os.path.abspath(__file__))
|
||||
path_entries.insert(0, bindir)
|
||||
path = os.pathsep.join(path_entries)
|
||||
os.environ['PATH'] = path
|
||||
os.putenv('PATH', path)
|
||||
prefix = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Look for modules in the source or install trees
|
||||
thirdparty = os.path.join(prefix, 'thirdparty')
|
||||
if os.path.exists(thirdparty):
|
||||
# Source tree: prefer cola's modules but allow system versions of thirdparty
|
||||
sys.path.insert(0, prefix)
|
||||
sys.path.append(thirdparty)
|
||||
else:
|
||||
# Install tree: prefer cola
|
||||
paths_to_try = [os.path.join(prefix, 'share', 'git-cola', 'lib')]
|
||||
sys.path = paths_to_try + sys.path
|
||||
|
||||
if __name__ == '__main__':
|
||||
# lights, cameras, action
|
||||
cola_init()
|
||||
import cola.main
|
||||
cola.main.main()
|
||||
import cola.app
|
||||
cola.app.main('git-cola')
|
||||
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- python-mode -*-
|
||||
"""git-dag: An advanced git DAG visualizer
|
||||
"""
|
||||
|
||||
__copyright__ = """
|
||||
Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012
|
||||
David Aguilar <davvid@gmail.com> and contributors
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License version 2 as
|
||||
published by the Free Software Foundation.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
def cola_init():
|
||||
"""Provides access to the cola modules"""
|
||||
# Try to detect where it is run from and set prefix and the search path.
|
||||
# It is assumed that the user installed Cola using the --prefix= option
|
||||
prefix = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Look for modules in the source or install trees
|
||||
thirdparty = os.path.join(prefix, 'thirdparty')
|
||||
if os.path.exists(thirdparty):
|
||||
# Source tree: prefer cola's modules but allow system versions of thirdparty
|
||||
sys.path.insert(0, prefix)
|
||||
sys.path.append(thirdparty)
|
||||
else:
|
||||
# Install tree: prefer cola
|
||||
paths_to_try = [os.path.join(prefix, 'share', 'git-cola', 'lib')]
|
||||
sys.path = paths_to_try + sys.path
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# lights, cameras, action
|
||||
cola_init()
|
||||
import cola.app
|
||||
cola.app.main('git-dag')
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
from cola.cmdfactory import factory
|
||||
from cola.models.main import model
|
||||
from cola.main.model import model
|
||||
from cola.models.selection import selection
|
||||
from cola.models.selection import selection_model
|
||||
from cola.models.selection import single_selection
|
||||
|
||||
+274
-23
@@ -1,15 +1,50 @@
|
||||
# Copyright (C) 2009, David Aguilar <davvid@gmail.com>
|
||||
"""Provides the cola QApplication subclass"""
|
||||
# style note: we use camelCase here since we're masquerading a Qt class
|
||||
"""Provides the main() routine and ColaApplicaiton"""
|
||||
|
||||
import glob
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
|
||||
from cola import utils
|
||||
from cola import git
|
||||
from cola import i18n
|
||||
from cola import inotify
|
||||
from cola import resources
|
||||
from cola import signals
|
||||
from cola import version
|
||||
from cola.decorators import memoize
|
||||
|
||||
|
||||
def setup_environment():
|
||||
# Spoof an X11 display for SSH
|
||||
os.environ.setdefault('DISPLAY', ':0')
|
||||
|
||||
# Provide an SSH_ASKPASS fallback
|
||||
if sys.platform == 'darwin':
|
||||
os.environ.setdefault('SSH_ASKPASS',
|
||||
resources.share('bin', 'ssh-askpass-darwin'))
|
||||
else:
|
||||
os.environ.setdefault('SSH_ASKPASS',
|
||||
resources.share('bin', 'ssh-askpass'))
|
||||
|
||||
# Setup the path so that git finds us when we run 'git cola'
|
||||
path_entries = os.environ.get('PATH').split(os.pathsep)
|
||||
bindir = os.path.dirname(os.path.abspath(__file__))
|
||||
path_entries.insert(0, bindir)
|
||||
path = os.pathsep.join(path_entries)
|
||||
os.environ['PATH'] = path
|
||||
os.putenv('PATH', path)
|
||||
|
||||
|
||||
@memoize
|
||||
def instance(argv):
|
||||
from PyQt4 import QtGui
|
||||
|
||||
return QtGui.QApplication(list(argv))
|
||||
|
||||
|
||||
# style note: we use camelCase here since we're masquerading a Qt class
|
||||
class ColaApplication(object):
|
||||
"""The main cola application
|
||||
|
||||
@@ -19,9 +54,17 @@ class ColaApplication(object):
|
||||
def __init__(self, argv, locale=None, gui=True):
|
||||
"""Initialize our QApplication for translation
|
||||
"""
|
||||
# monkey-patch Qt's app translate() to handle .po files
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
|
||||
from cola import cmds
|
||||
from cola import utils
|
||||
|
||||
i18n.install(locale)
|
||||
|
||||
# monkey-patch Qt's translate() to use our translate()
|
||||
if gui:
|
||||
self._app = QtGui.QApplication(argv)
|
||||
self._app = instance(tuple(argv))
|
||||
self._app.setWindowIcon(QtGui.QIcon(resources.icon('git.svg')))
|
||||
self._translate_base = QtGui.QApplication.translate
|
||||
QtGui.QApplication.translate = self.translate
|
||||
@@ -30,21 +73,22 @@ class ColaApplication(object):
|
||||
self._translate_base = QtCore.QCoreApplication.translate
|
||||
QtCore.QCoreApplication.translate = self.translate
|
||||
|
||||
# Find the current language settings and apply them
|
||||
if not locale:
|
||||
locale = str(QtCore.QLocale().system().name())
|
||||
# Register model commands
|
||||
cmds.register()
|
||||
|
||||
qmfile = utils.qm_for_locale(locale)
|
||||
if os.path.exists(qmfile):
|
||||
translator = QtCore.QTranslator(self._app)
|
||||
translator.load(qmfile)
|
||||
self._app.installTranslator(translator)
|
||||
# Make file descriptors binary for win32
|
||||
utils.set_binary(sys.stdin)
|
||||
utils.set_binary(sys.stdout)
|
||||
utils.set_binary(sys.stderr)
|
||||
|
||||
def translate(self, context, *args):
|
||||
"""Supports @@noun/@@verb and context-less translation
|
||||
def translate(self, domain, txt):
|
||||
"""
|
||||
# We set the context to '' to properly handle .qm files
|
||||
trtxt = unicode(self._translate_base('', *args))
|
||||
Translate strings with gettext
|
||||
|
||||
Supports @@noun/@@verb specifiers.
|
||||
|
||||
"""
|
||||
trtxt = i18n.gettext(txt)
|
||||
if trtxt[-6:-4] == '@@': # handle @@verb / @@noun
|
||||
trtxt = trtxt[:-6]
|
||||
return trtxt
|
||||
@@ -57,6 +101,213 @@ class ColaApplication(object):
|
||||
"""Wrap exec_()"""
|
||||
return self._app.exec_()
|
||||
|
||||
def setStyleSheet(self, txt):
|
||||
"""Wrap setStyleSheet(txt)"""
|
||||
return self._app.setStyleSheet(txt)
|
||||
|
||||
def parse_args(context):
|
||||
parser = optparse.OptionParser(usage='%prog [options]')
|
||||
|
||||
# We also accept 'git cola version'
|
||||
parser.add_option('-v', '--version',
|
||||
help='Show cola version',
|
||||
dest='version',
|
||||
default=False,
|
||||
action='store_true')
|
||||
|
||||
# Accept git cola --classic
|
||||
parser.add_option('--classic',
|
||||
help='Launch cola classic',
|
||||
dest='classic',
|
||||
default=False,
|
||||
action='store_true')
|
||||
|
||||
# Specifies a git repository to open
|
||||
parser.add_option('-r', '--repo',
|
||||
help='Specifies the path to a git repository.',
|
||||
dest='repo',
|
||||
metavar='PATH',
|
||||
default=os.getcwd())
|
||||
|
||||
# Specifies that we should prompt for a repository at startup
|
||||
parser.add_option('--prompt',
|
||||
help='Prompt for a repository before starting the main GUI.',
|
||||
dest='prompt',
|
||||
action='store_true',
|
||||
default=False)
|
||||
|
||||
# Used on Windows for adding 'git' to the path
|
||||
parser.add_option('-g', '--git-path',
|
||||
help='Specifies the path to the git binary',
|
||||
dest='git',
|
||||
metavar='PATH',
|
||||
default='')
|
||||
|
||||
if context == 'git-dag':
|
||||
parser.add_option('-c', '--count',
|
||||
help='Number of commits to display.',
|
||||
dest='count',
|
||||
type='int',
|
||||
default=1000)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def process_args(opts, args):
|
||||
if opts.version or (args and args[0] == 'version'):
|
||||
# Accept 'git cola --version' or 'git cola version'
|
||||
print 'cola version', version.version()
|
||||
sys.exit(0)
|
||||
|
||||
if opts.git:
|
||||
# Adds git to the PATH. This is needed on Windows.
|
||||
path_entries = os.environ.get('PATH', '').split(os.pathsep)
|
||||
path_entries.insert(0, os.path.dirname(opts.git))
|
||||
os.environ['PATH'] = os.pathsep.join(path_entries)
|
||||
|
||||
# Bail out if --repo is not a directory
|
||||
repo = os.path.realpath(opts.repo)
|
||||
if not os.path.isdir(repo):
|
||||
print >> sys.stderr, "fatal: '%s' is not a directory. Consider supplying -r <path>.\n" % repo
|
||||
sys.exit(-1)
|
||||
|
||||
# We do everything relative to the repo root
|
||||
os.chdir(opts.repo)
|
||||
|
||||
return repo
|
||||
|
||||
|
||||
def main(context):
|
||||
"""Parses the command-line arguments and starts git-cola
|
||||
"""
|
||||
setup_environment()
|
||||
opts, args = parse_args(context)
|
||||
repo = process_args(opts, args)
|
||||
|
||||
try:
|
||||
# Defer these imports to allow git cola --version without pyqt installed
|
||||
from PyQt4 import QtCore
|
||||
except ImportError:
|
||||
print >> sys.stderr, 'Sorry, you do not seem to have PyQt4 installed.'
|
||||
print >> sys.stderr, 'Please install it before using cola.'
|
||||
print >> sys.stderr, 'e.g.: sudo apt-get install python-qt4'
|
||||
sys.exit(-1)
|
||||
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
# Import cola modules
|
||||
import cola
|
||||
from cola import qtcompat
|
||||
qtcompat.install()
|
||||
|
||||
from cola import guicmds
|
||||
from cola.app import ColaApplication
|
||||
from cola.classic import cola_classic
|
||||
from cola.dag import git_dag
|
||||
from cola.widgets import cfgactions
|
||||
from cola.widgets import startup
|
||||
from cola.main.view import MainView
|
||||
from cola.main.controller import MainController
|
||||
|
||||
# Allow Ctrl-C to exit
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
# Add the default style dir so that we find our icons
|
||||
_setup_resource_dir(resources.icon_dir())
|
||||
|
||||
# Initialize the app
|
||||
app = ColaApplication(sys.argv)
|
||||
|
||||
# Ensure that we're working in a valid git repository.
|
||||
# If not, try to find one. When found, chdir there.
|
||||
model = cola.model()
|
||||
valid = model.use_worktree(repo) and not opts.prompt
|
||||
while not valid:
|
||||
startup_dlg = startup.StartupDialog(app.activeWindow())
|
||||
gitdir = startup_dlg.find_git_repo()
|
||||
if not gitdir:
|
||||
sys.exit(-1)
|
||||
valid = model.use_worktree(gitdir)
|
||||
|
||||
# Finally, go to the root of the git repo
|
||||
os.chdir(model.git.worktree())
|
||||
|
||||
# Show the GUI
|
||||
if opts.classic:
|
||||
view = cola_classic(update=False)
|
||||
elif context == 'git-cola':
|
||||
view = MainView(model)
|
||||
ctl = MainController(model, view)
|
||||
elif context == 'git-dag':
|
||||
ctl = git_dag(model, app.activeWindow(), opts=opts, args=args)
|
||||
view = ctl.view
|
||||
|
||||
# Install UI wrappers for command objects
|
||||
cfgactions.install_command_wrapper(view)
|
||||
guicmds.install_command_wrapper(view)
|
||||
|
||||
# Show the view and start the main event loop
|
||||
view.show()
|
||||
|
||||
# Make sure that we start out on top
|
||||
view.raise_()
|
||||
|
||||
# Scan for the first time
|
||||
task = _start_update_thread(model)
|
||||
|
||||
# Start the inotify thread
|
||||
inotify.start()
|
||||
|
||||
msg_timer = QtCore.QTimer()
|
||||
msg_timer.setSingleShot(True)
|
||||
msg_timer.connect(msg_timer, SIGNAL('timeout()'), _send_msg)
|
||||
msg_timer.start(0)
|
||||
|
||||
# Start the event loop
|
||||
result = app.exec_()
|
||||
|
||||
# All done, cleanup
|
||||
inotify.stop()
|
||||
QtCore.QThreadPool.globalInstance().waitForDone()
|
||||
|
||||
pattern = cola.model().tmp_file_pattern()
|
||||
for filename in glob.glob(pattern):
|
||||
os.unlink(filename)
|
||||
sys.exit(result)
|
||||
|
||||
return ctl, task
|
||||
|
||||
|
||||
def _start_update_thread(model):
|
||||
"""Update the model in the background
|
||||
|
||||
git-cola should startup as quickly as possible.
|
||||
|
||||
"""
|
||||
from PyQt4 import QtCore
|
||||
|
||||
class UpdateTask(QtCore.QRunnable):
|
||||
def run(self):
|
||||
model.update_status(update_index=True)
|
||||
|
||||
# Hold onto a reference to prevent PyQt from dereferencing
|
||||
task = UpdateTask()
|
||||
QtCore.QThreadPool.globalInstance().start(task)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
def _send_msg():
|
||||
import cola
|
||||
git.GIT_COLA_TRACE = os.getenv('GIT_COLA_TRACE', False)
|
||||
if git.GIT_COLA_TRACE:
|
||||
msg = ('info: Trace enabled. '
|
||||
'Many of commands reported with "trace" use git\'s stable '
|
||||
'"plumbing" API and are not intended for typical '
|
||||
'day-to-day use. Here be dragons')
|
||||
cola.notifier().broadcast(signals.log_cmd, 0, msg)
|
||||
|
||||
|
||||
def _setup_resource_dir(dirname):
|
||||
"""Adds resource directories to Qt's search path"""
|
||||
from cola import qtcompat
|
||||
|
||||
basename = os.path.basename(dirname)
|
||||
qtcompat.add_search_path(basename, dirname)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# Copyright (c) 2009 David Aguilar
|
||||
"""Provides a serializable data container"""
|
||||
import types
|
||||
import copy
|
||||
|
||||
from cola.compat import set
|
||||
|
||||
class BaseModel(object):
|
||||
def param_names(self, export=False):
|
||||
"""Returns a list of serializable attribute names.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m._question = 'unknown'
|
||||
>>> m.answer = 42
|
||||
|
||||
>>> m.param_names()
|
||||
['answer']
|
||||
|
||||
>>> m.param_names(export=True)
|
||||
['_question', 'answer']
|
||||
|
||||
"""
|
||||
names = []
|
||||
for k, v in self.__dict__.iteritems():
|
||||
if is_atom(v) or is_dict(v) or is_seq(v):
|
||||
if k.strip('_') != k:
|
||||
if not export:
|
||||
continue
|
||||
names.append(k)
|
||||
names.sort()
|
||||
return names
|
||||
|
||||
def has_param(self,param):
|
||||
"""Returns true if a parameter exists in a model.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> m.has_param('answer')
|
||||
True
|
||||
|
||||
>>> m.has_param('question')
|
||||
False
|
||||
|
||||
"""
|
||||
return param in self.__dict__
|
||||
|
||||
def param(self, param, default=None):
|
||||
"""Returns the value of a model parameter.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> m.param('answer')
|
||||
42
|
||||
|
||||
>>> m.param('another answer', 42)
|
||||
42
|
||||
"""
|
||||
return self.__dict__.get(param, default)
|
||||
|
||||
def __getattr__(self, param):
|
||||
"""Provides set_<attribute>(value) append methods.
|
||||
|
||||
This provides automatic convenience methods for handling
|
||||
setattrs with notification.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> m.set_answer(41)
|
||||
>>> # Observers are notified
|
||||
>>> m.answer
|
||||
41
|
||||
|
||||
"""
|
||||
# Base case: we actually have this param
|
||||
if param in self.__dict__:
|
||||
return getattr(self, param)
|
||||
|
||||
# Return a closure over param for calling set_param;
|
||||
# Concrete classes subclass set_param to provide notification
|
||||
if param.startswith('set_'):
|
||||
return lambda v: self.set_param(param[4:], v)
|
||||
|
||||
errmsg = ("'%s' object has no attribute '%s'" %
|
||||
(self.__class__.__name__, param))
|
||||
raise AttributeError(errmsg)
|
||||
|
||||
def set_param(self, param, value):
|
||||
"""Wrapper around setattr()
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 41
|
||||
>>> m.set_param('answer', 42)
|
||||
>>> m.answer
|
||||
42
|
||||
|
||||
"""
|
||||
setattr(self, param, value)
|
||||
|
||||
def copy_params(self, model, params_to_copy=None):
|
||||
"""Copies params from one model to another.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> m._question = 'unknown'
|
||||
>>> m2 = BaseModel()
|
||||
>>> m2.copy_params(m)
|
||||
>>> m2._question
|
||||
'unknown'
|
||||
|
||||
>>> m2.answer
|
||||
42
|
||||
|
||||
"""
|
||||
# Loop over all attributes and copy them over
|
||||
for k in params_to_copy or model.param_names(export=True):
|
||||
self.__dict__[k] = copy.copy(model.__dict__[k])
|
||||
|
||||
|
||||
def is_dict(item):
|
||||
return type(item) is types.DictType
|
||||
|
||||
|
||||
def is_seq(item):
|
||||
return (type(item) is types.ListType or
|
||||
type(item) is types.TupleType or
|
||||
type(item) is set)
|
||||
|
||||
def is_atom(item):
|
||||
return(item is None or
|
||||
type(item) in types.StringTypes or
|
||||
type(item) is types.BooleanType or
|
||||
type(item) is types.IntType or
|
||||
type(item) is types.LongType or
|
||||
type(item) is types.FloatType or
|
||||
type(item) is types.ComplexType)
|
||||
@@ -0,0 +1,2 @@
|
||||
from cola.classic.controller import cola_classic
|
||||
from cola.classic.controller import widget as classic_widget
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
@@ -7,25 +6,31 @@ import cola
|
||||
import cola.utils
|
||||
import cola.difftool
|
||||
|
||||
from cola import gitcmds
|
||||
from cola import qtutils
|
||||
from cola import signals
|
||||
from cola.controllers.selectcommits import select_commits
|
||||
from cola.models import gitrepo
|
||||
from cola.views import repo
|
||||
from cola.classic.view import RepoDialog
|
||||
from cola.classic.model import GitRepoModel
|
||||
from cola.classic.model import GitRepoEntryManager
|
||||
from cola.compat import set
|
||||
|
||||
|
||||
def widget():
|
||||
def widget(parent=None, update=True):
|
||||
"""Return a widget for immediate use."""
|
||||
view = repo.RepoTreeView()
|
||||
view.setModel(gitrepo.GitRepoModel(view))
|
||||
controller = ClassicController(view)
|
||||
view = RepoDialog(parent=parent, update=update)
|
||||
view.tree.setModel(GitRepoModel(view.tree))
|
||||
controller = ClassicController(view.tree)
|
||||
return view
|
||||
|
||||
|
||||
def cola_classic():
|
||||
def cola_classic(parent=None, update=True):
|
||||
"""Launch a new cola classic session."""
|
||||
view = widget()
|
||||
view.raise_()
|
||||
view = widget(parent=parent, update=update)
|
||||
if not parent:
|
||||
qtutils.center_on_screen(view)
|
||||
view.show()
|
||||
return view
|
||||
|
||||
|
||||
class ClassicController(QtCore.QObject):
|
||||
@@ -53,15 +58,16 @@ class ClassicController(QtCore.QObject):
|
||||
if path in self.updated:
|
||||
return
|
||||
self.updated.add(path)
|
||||
gitrepo.GitRepoEntryManager.entry(path).update()
|
||||
GitRepoEntryManager.entry(path).update()
|
||||
entry = GitRepoEntryManager.entry
|
||||
for row in xrange(item.rowCount()):
|
||||
path = item.child(row, 0).path
|
||||
gitrepo.GitRepoEntryManager.entry(path).update()
|
||||
entry(path).update()
|
||||
|
||||
def difftool_predecessor(self, paths):
|
||||
"""Prompt for an older commit and launch difftool against it."""
|
||||
args = ['--'] + paths
|
||||
revs, summaries = self.model.log_helper(all=True, extra_args=args)
|
||||
revs, summaries = gitcmds.log_helper(all=True, extra_args=args)
|
||||
commits = select_commits('Select Previous Version',
|
||||
revs, summaries, multiselect=False)
|
||||
if not commits:
|
||||
@@ -72,7 +78,6 @@ class ClassicController(QtCore.QObject):
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
from PyQt4 import QtGui
|
||||
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
cola_classic()
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from PyQt4 import QtCore
|
||||
@@ -8,9 +7,14 @@ from PyQt4.QtCore import Qt
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
import cola
|
||||
from cola import gitcfg
|
||||
from cola import core
|
||||
from cola import utils
|
||||
from cola import qtutils
|
||||
from cola import version
|
||||
from cola import resources
|
||||
from cola.compat import set
|
||||
|
||||
|
||||
# Custom event type for GitRepoInfoEvents
|
||||
INFO_EVENT_TYPE = QtCore.QEvent.User + 42
|
||||
@@ -29,9 +33,11 @@ class Columns(object):
|
||||
class GitRepoModel(QtGui.QStandardItemModel):
|
||||
"""Provides an interface into a git repository for browsing purposes."""
|
||||
def __init__(self, parent):
|
||||
QtGui.QStandardItem.__init__(self, parent)
|
||||
QtGui.QStandardItemModel.__init__(self, parent)
|
||||
self._interesting_paths = self._get_paths()
|
||||
self._known_paths = set()
|
||||
|
||||
self.connect(self, SIGNAL('updated'), self._updated_callback)
|
||||
model = cola.model()
|
||||
model.add_message_observer(model.message_updated,
|
||||
self._model_updated)
|
||||
@@ -120,6 +126,9 @@ class GitRepoModel(QtGui.QStandardItemModel):
|
||||
|
||||
def _model_updated(self):
|
||||
"""Observes model changes and updates paths accordingly."""
|
||||
self.emit(SIGNAL('updated'))
|
||||
|
||||
def _updated_callback(self):
|
||||
old_paths = self._interesting_paths
|
||||
new_paths = self._get_paths()
|
||||
for path in new_paths.union(old_paths):
|
||||
@@ -154,15 +163,16 @@ class GitRepoModel(QtGui.QStandardItemModel):
|
||||
entries = dirname.split('/')
|
||||
curdir = []
|
||||
parent = self.invisibleRootItem()
|
||||
curdir_append = curdir.append
|
||||
self_add_directory = self.add_directory
|
||||
for entry in entries:
|
||||
curdir.append(entry)
|
||||
curdir_append(entry)
|
||||
path = '/'.join(curdir)
|
||||
if path in direntries:
|
||||
parent = direntries[path]
|
||||
else:
|
||||
grandparent = parent
|
||||
parent_path = '/'.join(curdir[:-1])
|
||||
parent = self.add_directory(grandparent, path)
|
||||
parent = self_add_directory(grandparent, path)
|
||||
direntries[path] = parent
|
||||
return parent
|
||||
|
||||
@@ -178,11 +188,49 @@ class GitRepoEntryManager(object):
|
||||
static_entries = {}
|
||||
|
||||
@classmethod
|
||||
def entry(cls, path):
|
||||
def entry(cls, path, _static_entries=static_entries):
|
||||
"""Return a static instance of a GitRepoEntry."""
|
||||
if path not in cls.static_entries:
|
||||
cls.static_entries[path] = GitRepoEntry(path)
|
||||
return cls.static_entries[path]
|
||||
try:
|
||||
e = _static_entries[path]
|
||||
except KeyError:
|
||||
e = _static_entries[path] = GitRepoEntry(path)
|
||||
return e
|
||||
|
||||
|
||||
class TaskRunner(object):
|
||||
"""Manages QRunnable task instances to avoid python's garbage collector
|
||||
|
||||
When PyQt stops referencing a QRunnable instance Python cleans it up
|
||||
which leads to segfaults, e.g. the dreaded "C++ object has gone away".
|
||||
|
||||
This class keeps track of tasks and cleans up references to them as they
|
||||
complete.
|
||||
|
||||
"""
|
||||
singleton = None
|
||||
|
||||
@classmethod
|
||||
def instance(cls):
|
||||
if cls.singleton is None:
|
||||
cls.singleton = TaskRunner()
|
||||
return cls.singleton
|
||||
|
||||
def __init__(self):
|
||||
self.tasks = set()
|
||||
self.threadpool = QtCore.QThreadPool.globalInstance()
|
||||
self.notifier = QtCore.QObject()
|
||||
self.notifier.connect(self.notifier, SIGNAL('task_done'), self.task_done)
|
||||
|
||||
def run(self, task):
|
||||
self.tasks.add(task)
|
||||
self.threadpool.start(task)
|
||||
|
||||
def task_done(self, task):
|
||||
if task in self.tasks:
|
||||
self.tasks.remove(task)
|
||||
|
||||
def cleanup_task(self, task):
|
||||
self.notifier.emit(SIGNAL('task_done'), task)
|
||||
|
||||
|
||||
class GitRepoEntry(QtCore.QObject):
|
||||
@@ -195,7 +243,6 @@ class GitRepoEntry(QtCore.QObject):
|
||||
def __init__(self, path):
|
||||
QtCore.QObject.__init__(self)
|
||||
self.path = path
|
||||
self.task = None
|
||||
|
||||
def update_name(self):
|
||||
"""Emits a signal corresponding to the entry's name."""
|
||||
@@ -207,9 +254,8 @@ class GitRepoEntry(QtCore.QObject):
|
||||
def update(self):
|
||||
"""Starts a GitRepoInfoTask to calculate info for entries."""
|
||||
# GitRepoInfoTask handles expensive lookups
|
||||
threadpool = QtCore.QThreadPool.globalInstance()
|
||||
self.task = GitRepoInfoTask(self.path)
|
||||
threadpool.start(self.task)
|
||||
task = GitRepoInfoTask(self.path)
|
||||
TaskRunner.instance().run(task)
|
||||
|
||||
def event(self, e):
|
||||
"""Receive GitRepoInfoEvents and emit corresponding Qt signals."""
|
||||
@@ -220,11 +266,19 @@ class GitRepoEntry(QtCore.QObject):
|
||||
return QtCore.QObject.event(self, e)
|
||||
|
||||
|
||||
class GitRepoInfoTask(QtCore.QRunnable):
|
||||
# Support older versions of PyQt
|
||||
if version.check('pyqt_qrunnable', QtCore.PYQT_VERSION_STR):
|
||||
QRunnable = QtCore.QRunnable
|
||||
else:
|
||||
class QRunnable(object):
|
||||
pass
|
||||
|
||||
class GitRepoInfoTask(QRunnable):
|
||||
"""Handles expensive git lookups for a path."""
|
||||
def __init__(self, path):
|
||||
QtCore.QRunnable.__init__(self)
|
||||
QRunnable.__init__(self)
|
||||
self.path = path
|
||||
self._cfg = gitcfg.instance()
|
||||
self._data = {}
|
||||
|
||||
def data(self, key):
|
||||
@@ -238,18 +292,18 @@ class GitRepoInfoTask(QtCore.QRunnable):
|
||||
log_line = cola.model().git.log('-1', '--', self.path,
|
||||
M=True,
|
||||
all=True,
|
||||
pretty='format:%ar/%s/%an')
|
||||
no_color=True,
|
||||
pretty='format:%ar%x01%s%x01%an')
|
||||
if log_line:
|
||||
log_line = core.decode(log_line)
|
||||
date, rest = log_line.split('/', 1)
|
||||
message, author = rest.rsplit('/', 1)
|
||||
date, message, author = log_line.split(chr(0x01), 2)
|
||||
self._data['date'] = date
|
||||
self._data['message'] = message
|
||||
self._data['author'] = author
|
||||
else:
|
||||
self._data['date'] = self.date()
|
||||
self._data['message'] = '-'
|
||||
self._data['author'] = cola.model().local_user_name
|
||||
self._data['author'] = self._cfg.get('user.name', 'unknown')
|
||||
return self._data[key]
|
||||
|
||||
def name(self):
|
||||
@@ -286,31 +340,39 @@ class GitRepoInfoTask(QtCore.QRunnable):
|
||||
upstream_changed = utils.add_parents(set(model.upstream_changed))
|
||||
|
||||
if self.path in unmerged:
|
||||
return qtutils.tr('Unmerged')
|
||||
return (resources.icon('sigil-unmerged.png'),
|
||||
qtutils.tr('Unmerged'))
|
||||
if self.path in modified and self.path in staged:
|
||||
return qtutils.tr('Partially Staged')
|
||||
return (resources.icon('sigil-partial.png'),
|
||||
qtutils.tr('Partially Staged'))
|
||||
if self.path in modified:
|
||||
return qtutils.tr('Modified')
|
||||
return (resources.icon('sigil-modified.png'),
|
||||
qtutils.tr('Modified'))
|
||||
if self.path in staged:
|
||||
return qtutils.tr('Staged')
|
||||
if self.path in untracked:
|
||||
return qtutils.tr('Untracked')
|
||||
return (resources.icon('sigil-staged.png'),
|
||||
qtutils.tr('Staged'))
|
||||
if self.path in upstream_changed:
|
||||
return qtutils.tr('Changed Upstream')
|
||||
return '-'
|
||||
return (resources.icon('sigil-upstream.png'),
|
||||
qtutils.tr('Changed Upstream'))
|
||||
if self.path in untracked:
|
||||
return (None, '?')
|
||||
return (None, '')
|
||||
|
||||
def run(self):
|
||||
"""Perform expensive lookups and post corresponding events."""
|
||||
app = QtGui.QApplication.instance()
|
||||
app.postEvent(GitRepoEntryManager.entry(self.path),
|
||||
entry = GitRepoEntryManager.entry(self.path)
|
||||
app.postEvent(entry,
|
||||
GitRepoInfoEvent(Columns.MESSAGE, self.data('message')))
|
||||
app.postEvent(GitRepoEntryManager.entry(self.path),
|
||||
app.postEvent(entry,
|
||||
GitRepoInfoEvent(Columns.AGE, self.data('date')))
|
||||
app.postEvent(GitRepoEntryManager.entry(self.path),
|
||||
app.postEvent(entry,
|
||||
GitRepoInfoEvent(Columns.WHO, self.data('author')))
|
||||
app.postEvent(GitRepoEntryManager.entry(self.path),
|
||||
app.postEvent(entry,
|
||||
GitRepoInfoEvent(Columns.STATUS, self.status()))
|
||||
|
||||
TaskRunner.instance().cleanup_task(self)
|
||||
|
||||
|
||||
class GitRepoInfoEvent(QtCore.QEvent):
|
||||
"""Transport mechanism for communicating from a GitRepoInfoTask."""
|
||||
@@ -337,7 +399,18 @@ class GitRepoItem(QtGui.QStandardItem):
|
||||
self.setEditable(False)
|
||||
self.setDragEnabled(False)
|
||||
entry = GitRepoEntryManager.entry(path)
|
||||
QtCore.QObject.connect(entry, SIGNAL(column), self.setText)
|
||||
if column == Columns.STATUS:
|
||||
QtCore.QObject.connect(entry, SIGNAL(column), self.set_status)
|
||||
else:
|
||||
QtCore.QObject.connect(entry, SIGNAL(column), self.setText)
|
||||
|
||||
def set_status(self, data):
|
||||
icon, txt = data
|
||||
if icon:
|
||||
self.setIcon(QtGui.QIcon(icon))
|
||||
else:
|
||||
self.setIcon(QtGui.QIcon())
|
||||
self.setText(txt)
|
||||
|
||||
|
||||
class GitRepoNameItem(GitRepoItem):
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import Qt
|
||||
@@ -7,7 +9,50 @@ import cola
|
||||
import cola.utils
|
||||
import cola.qtutils
|
||||
from cola import signals
|
||||
from cola.models import gitrepo
|
||||
from cola.views import standard
|
||||
from cola.classic.model import GitRepoNameItem
|
||||
|
||||
|
||||
class RepoDialog(standard.Dialog):
|
||||
def __init__(self, parent=None, update=True):
|
||||
standard.Dialog.__init__(self, parent)
|
||||
self.setObjectName('classic')
|
||||
self.tree = RepoTreeView(parent)
|
||||
self.setLayout(QtGui.QHBoxLayout())
|
||||
self.layout().setMargin(1)
|
||||
self.layout().addWidget(self.tree)
|
||||
self.resize(720, 420)
|
||||
|
||||
self.connect(self, SIGNAL('updated'), self._updated_callback)
|
||||
self.model = cola.model()
|
||||
self.model.add_message_observer(self.model.message_updated,
|
||||
self._model_updated)
|
||||
cola.qtutils.add_close_action(self)
|
||||
if update:
|
||||
self._model_updated()
|
||||
|
||||
# Read-only mode property
|
||||
mode = property(lambda self: self.model.mode)
|
||||
|
||||
def _model_updated(self):
|
||||
"""Update the title with the current branch and directory name."""
|
||||
self.emit(SIGNAL('updated'))
|
||||
|
||||
def _updated_callback(self):
|
||||
branch = self.model.currentbranch
|
||||
curdir = os.getcwd()
|
||||
msg = 'Repository: %s\nBranch: %s' % (curdir, branch)
|
||||
|
||||
self.setToolTip(msg)
|
||||
|
||||
title = '%s [%s]' % (self.model.project, branch)
|
||||
if self.mode in (self.model.mode_diff, self.model.mode_diff_expr):
|
||||
title += ' *** diff mode***'
|
||||
elif self.mode == self.model.mode_review:
|
||||
title += ' *** review mode***'
|
||||
elif self.mode == self.model.mode_amend:
|
||||
title += ' *** amending ***'
|
||||
self.setWindowTitle(title)
|
||||
|
||||
|
||||
class RepoTreeView(QtGui.QTreeView):
|
||||
@@ -15,8 +60,6 @@ class RepoTreeView(QtGui.QTreeView):
|
||||
def __init__(self, parent=None):
|
||||
QtGui.QTreeView.__init__(self, parent)
|
||||
|
||||
self.resize(720, 300)
|
||||
self.setWindowTitle(self.tr('classic'))
|
||||
self.setSortingEnabled(False)
|
||||
self.setAllColumnsShowFocus(True)
|
||||
self.setAlternatingRowColors(True)
|
||||
@@ -199,10 +242,10 @@ class RepoTreeView(QtGui.QTreeView):
|
||||
paths = self.selected_paths()
|
||||
|
||||
model = cola.model()
|
||||
model_staged = set(model.staged)
|
||||
model_modified = set(model.modified)
|
||||
model_unmerged = set(model.unmerged)
|
||||
model_untracked = set(model.untracked)
|
||||
model_staged = cola.utils.add_parents(set(model.staged))
|
||||
model_modified = cola.utils.add_parents(set(model.modified))
|
||||
model_unmerged = cola.utils.add_parents(set(model.unmerged))
|
||||
model_untracked =cola.utils.add_parents(set(model.untracked))
|
||||
|
||||
for path in paths:
|
||||
if path in model_unmerged:
|
||||
@@ -213,6 +256,8 @@ class RepoTreeView(QtGui.QTreeView):
|
||||
staged.append(path)
|
||||
elif path in model_modified:
|
||||
modified.append(path)
|
||||
else:
|
||||
staged.append(path)
|
||||
# Push the new selection into the model.
|
||||
cola.selection_model().set_selection(staged, modified,
|
||||
unmerged, untracked)
|
||||
@@ -243,7 +288,7 @@ class RepoTreeView(QtGui.QTreeView):
|
||||
"""Return the selected paths."""
|
||||
items = map(self.model().itemFromIndex, self.selectedIndexes())
|
||||
return [i.path for i in items
|
||||
if i.type() == gitrepo.GitRepoNameItem.TYPE]
|
||||
if i.type() == GitRepoNameItem.TYPE]
|
||||
|
||||
def selected_staged_paths(self, selection=None):
|
||||
"""Return selected staged paths."""
|
||||
@@ -280,13 +325,14 @@ class RepoTreeView(QtGui.QTreeView):
|
||||
untracked = cola.utils.add_parents(set(model.untracked))
|
||||
tracked = staged.union(modified)
|
||||
return [p for p in selection
|
||||
if p not in untracked or p in staged or p in modified]
|
||||
if p not in untracked or p in tracked]
|
||||
|
||||
def _create_action(self, name, tooltip, slot, shortcut):
|
||||
"""Create an action with a shortcut, tooltip, and callback slot."""
|
||||
action = QtGui.QAction(self.tr(name), self)
|
||||
action.setStatusTip(self.tr(tooltip))
|
||||
action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
|
||||
if hasattr(Qt, 'WidgetWithChildrenShortcut'):
|
||||
action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
|
||||
action.setShortcut(shortcut)
|
||||
self.addAction(action)
|
||||
self.connect(action, SIGNAL('triggered()'), slot)
|
||||
+43
-33
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Maps Qt signals to Command objects.
|
||||
|
||||
The command factory listens to the global notifier and
|
||||
The command factory connects to the global notifier and
|
||||
creates commands objects as registered signals are
|
||||
encountered.
|
||||
|
||||
@@ -11,16 +11,14 @@ the undo and redo signals and manages the undo/redo stack.
|
||||
"""
|
||||
import cola
|
||||
from cola import signals
|
||||
from cola import errors
|
||||
from cola.decorators import memoize
|
||||
|
||||
|
||||
_factory = None
|
||||
@memoize
|
||||
def factory():
|
||||
"""Return a static instance of the command factory."""
|
||||
global _factory
|
||||
if _factory:
|
||||
return _factory
|
||||
_factory = CommandFactory()
|
||||
return _factory
|
||||
return CommandFactory()
|
||||
|
||||
|
||||
def SLOT(signal, *args, **opts):
|
||||
@@ -34,40 +32,35 @@ def SLOT(signal, *args, **opts):
|
||||
|
||||
|
||||
class CommandFactory(object):
|
||||
def __init__(self):
|
||||
def __init__(self, context=None):
|
||||
"""Setup the undo/redo stacks and register for notifications."""
|
||||
self.undoable = True
|
||||
self.undostack = []
|
||||
self.redostack = []
|
||||
self.signal_to_command = {}
|
||||
self.callbacks = {}
|
||||
self.context = context
|
||||
|
||||
cola.notifier().listen(signals.undo, self.undo)
|
||||
cola.notifier().listen(signals.redo, self.redo)
|
||||
|
||||
self.model = cola.model()
|
||||
self.model.add_observer(self)
|
||||
def has_command(self, signal):
|
||||
return signal in self.signal_to_command
|
||||
|
||||
def add_command(self, signal, command):
|
||||
"""Register a signal/command pair."""
|
||||
self.signal_to_command[signal] = command
|
||||
cola.notifier().listen(signal, self.cmdrunner(signal))
|
||||
|
||||
def notify(self, *params):
|
||||
"""
|
||||
Observe model changes.
|
||||
def add_global_command(self, signal, command):
|
||||
"""Register a global signal/command pair."""
|
||||
self.add_command(signal, command)
|
||||
cola.notifier().connect(signal, self.cmdrunner(signal))
|
||||
|
||||
This captures model parameters and maps them to signals that
|
||||
are observed by the UIs.
|
||||
def add_command_wrapper(self, cmd_wrapper):
|
||||
self.callbacks.update(cmd_wrapper.callbacks)
|
||||
|
||||
"""
|
||||
actions = {
|
||||
'diff_text': SLOT(signals.diff_text, self.model.diff_text),
|
||||
'commitmsg': SLOT(signals.editor_text, self.model.commitmsg),
|
||||
'mode': SLOT(signals.mode, self.model.mode),
|
||||
}
|
||||
for param in params:
|
||||
action = actions.get(param, lambda: None)
|
||||
action()
|
||||
def prompt_user(self, name, *args, **opts):
|
||||
try:
|
||||
return self.callbacks[name](*args, **opts)
|
||||
except KeyError:
|
||||
raise NotImplementedError('No callback for "%s' % name)
|
||||
|
||||
def clear(self):
|
||||
"""Clear the undo and redo stacks."""
|
||||
@@ -84,27 +77,44 @@ class CommandFactory(object):
|
||||
"""Given a signal and arguments, run its corresponding command."""
|
||||
cmdclass = self.signal_to_command[signal]
|
||||
cmdobj = cmdclass(*args, **opts)
|
||||
cmdobj.context = self.context
|
||||
# TODO we disable undo/redo for now; views just need to
|
||||
# inspect the stack and add menu entries when we enable it.
|
||||
if self.undoable and cmdobj.is_undoable():
|
||||
ok, result = self._do(cmdobj)
|
||||
if ok and self.undoable and cmdobj.is_undoable():
|
||||
self.undostack.append(cmdobj)
|
||||
return cmdobj.do()
|
||||
return result
|
||||
|
||||
def _do(self, cmdobj):
|
||||
try:
|
||||
result = cmdobj.do()
|
||||
except errors.UsageError, e:
|
||||
self.prompt_user(signals.information, e.title, e.message)
|
||||
return False, None
|
||||
else:
|
||||
return True, result
|
||||
|
||||
def undo(self):
|
||||
"""Undo the last command and add it to the redo stack."""
|
||||
if self.undostack:
|
||||
cmdobj = self.undostack.pop()
|
||||
cmdobj.undo()
|
||||
result = cmdobj.undo()
|
||||
self.redostack.append(cmdobj)
|
||||
return result
|
||||
else:
|
||||
print 'warning: undo stack is empty, doing nothing'
|
||||
return None
|
||||
|
||||
def redo(self):
|
||||
"""Redo the last command and add it to the undo stack."""
|
||||
if self.redostack:
|
||||
cmdobj = self.redostack.pop()
|
||||
cmdobj.do()
|
||||
self.undo.append(cmd)
|
||||
ok, result = self._do(cmdobj)
|
||||
if ok and cmdobj.is_undoable():
|
||||
self.undo.append(cmdobj)
|
||||
else:
|
||||
self.redostack.push(cmdobj)
|
||||
return result
|
||||
else:
|
||||
print 'warning: redo stack is empty, doing nothing'
|
||||
|
||||
|
||||
+372
-106
@@ -1,29 +1,54 @@
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
|
||||
from cStringIO import StringIO
|
||||
|
||||
import cola
|
||||
from cola import i18n
|
||||
from cola import core
|
||||
from cola import errors
|
||||
from cola import gitcfg
|
||||
from cola import gitcmds
|
||||
from cola import utils
|
||||
from cola import signals
|
||||
from cola import cmdfactory
|
||||
from cola import difftool
|
||||
from cola import version
|
||||
from cola.diffparse import DiffParser
|
||||
from cola.models import selection
|
||||
|
||||
_notifier = cola.notifier()
|
||||
_factory = cmdfactory.factory()
|
||||
_config = gitcfg.instance()
|
||||
|
||||
|
||||
class Command(object):
|
||||
"""Base class for all commands; provides the command pattern."""
|
||||
def __init__(self, update=False):
|
||||
class BaseCommand(object):
|
||||
"""Base class for all commands; provides the command pattern"""
|
||||
def __init__(self):
|
||||
self.undoable = False
|
||||
|
||||
def is_undoable(self):
|
||||
"""Can this be undone?"""
|
||||
return self.undoable
|
||||
|
||||
def name(self):
|
||||
"""Return this command's name."""
|
||||
return self.__class__.__name__
|
||||
|
||||
def do(self):
|
||||
raise NotImplementedError('%s.do() is unimplemented' % self.name())
|
||||
|
||||
def undo(self):
|
||||
raise NotImplementedError('%s.undo() is unimplemented' % self.name())
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Base class for commands that modify the main model"""
|
||||
def __init__(self):
|
||||
"""Initialize the command and stash away values for use in do()"""
|
||||
# These are commonly used so let's make it easier to write new commands.
|
||||
self.undoable = False
|
||||
BaseCommand.__init__(self)
|
||||
self.model = cola.model()
|
||||
self.update = update
|
||||
|
||||
self.old_diff_text = self.model.diff_text
|
||||
self.old_filename = self.model.filename
|
||||
@@ -41,12 +66,6 @@ class Command(object):
|
||||
self.model.set_filename(self.new_filename)
|
||||
self.model.set_head(self.new_head)
|
||||
self.model.set_mode(self.new_mode)
|
||||
if self.update:
|
||||
self.model.update_status()
|
||||
|
||||
def is_undoable(self):
|
||||
"""Can this be undone?"""
|
||||
return self.undoable
|
||||
|
||||
def undo(self):
|
||||
"""Undo the operation."""
|
||||
@@ -54,37 +73,12 @@ class Command(object):
|
||||
self.model.set_filename(self.old_filename)
|
||||
self.model.set_head(self.old_head)
|
||||
self.model.set_mode(self.old_mode)
|
||||
if self.update:
|
||||
self.model.update_status()
|
||||
|
||||
def name(self):
|
||||
"""Return this command's name."""
|
||||
return self.__class__.__name__
|
||||
|
||||
|
||||
class AddSignoff(Command):
|
||||
"""Add a signed-off-by to the commit message."""
|
||||
def __init__(self):
|
||||
Command.__init__(self)
|
||||
self.undoable = True
|
||||
self.old_commitmsg = self.model.commitmsg
|
||||
self.new_commitmsg = self.old_commitmsg
|
||||
signoff = ('\nSigned-off-by: %s <%s>\n' %
|
||||
(self.model.local_user_name, self.model.local_user_email))
|
||||
if signoff not in self.new_commitmsg:
|
||||
self.new_commitmsg += ('\n' + signoff)
|
||||
|
||||
def do(self):
|
||||
self.model.set_commitmsg(self.new_commitmsg)
|
||||
|
||||
def undo(self):
|
||||
self.model.set_commitmsg(self.old_commitmsg)
|
||||
|
||||
|
||||
class AmendMode(Command):
|
||||
"""Try to amend a commit."""
|
||||
def __init__(self, amend):
|
||||
Command.__init__(self, update=True)
|
||||
Command.__init__(self)
|
||||
self.undoable = True
|
||||
self.skip = False
|
||||
self.amending = amend
|
||||
@@ -98,9 +92,10 @@ class AmendMode(Command):
|
||||
# else, amend unchecked, regular commit
|
||||
self.new_mode = self.model.mode_none
|
||||
self.new_head = 'HEAD'
|
||||
self.new_diff_text = ''
|
||||
self.new_commitmsg = self.model.commitmsg
|
||||
# If we're going back into new-commit-mode then search the
|
||||
# undo stack for a previous amend-commid-mode and grab the
|
||||
# undo stack for a previous amend-commit-mode and grab the
|
||||
# commit message at that point in time.
|
||||
if not _factory.undostack:
|
||||
return
|
||||
@@ -119,10 +114,10 @@ class AmendMode(Command):
|
||||
"""Leave/enter amend mode."""
|
||||
"""Attempt to enter amend mode. Do not allow this when merging."""
|
||||
if self.amending:
|
||||
if os.path.exists(self.model.git_repo_path('MERGE_HEAD')):
|
||||
if os.path.exists(self.model.git.git_path('MERGE_HEAD')):
|
||||
self.skip = True
|
||||
_notifier.broadcast(signals.amend, False)
|
||||
_notifier.broadcast(signals.information,
|
||||
_factory.prompt_user(signals.information,
|
||||
'Oops! Unmerged',
|
||||
'You are in the middle of a merge.\n'
|
||||
'You cannot amend while merging.')
|
||||
@@ -131,17 +126,19 @@ class AmendMode(Command):
|
||||
_notifier.broadcast(signals.amend, self.amending)
|
||||
self.model.set_commitmsg(self.new_commitmsg)
|
||||
Command.do(self)
|
||||
self.model.update_file_status()
|
||||
|
||||
def undo(self):
|
||||
if self.skip:
|
||||
return
|
||||
self.model.set_commitmsg(self.old_commitmsg)
|
||||
Command.undo(self)
|
||||
self.model.update_file_status()
|
||||
|
||||
|
||||
class ApplyDiffSelection(Command):
|
||||
def __init__(self, staged, selected, offset, selection, apply_to_worktree):
|
||||
Command.__init__(self, update=True)
|
||||
Command.__init__(self)
|
||||
self.staged = staged
|
||||
self.selected = selected
|
||||
self.offset = offset
|
||||
@@ -155,6 +152,7 @@ class ApplyDiffSelection(Command):
|
||||
filename=self.model.filename,
|
||||
cached=False,
|
||||
branch=self.model.head)
|
||||
status, output = \
|
||||
parser.process_diff_selection(self.selected,
|
||||
self.offset,
|
||||
self.selection,
|
||||
@@ -165,27 +163,71 @@ class ApplyDiffSelection(Command):
|
||||
filename=self.model.filename,
|
||||
cached=self.staged,
|
||||
reverse=self.apply_to_worktree)
|
||||
status, output = \
|
||||
parser.process_diff_selection(self.selected,
|
||||
self.offset,
|
||||
self.selection,
|
||||
apply_to_worktree=
|
||||
self.apply_to_worktree)
|
||||
_notifier.broadcast(signals.log_cmd, status, output)
|
||||
# Redo the diff to show changes
|
||||
if self.staged:
|
||||
diffcmd = DiffStaged([self.model.filename])
|
||||
else:
|
||||
diffcmd = Diff([self.model.filename])
|
||||
diffcmd.do()
|
||||
self.model.update_status()
|
||||
self.model.update_file_status()
|
||||
|
||||
|
||||
class ApplyPatches(Command):
|
||||
def __init__(self, patches):
|
||||
Command.__init__(self)
|
||||
patches.sort()
|
||||
self.patches = patches
|
||||
|
||||
def do(self):
|
||||
diff_text = ''
|
||||
num_patches = len(self.patches)
|
||||
orig_head = self.model.git.rev_parse('HEAD')
|
||||
|
||||
for idx, patch in enumerate(self.patches):
|
||||
status, output = self.model.git.am(patch,
|
||||
with_status=True,
|
||||
with_stderr=True)
|
||||
# Log the git-am command
|
||||
_notifier.broadcast(signals.log_cmd, status, output)
|
||||
|
||||
if num_patches > 1:
|
||||
diff = self.model.git.diff('HEAD^!', stat=True)
|
||||
diff_text += 'Patch %d/%d - ' % (idx+1, num_patches)
|
||||
diff_text += '%s:\n%s\n\n' % (os.path.basename(patch), diff)
|
||||
|
||||
diff_text += 'Summary:\n'
|
||||
diff_text += self.model.git.diff(orig_head, stat=True)
|
||||
|
||||
# Display a diffstat
|
||||
self.model.set_diff_text(diff_text)
|
||||
|
||||
self.model.update_file_status()
|
||||
|
||||
_factory.prompt_user(signals.information,
|
||||
'Patch(es) Applied',
|
||||
'%d patch(es) applied:\n\n%s' %
|
||||
(len(self.patches),
|
||||
'\n'.join(map(os.path.basename, self.patches))))
|
||||
|
||||
|
||||
class HeadChangeCommand(Command):
|
||||
"""Changes the model's current head."""
|
||||
def __init__(self, treeish):
|
||||
Command.__init__(self, update=True)
|
||||
Command.__init__(self)
|
||||
self.new_head = treeish
|
||||
self.new_diff_text = ''
|
||||
|
||||
def do(self):
|
||||
Command.do(self)
|
||||
self.model.update_file_status()
|
||||
|
||||
|
||||
class BranchMode(HeadChangeCommand):
|
||||
"""Enter into diff-branch mode."""
|
||||
@@ -194,10 +236,10 @@ class BranchMode(HeadChangeCommand):
|
||||
self.old_filename = self.model.filename
|
||||
self.new_filename = filename
|
||||
self.new_mode = self.model.mode_branch
|
||||
self.new_diff_text = self.model.diff_helper(filename=filename,
|
||||
cached=False,
|
||||
reverse=True,
|
||||
branch=treeish)
|
||||
self.new_diff_text = gitcmds.diff_helper(filename=filename,
|
||||
cached=False,
|
||||
reverse=True,
|
||||
branch=treeish)
|
||||
class Checkout(Command):
|
||||
"""
|
||||
A command object for git-checkout.
|
||||
@@ -205,21 +247,25 @@ class Checkout(Command):
|
||||
'argv' is handed off directly to git.
|
||||
|
||||
"""
|
||||
def __init__(self, argv):
|
||||
def __init__(self, argv, checkout_branch=False):
|
||||
Command.__init__(self)
|
||||
self.argv = argv
|
||||
self.checkout_branch = checkout_branch
|
||||
self.new_diff_text = ''
|
||||
|
||||
def do(self):
|
||||
status, output = self.model.git.checkout(with_stderr=True,
|
||||
with_status=True, *self.argv)
|
||||
_notifier.broadcast(signals.log_cmd, status, output)
|
||||
self.model.set_diff_text('')
|
||||
self.model.update_status()
|
||||
if self.checkout_branch:
|
||||
self.model.update_status()
|
||||
else:
|
||||
self.model.update_file_status()
|
||||
|
||||
|
||||
class CheckoutBranch(Checkout):
|
||||
"""Checkout a branch."""
|
||||
def __init__(self, branch):
|
||||
def __init__(self, branch, checkout_branch=True):
|
||||
Checkout.__init__(self, [branch])
|
||||
|
||||
|
||||
@@ -231,16 +277,21 @@ class CherryPick(Command):
|
||||
|
||||
def do(self):
|
||||
self.model.cherry_pick_list(self.commits)
|
||||
self.model.update_file_status()
|
||||
|
||||
|
||||
class ResetMode(Command):
|
||||
"""Reset the mode and clear the model's diff text."""
|
||||
def __init__(self):
|
||||
Command.__init__(self, update=True)
|
||||
Command.__init__(self)
|
||||
self.new_mode = self.model.mode_none
|
||||
self.new_head = 'HEAD'
|
||||
self.new_diff_text = ''
|
||||
|
||||
def do(self):
|
||||
Command.do(self)
|
||||
self.model.update_file_status()
|
||||
|
||||
|
||||
class Commit(ResetMode):
|
||||
"""Attempt to create a new commit."""
|
||||
@@ -262,6 +313,28 @@ class Commit(ResetMode):
|
||||
_notifier.broadcast(signals.log_cmd, status, title+output)
|
||||
|
||||
|
||||
class Ignore(Command):
|
||||
"""Add files to .gitignore"""
|
||||
def __init__(self, filenames):
|
||||
Command.__init__(self)
|
||||
self.filenames = filenames
|
||||
|
||||
def do(self):
|
||||
new_additions = ''
|
||||
for fname in self.filenames:
|
||||
new_additions = new_additions + fname + '\n'
|
||||
for_status = new_additions
|
||||
if new_additions:
|
||||
if os.path.exists('.gitignore'):
|
||||
current_list = utils.slurp('.gitignore')
|
||||
new_additions = new_additions + current_list
|
||||
utils.write('.gitignore', new_additions)
|
||||
_notifier.broadcast(signals.log_cmd,
|
||||
0,
|
||||
'Added to .gitignore:\n%s' % for_status)
|
||||
self.model.update_file_status()
|
||||
|
||||
|
||||
class Delete(Command):
|
||||
"""Simply delete files."""
|
||||
def __init__(self, filenames):
|
||||
@@ -277,11 +350,11 @@ class Delete(Command):
|
||||
os.remove(filename)
|
||||
rescan=True
|
||||
except:
|
||||
_notifier.broadcast(signals.information,
|
||||
_factory.prompt_user(signals.information,
|
||||
'Error'
|
||||
'Deleting "%s" failed.' % filename)
|
||||
if rescan:
|
||||
self.model.update_status()
|
||||
self.model.update_file_status()
|
||||
|
||||
class DeleteBranch(Command):
|
||||
"""Delete a git branch."""
|
||||
@@ -303,6 +376,9 @@ class Diff(Command):
|
||||
"""Perform a diff and set the model's current text."""
|
||||
def __init__(self, filenames, cached=False):
|
||||
Command.__init__(self)
|
||||
# Guard against the list of files being empty
|
||||
if not filenames:
|
||||
return
|
||||
opts = {}
|
||||
if cached:
|
||||
cached = not self.model.read_only()
|
||||
@@ -311,9 +387,10 @@ class Diff(Command):
|
||||
self.new_filename = filenames[0]
|
||||
self.old_filename = self.model.filename
|
||||
if not self.model.read_only():
|
||||
self.new_mode = self.model.mode_worktree
|
||||
self.new_diff_text = self.model.diff_helper(filename=self.new_filename,
|
||||
cached=cached, **opts)
|
||||
if self.model.mode != self.model.mode_amend:
|
||||
self.new_mode = self.model.mode_worktree
|
||||
self.new_diff_text = gitcmds.diff_helper(filename=self.new_filename,
|
||||
cached=cached, **opts)
|
||||
|
||||
|
||||
class DiffMode(HeadChangeCommand):
|
||||
@@ -335,7 +412,7 @@ class Diffstat(Command):
|
||||
def __init__(self):
|
||||
Command.__init__(self)
|
||||
diff = self.model.git.diff(self.model.head,
|
||||
unified=self.model.diff_context,
|
||||
unified=_config.get('diff.context', 3),
|
||||
no_color=True,
|
||||
M=True,
|
||||
stat=True)
|
||||
@@ -380,7 +457,9 @@ class Difftool(Command):
|
||||
args = []
|
||||
if self.staged and not self.model.read_only():
|
||||
args.append('--cached')
|
||||
args.extend([self.model.head, '--'])
|
||||
if self.model.head != 'HEAD':
|
||||
args.append(self.model.head)
|
||||
args.append('--')
|
||||
args.extend(self.filenames)
|
||||
difftool.launch(args)
|
||||
|
||||
@@ -411,8 +490,8 @@ class FormatPatch(Command):
|
||||
self.revs = revs
|
||||
|
||||
def do(self):
|
||||
self.model.format_patch_helper(self.to_export, self.revs,
|
||||
output='patches')
|
||||
status, output = gitcmds.format_patchsets(self.to_export, self.revs)
|
||||
_notifier.broadcast(signals.log_cmd, status, output)
|
||||
|
||||
|
||||
class GrepMode(Command):
|
||||
@@ -420,7 +499,7 @@ class GrepMode(Command):
|
||||
"""Perform a git-grep."""
|
||||
Command.__init__(self)
|
||||
self.new_mode = self.model.mode_grep
|
||||
self.new_diff_text = self.model.git.grep(txt, n=True)
|
||||
self.new_diff_text = core.decode(self.model.git.grep(txt, n=True))
|
||||
|
||||
|
||||
class LoadCommitMessage(Command):
|
||||
@@ -433,17 +512,48 @@ class LoadCommitMessage(Command):
|
||||
self.old_directory = self.model.directory
|
||||
|
||||
def do(self):
|
||||
self.model.set_directory(os.path.dirname(self.path))
|
||||
fh = open(self.path, 'r')
|
||||
contents = core.decode(core.read_nointr(fh))
|
||||
fh.close()
|
||||
self.model.set_commitmsg(contents)
|
||||
path = self.path
|
||||
if not path or not os.path.isfile(path):
|
||||
raise errors.UsageError('Error: cannot find commit template',
|
||||
'%s: No such file or directory.' % path)
|
||||
self.model.set_directory(os.path.dirname(path))
|
||||
self.model.set_commitmsg(utils.slurp(path))
|
||||
|
||||
def undo(self):
|
||||
self.model.set_commitmsg(self.old_commitmsg)
|
||||
self.model.set_directory(self.old_directory)
|
||||
|
||||
|
||||
class LoadCommitTemplate(LoadCommitMessage):
|
||||
"""Loads the commit message template specified by commit.template."""
|
||||
def __init__(self):
|
||||
LoadCommitMessage.__init__(self, _config.get('commit.template'))
|
||||
|
||||
def do(self):
|
||||
if self.path is None:
|
||||
raise errors.UsageError('Error: unconfigured commit template',
|
||||
'A commit template has not been configured.\n'
|
||||
'Use "git config" to define "commit.template"\n'
|
||||
'so that it points to a commit template.')
|
||||
return LoadCommitMessage.do(self)
|
||||
|
||||
|
||||
class LoadPreviousMessage(Command):
|
||||
"""Try to amend a commit."""
|
||||
def __init__(self, sha1):
|
||||
Command.__init__(self)
|
||||
self.sha1 = sha1
|
||||
self.old_commitmsg = self.model.commitmsg
|
||||
self.new_commitmsg = self.model.prev_commitmsg(sha1)
|
||||
self.undoable = True
|
||||
|
||||
def do(self):
|
||||
self.model.set_commitmsg(self.new_commitmsg)
|
||||
|
||||
def undo(self):
|
||||
self.model.set_commitmsg(self.old_commitmsg)
|
||||
|
||||
|
||||
class Mergetool(Command):
|
||||
"""Launch git-mergetool on a list of paths."""
|
||||
def __init__(self, paths):
|
||||
@@ -453,56 +563,143 @@ class Mergetool(Command):
|
||||
def do(self):
|
||||
if not self.paths:
|
||||
return
|
||||
if version.check('mergetool-no-prompt',
|
||||
self.model.git.version().split()[-1]):
|
||||
utils.fork(['git', 'mergetool', '--no-prompt', '--'] + self.paths)
|
||||
else:
|
||||
utils.fork(['xterm', '-e', 'git', 'mergetool', '--'] + self.paths)
|
||||
utils.fork(['git', 'mergetool', '--no-prompt', '--'] + self.paths)
|
||||
|
||||
|
||||
class OpenRepo(Command):
|
||||
"""Launches git-cola on a repo."""
|
||||
def __init__(self, dirname):
|
||||
Command.__init__(self)
|
||||
self.new_directory = utils.quote_repopath(dirname)
|
||||
self.new_directory = dirname
|
||||
|
||||
def do(self):
|
||||
self.model.set_directory(self.new_directory)
|
||||
utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
|
||||
utils.fork([sys.executable, sys.argv[0], '--repo', self.new_directory])
|
||||
|
||||
|
||||
class Clone(Command):
|
||||
"""Clones a repository and launches a new cola session."""
|
||||
def __init__(self, url, destdir):
|
||||
"""Clones a repository and optionally spawns a new cola session."""
|
||||
def __init__(self, url, destdir, spawn=True):
|
||||
Command.__init__(self)
|
||||
self.url = url
|
||||
self.new_directory = utils.quote_repopath(destdir)
|
||||
self.new_directory = destdir
|
||||
self.spawn = spawn
|
||||
|
||||
def do(self):
|
||||
self.model.git.clone(self.url, self.new_directory,
|
||||
with_stderr=True, with_status=True)
|
||||
utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
|
||||
if self.spawn:
|
||||
utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
|
||||
|
||||
|
||||
rescan = 'rescan'
|
||||
|
||||
class Rescan(Command):
|
||||
"""Rescans for changes."""
|
||||
def __init__(self):
|
||||
Command.__init__(self, update=True)
|
||||
def do(self):
|
||||
self.model.update_status(update_index=True)
|
||||
|
||||
|
||||
class ReviewBranchMode(Command):
|
||||
"""Enter into review-branch mode."""
|
||||
def __init__(self, branch):
|
||||
Command.__init__(self, update=True)
|
||||
Command.__init__(self)
|
||||
self.new_mode = self.model.mode_review
|
||||
self.new_head = self.model.merge_base_to(branch)
|
||||
self.new_head = gitcmds.merge_base_parent(branch)
|
||||
self.new_diff_text = ''
|
||||
|
||||
def do(self):
|
||||
Command.do(self)
|
||||
self.model.update_status()
|
||||
|
||||
|
||||
class RunConfigAction(Command):
|
||||
"""Run a user-configured action, typically from the "Tools" menu"""
|
||||
def __init__(self, name):
|
||||
Command.__init__(self)
|
||||
self.name = name
|
||||
self.model = cola.model()
|
||||
|
||||
def do(self):
|
||||
for env in ('FILENAME', 'REVISION', 'ARGS'):
|
||||
try:
|
||||
del os.environ[env]
|
||||
except KeyError:
|
||||
pass
|
||||
rev = None
|
||||
args = None
|
||||
opts = _config.get_guitool_opts(self.name)
|
||||
cmd = opts.get('cmd')
|
||||
if 'title' not in opts:
|
||||
opts['title'] = cmd
|
||||
|
||||
if 'prompt' not in opts or opts.get('prompt') is True:
|
||||
prompt = i18n.gettext('Are you sure you want to run %s?') % cmd
|
||||
opts['prompt'] = prompt
|
||||
|
||||
if opts.get('needsfile'):
|
||||
filename = selection.filename()
|
||||
if not filename:
|
||||
_factory.prompt_user(signals.information,
|
||||
'Please select a file',
|
||||
'"%s" requires a selected file' % cmd)
|
||||
return
|
||||
os.environ['FILENAME'] = filename
|
||||
|
||||
if opts.get('revprompt') or opts.get('argprompt'):
|
||||
while True:
|
||||
ok = _factory.prompt_user(signals.run_config_action, cmd, opts)
|
||||
if not ok:
|
||||
return
|
||||
rev = opts.get('revision')
|
||||
args = opts.get('args')
|
||||
if opts.get('revprompt') and not rev:
|
||||
msg = ('Invalid revision:\n\n'
|
||||
'Revision expression is empty')
|
||||
title = 'Oops!'
|
||||
_factory.prompt_user(signals.information, title, msg)
|
||||
continue
|
||||
break
|
||||
|
||||
elif opts.get('confirm'):
|
||||
title = os.path.expandvars(opts.get('title'))
|
||||
prompt = os.path.expandvars(opts.get('prompt'))
|
||||
if not _factory.prompt_user(signals.question, title, prompt):
|
||||
return
|
||||
if rev:
|
||||
os.environ['REVISION'] = rev
|
||||
if args:
|
||||
os.environ['ARGS'] = args
|
||||
title = os.path.expandvars(cmd)
|
||||
_notifier.broadcast(signals.log_cmd, 0, 'running: ' + title)
|
||||
cmd = ['sh', '-c', cmd]
|
||||
|
||||
if opts.get('noconsole'):
|
||||
status, out, err = utils.run_command(cmd, flag_error=False)
|
||||
else:
|
||||
status, out, err = _factory.prompt_user(signals.run_command,
|
||||
title, cmd)
|
||||
|
||||
_notifier.broadcast(signals.log_cmd, status,
|
||||
'stdout: %s\nstatus: %s\nstderr: %s' %
|
||||
(out.rstrip(), status, err.rstrip()))
|
||||
|
||||
if not opts.get('norescan'):
|
||||
self.model.update_status()
|
||||
return status
|
||||
|
||||
|
||||
class SetDiffText(Command):
|
||||
def __init__(self, text):
|
||||
Command.__init__(self)
|
||||
self.undoable = True
|
||||
self.new_diff_text = text
|
||||
|
||||
|
||||
class ShowUntracked(Command):
|
||||
"""Show an untracked file."""
|
||||
# We don't actually do anything other than set the mode right now.
|
||||
# We could probably check the mimetype for the file and handle things
|
||||
# TODO check the mimetype for the file and handle things
|
||||
# generically.
|
||||
def __init__(self, filenames):
|
||||
Command.__init__(self)
|
||||
@@ -510,6 +707,33 @@ class ShowUntracked(Command):
|
||||
# TODO new_diff_text = utils.file_preview(filenames[0])
|
||||
|
||||
|
||||
class SignOff(Command):
|
||||
def __init__(self):
|
||||
Command.__init__(self)
|
||||
self.undoable = True
|
||||
self.old_commitmsg = self.model.commitmsg
|
||||
|
||||
def do(self):
|
||||
signoff = self.signoff()
|
||||
if signoff in self.model.commitmsg:
|
||||
return
|
||||
self.model.set_commitmsg(self.model.commitmsg + '\n' + signoff)
|
||||
|
||||
def undo(self):
|
||||
self.model.set_commitmsg(self.old_commitmsg)
|
||||
|
||||
def signoff(self):
|
||||
try:
|
||||
import pwd
|
||||
user = pwd.getpwuid(os.getuid()).pw_name
|
||||
except ImportError:
|
||||
user = os.getenv('USER', 'unknown')
|
||||
|
||||
name = _config.get('user.name', user)
|
||||
email = _config.get('user.email', '%s@%s' % (user, platform.node()))
|
||||
return '\nSigned-off-by: %s <%s>' % (name, email)
|
||||
|
||||
|
||||
class Stage(Command):
|
||||
"""Stage a set of paths."""
|
||||
def __init__(self, paths):
|
||||
@@ -535,6 +759,7 @@ class StageUntracked(Stage):
|
||||
Stage.__init__(self, None)
|
||||
self.paths = self.model.untracked
|
||||
|
||||
|
||||
class Tag(Command):
|
||||
"""Create a tag object."""
|
||||
def __init__(self, name, revision, sign=False, message=''):
|
||||
@@ -546,28 +771,35 @@ class Tag(Command):
|
||||
|
||||
def do(self):
|
||||
log_msg = 'Tagging: "%s" as "%s"' % (self._revision, self._name)
|
||||
opts = {}
|
||||
if self._message:
|
||||
opts['F'] = self.model.tmp_filename()
|
||||
utils.write(opts['F'], self._message)
|
||||
|
||||
if self._sign:
|
||||
log_msg += ', GPG-signed'
|
||||
path = cola.model().tmp_filename()
|
||||
utils.write(path, self._message)
|
||||
status, output = cola.model().git.tag(self._name,
|
||||
self._revision,
|
||||
s=True,
|
||||
F=path,
|
||||
with_status=True,
|
||||
with_stderr=True)
|
||||
os.unlink(path)
|
||||
opts['s'] = True
|
||||
status, output = self.model.git.tag(self._name,
|
||||
self._revision,
|
||||
with_status=True,
|
||||
with_stderr=True,
|
||||
**opts)
|
||||
else:
|
||||
status, output = cola.model().git.tag(self._name,
|
||||
self._revision,
|
||||
with_status=True,
|
||||
with_stderr=True)
|
||||
opts['a'] = bool(self._message)
|
||||
status, output = self.model.git.tag(self._name,
|
||||
self._revision,
|
||||
with_status=True,
|
||||
with_stderr=True,
|
||||
**opts)
|
||||
if 'F' in opts:
|
||||
os.unlink(opts['F'])
|
||||
|
||||
if output:
|
||||
log_msg += '\nOutput:\n%s' % output
|
||||
|
||||
_notifier.broadcast(signals.log_cmd, status, log_msg)
|
||||
if status == 0:
|
||||
cola.model().update_status()
|
||||
self.model.update_status()
|
||||
|
||||
|
||||
class Unstage(Command):
|
||||
@@ -584,9 +816,6 @@ class Unstage(Command):
|
||||
|
||||
class UnstageAll(Command):
|
||||
"""Unstage all files; resets the index."""
|
||||
def __init__(self):
|
||||
Command.__init__(self, update=True)
|
||||
|
||||
def do(self):
|
||||
self.model.unstage_all()
|
||||
|
||||
@@ -608,8 +837,18 @@ class UntrackedSummary(Command):
|
||||
if untracked:
|
||||
io.write('# possible .gitignore rule%s:\n' % suffix)
|
||||
for u in untracked:
|
||||
io.write('/%s\n' % u)
|
||||
self.new_diff_text = io.getvalue()
|
||||
io.write('/'+core.encode(u))
|
||||
self.new_diff_text = core.decode(io.getvalue())
|
||||
|
||||
|
||||
class UpdateStatus(Command):
|
||||
"""Update the status of a list of files."""
|
||||
def __init__(self, files):
|
||||
Command.__init__(self)
|
||||
self.files = files
|
||||
|
||||
def do(self):
|
||||
self.model.update_status_of_files(self.files)
|
||||
|
||||
|
||||
class VisualizeAll(Command):
|
||||
@@ -640,6 +879,25 @@ class VisualizePaths(Command):
|
||||
utils.fork(self.argv)
|
||||
|
||||
|
||||
visualize_revision = 'visualize_revision'
|
||||
|
||||
class VisualizeRevision(Command):
|
||||
"""Visualize a specific revision."""
|
||||
def __init__(self, revision, paths=None):
|
||||
Command.__init__(self)
|
||||
self.revision = revision
|
||||
self.paths = paths
|
||||
|
||||
def do(self):
|
||||
argv = [self.model.history_browser()]
|
||||
if self.revision:
|
||||
argv.append(self.revision)
|
||||
if self.paths:
|
||||
argv.append('--')
|
||||
argv.extend(self.paths)
|
||||
utils.fork(argv)
|
||||
|
||||
|
||||
def register():
|
||||
"""
|
||||
Register signal mappings with the factory.
|
||||
@@ -649,9 +907,9 @@ def register():
|
||||
|
||||
"""
|
||||
signal_to_command_map = {
|
||||
signals.add_signoff: AddSignoff,
|
||||
signals.apply_diff_selection: ApplyDiffSelection,
|
||||
signals.amend_mode: AmendMode,
|
||||
signals.apply_diff_selection: ApplyDiffSelection,
|
||||
signals.apply_patches: ApplyPatches,
|
||||
signals.branch_mode: BranchMode,
|
||||
signals.clone: Clone,
|
||||
signals.checkout: Checkout,
|
||||
@@ -669,14 +927,20 @@ def register():
|
||||
signals.edit: Edit,
|
||||
signals.format_patch: FormatPatch,
|
||||
signals.grep: GrepMode,
|
||||
signals.ignore: Ignore,
|
||||
signals.load_commit_message: LoadCommitMessage,
|
||||
signals.load_commit_template: LoadCommitTemplate,
|
||||
signals.load_previous_message: LoadPreviousMessage,
|
||||
signals.modified_summary: Diffstat,
|
||||
signals.mergetool: Mergetool,
|
||||
signals.open_repo: OpenRepo,
|
||||
signals.rescan: Rescan,
|
||||
signals.reset_mode: ResetMode,
|
||||
signals.review_branch_mode: ReviewBranchMode,
|
||||
signals.run_config_action: RunConfigAction,
|
||||
signals.set_diff_text: SetDiffText,
|
||||
signals.show_untracked: ShowUntracked,
|
||||
signals.signoff: SignOff,
|
||||
signals.stage: Stage,
|
||||
signals.stage_modified: StageModified,
|
||||
signals.stage_untracked: StageUntracked,
|
||||
@@ -686,10 +950,12 @@ def register():
|
||||
signals.unstage_all: UnstageAll,
|
||||
signals.unstage_selected: UnstageSelected,
|
||||
signals.untracked_summary: UntrackedSummary,
|
||||
signals.update_status: UpdateStatus,
|
||||
signals.visualize_all: VisualizeAll,
|
||||
signals.visualize_current: VisualizeCurrent,
|
||||
signals.visualize_paths: VisualizePaths,
|
||||
signals.visualize_revision: VisualizeRevision,
|
||||
}
|
||||
|
||||
for signal, cmd in signal_to_command_map.iteritems():
|
||||
_factory.add_command(signal, cmd)
|
||||
_factory.add_global_command(signal, cmd)
|
||||
@@ -0,0 +1,14 @@
|
||||
try:
|
||||
set = set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
set = set
|
||||
|
||||
try:
|
||||
import hashlib
|
||||
except ImportError:
|
||||
import md5
|
||||
class hashlib(object):
|
||||
@staticmethod
|
||||
def new(*args):
|
||||
return md5.new()
|
||||
@@ -5,6 +5,7 @@ import sys
|
||||
|
||||
from PyQt4 import QtGui
|
||||
|
||||
from cola import core
|
||||
from cola import utils
|
||||
from cola import qtutils
|
||||
from cola.qobserver import QObserver
|
||||
@@ -19,8 +20,8 @@ def save_bookmark():
|
||||
|
||||
"""
|
||||
model = settings.SettingsManager.settings()
|
||||
model.add_bookmark(os.getcwd())
|
||||
model.save()
|
||||
model.add_bookmark(core.decode(os.getcwd()))
|
||||
settings.SettingsManager.save()
|
||||
qtutils.information("Bookmark Saved")
|
||||
|
||||
def manage_bookmarks():
|
||||
@@ -31,6 +32,7 @@ def manage_bookmarks():
|
||||
ctl = BookmarkController(model, view)
|
||||
view.show()
|
||||
|
||||
|
||||
class BookmarkController(QObserver):
|
||||
"""Handles interactions with the bookmarks dialog
|
||||
"""
|
||||
@@ -45,7 +47,7 @@ class BookmarkController(QObserver):
|
||||
|
||||
def save(self):
|
||||
"""Saves the bookmarks settings and exits"""
|
||||
self.model.save()
|
||||
settings.SettingsManager.save()
|
||||
self.view.accept()
|
||||
|
||||
def open(self):
|
||||
@@ -55,7 +57,7 @@ class BookmarkController(QObserver):
|
||||
if not selection:
|
||||
return
|
||||
for item in selection:
|
||||
utils.fork(['git', 'cola', item])
|
||||
utils.fork([sys.executable, sys.argv[0], '--repo', item])
|
||||
|
||||
def delete(self):
|
||||
"""Removes a bookmark from the bookmarks list"""
|
||||
|
||||
+25
-16
@@ -1,44 +1,42 @@
|
||||
"""This controller handles the compare commits dialog."""
|
||||
|
||||
import os
|
||||
|
||||
from PyQt4 import QtGui
|
||||
|
||||
import cola
|
||||
from cola import utils
|
||||
from cola import qtutils
|
||||
from cola import difftool
|
||||
from cola import gitcmds
|
||||
from cola.qobserver import QObserver
|
||||
from cola.models.compare import CompareModel
|
||||
from cola.models.compare import BranchCompareModel
|
||||
from cola.views import compare
|
||||
from cola.controllers.repobrowser import select_file_from_repo
|
||||
from cola.views import compare as vcompare
|
||||
from cola.widgets.browse import BrowseDialog
|
||||
|
||||
|
||||
def compare_file():
|
||||
"""Launches a dialog for comparing revisions touching a file path"""
|
||||
model = cola.model()
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
filename = select_file_from_repo()
|
||||
filename = BrowseDialog.select_file('HEAD')
|
||||
if not filename:
|
||||
return
|
||||
compare(filename)
|
||||
|
||||
|
||||
def compare(filename=None):
|
||||
"""Launches a dialog for comparing a pair of commits"""
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
model = CompareModel()
|
||||
view = compare.CompareView(parent)
|
||||
view = vcompare.CompareView(parent)
|
||||
ctl = CompareController(model, view, filename)
|
||||
view.show()
|
||||
return ctl
|
||||
|
||||
def branch_compare():
|
||||
"""Launches a dialog for comparing a pair of branches"""
|
||||
model = BranchCompareModel()
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
view = compare.BranchCompareView(parent)
|
||||
view = vcompare.BranchCompareView(parent)
|
||||
ctl = BranchCompareController(model, view)
|
||||
view.show()
|
||||
return ctl
|
||||
|
||||
class BranchCompareController(QObserver):
|
||||
"""Provides a dialog for comparing local and remote git branches"""
|
||||
@@ -124,9 +122,20 @@ class BranchCompareController(QObserver):
|
||||
"""
|
||||
if branch == BranchCompareController.BRANCH_POINT:
|
||||
# Compare against the branch point so find the merge-base
|
||||
branch = self.model.currentbranch
|
||||
remote = gitcmds.corresponding_remote_ref()
|
||||
return self.model.git.merge_base(branch, remote)
|
||||
branch = gitcmds.current_branch()
|
||||
tracked_branch = gitcmds.tracked_branch()
|
||||
if tracked_branch:
|
||||
return self.model.git.merge_base(branch, tracked_branch)
|
||||
else:
|
||||
remote_branches = gitcmds.branch_list(remote=True)
|
||||
remote_branch = 'origin/%s' % branch
|
||||
if remote_branch in remote_branches:
|
||||
return self.model.git.merge_base(branch, remote_branch)
|
||||
|
||||
elif 'origin/master' in remote_branches:
|
||||
return self.model.git.merge_base(branch, 'origin/master')
|
||||
else:
|
||||
return 'HEAD'
|
||||
else:
|
||||
# Compare against the remote branch
|
||||
return branch
|
||||
@@ -304,11 +313,11 @@ class CompareController(QObserver):
|
||||
# get the changed files list
|
||||
start = self.model.revision_start
|
||||
end = self.model.revision_end
|
||||
files = self.model.changed_files(start, end)
|
||||
files = gitcmds.changed_files(start, end)
|
||||
|
||||
# get the old name of any renamed files, and prune them
|
||||
# from the changes list
|
||||
renamed_files = self.model.renamed_files(start, end)
|
||||
renamed_files = gitcmds.renamed_files(start, end)
|
||||
for renamed in renamed_files:
|
||||
try:
|
||||
files.remove(renamed)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""This controller handles the create branch dialog."""
|
||||
|
||||
|
||||
import os
|
||||
from PyQt4 import QtGui
|
||||
|
||||
import cola
|
||||
from cola import serializer
|
||||
from cola import gitcmds
|
||||
from cola import utils
|
||||
from cola import qtutils
|
||||
from cola.views import createbranch
|
||||
@@ -12,7 +12,7 @@ from cola.qobserver import QObserver
|
||||
|
||||
def create_new_branch(revision=''):
|
||||
"""Launches a dialog for creating a new branch"""
|
||||
model = cola.model().clone()
|
||||
model = serializer.clone(cola.model())
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
view = createbranch.CreateBranchView(parent)
|
||||
ctl = CreateBranchController(model, view)
|
||||
@@ -55,7 +55,7 @@ class CreateBranchController(QObserver):
|
||||
qtutils.information('warning', msg)
|
||||
return
|
||||
# Whether we should prompt the user for lost commits
|
||||
commits = self.model.rev_list_range(revision, branch)
|
||||
commits = gitcmds.rev_list_range(revision, branch)
|
||||
check_branch = bool(commits)
|
||||
|
||||
if check_branch:
|
||||
|
||||
@@ -10,17 +10,20 @@ from cola.models import tag
|
||||
from cola.views import createtag
|
||||
|
||||
|
||||
def create_tag():
|
||||
def create_tag(revision=''):
|
||||
"""Entry point for external callers."""
|
||||
app = QtGui.QApplication.instance()
|
||||
model = tag.TagModel()
|
||||
if revision:
|
||||
model.revision = [revision]
|
||||
view = createtag.CreateTag(parent=app.activeWindow())
|
||||
ctl = CreateTagController(view)
|
||||
ctl = CreateTagController(model, view)
|
||||
view.show()
|
||||
|
||||
|
||||
class CreateTagController(qobserver.QObserver):
|
||||
def __init__(self, view):
|
||||
qobserver.QObserver.__init__(self, tag.TagModel(), view)
|
||||
def __init__(self, model, view):
|
||||
qobserver.QObserver.__init__(self, model, view)
|
||||
self.add_observables('tag_name', 'tag_msg', 'revision', 'sign_tag')
|
||||
self.add_callbacks(create_button=self.create_tag)
|
||||
self.refresh_view()
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
"""Provides the main application controller."""
|
||||
|
||||
from PyQt4 import QtGui
|
||||
|
||||
import cola
|
||||
from cola import qtutils
|
||||
from cola import version
|
||||
from cola.qobserver import QObserver
|
||||
from cola.views import log
|
||||
|
||||
|
||||
class MainController(QObserver):
|
||||
"""Manage interactions between models and views."""
|
||||
|
||||
def __init__(self, model, view):
|
||||
"""Initializes the MainController's internal data."""
|
||||
QObserver.__init__(self, model, view)
|
||||
|
||||
# Binds model params to their equivalent view widget
|
||||
self.add_observables('commitmsg')
|
||||
|
||||
# When a model attribute changes, this runs a specific action
|
||||
self.add_actions(global_cola_fontdiff = self.update_diff_font)
|
||||
self.add_actions(global_cola_tabwidth = self.update_tab_width)
|
||||
|
||||
# Route events here
|
||||
self._init_log_window()
|
||||
self.refresh_view('global_cola_fontdiff') # Update the diff font
|
||||
|
||||
def update_diff_font(self):
|
||||
"""Updates the diff font based on the configured value."""
|
||||
qtutils.set_diff_font(qtutils.logger())
|
||||
qtutils.set_diff_font(self.view.display_text)
|
||||
qtutils.set_diff_font(self.view.commitmsg)
|
||||
|
||||
def update_tab_width(self):
|
||||
"""Implement the variable-tab-width setting."""
|
||||
tab_width = self.model.cola_config('tabwidth')
|
||||
display_font = self.view.display_text.font()
|
||||
space_width = QtGui.QFontMetrics(display_font).width(' ')
|
||||
self.view.display_text.setTabStopWidth(tab_width * space_width)
|
||||
|
||||
def _init_log_window(self):
|
||||
"""Initialize the logging subwindow."""
|
||||
branch = self.model.currentbranch
|
||||
qtutils.log(0, self.model.git_version +
|
||||
'\ncola version ' + version.version())
|
||||
@@ -1,96 +0,0 @@
|
||||
"""This controller handles the merge dialog."""
|
||||
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.Qt import Qt
|
||||
|
||||
import cola
|
||||
from cola import utils
|
||||
from cola import qtutils
|
||||
from cola.qobserver import QObserver
|
||||
from cola.views.merge import MergeView
|
||||
|
||||
def abort_merge():
|
||||
"""Prompts before aborting a merge in progress
|
||||
"""
|
||||
txt = ('Abort merge?\n'
|
||||
'Aborting the current merge will cause '
|
||||
'*ALL* uncommitted changes to be lost.\n\n'
|
||||
'Continue with aborting the current merge?')
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
model = cola.model()
|
||||
answer = qtutils.question(parent, 'Abort Merge?', txt, default=False)
|
||||
if answer:
|
||||
model.abort_merge()
|
||||
|
||||
def local_merge():
|
||||
"""Provides a dialog for merging branches"""
|
||||
model = cola.model().clone()
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
view = MergeView(parent)
|
||||
ctl = MergeController(model, view)
|
||||
view.show()
|
||||
|
||||
class MergeController(QObserver):
|
||||
"""Provide control to the merge dialog"""
|
||||
def __init__(self, model, view):
|
||||
QObserver.__init__(self, model, view)
|
||||
# Set the current branch label
|
||||
self.view.set_branch(self.model.currentbranch)
|
||||
self.add_observables('revision', 'revisions')
|
||||
self.add_callbacks(radio_local = self.radio_callback,
|
||||
radio_remote = self.radio_callback,
|
||||
radio_tag = self.radio_callback,
|
||||
revisions = self.revision_selected,
|
||||
button_viz = self.viz_revision,
|
||||
button_merge = self.merge_revision)
|
||||
self.model.set_revisions(self.model.local_branches)
|
||||
|
||||
def revisions(self):
|
||||
"""Retrieve candidate items to merge"""
|
||||
if self.view.radio_local.isChecked():
|
||||
return self.model.local_branches
|
||||
elif self.view.radio_remote.isChecked():
|
||||
return self.model.remote_branches
|
||||
elif self.view.radio_tag.isChecked():
|
||||
return self.model.tags
|
||||
return []
|
||||
|
||||
def revision_selected(self, *args):
|
||||
"""Update the revision field when a list item is selected"""
|
||||
revlist = self.revisions()
|
||||
widget = self.view.revisions
|
||||
row, selected = qtutils.selected_row(widget)
|
||||
if selected and row < len(revlist):
|
||||
revision = revlist[row]
|
||||
self.model.set_revision(revision)
|
||||
|
||||
def radio_callback(self):
|
||||
"""Update the revision list whenever a radio button is clicked"""
|
||||
self.model.set_revisions(self.revisions())
|
||||
|
||||
def merge_revision(self):
|
||||
"""Merge the selected revision/branch"""
|
||||
revision = self.model.revision
|
||||
if not revision:
|
||||
qtutils.information('No Revision Specified',
|
||||
'You must specify a revision to merge')
|
||||
return
|
||||
|
||||
no_commit = not(self.view.checkbox_commit.isChecked())
|
||||
squash = self.view.checkbox_squash.isChecked()
|
||||
msg = self.model.merge_message()
|
||||
qtutils.log(*self.model.git.merge('-m'+msg,
|
||||
revision,
|
||||
strategy='recursive',
|
||||
no_commit=no_commit,
|
||||
squash=squash,
|
||||
with_stderr=True,
|
||||
with_status=True))
|
||||
self.view.accept()
|
||||
|
||||
def viz_revision(self):
|
||||
"""Launch a gitk-like viewer on the selection revision"""
|
||||
revision = self.model.revision
|
||||
browser = self.model.history_browser()
|
||||
utils.fork([browser, revision])
|
||||
@@ -1,111 +0,0 @@
|
||||
"""This module provides the controller for the options gui
|
||||
|
||||
"""
|
||||
|
||||
from PyQt4 import QtGui
|
||||
|
||||
import cola
|
||||
from cola import qtutils
|
||||
from cola.views import option
|
||||
from cola.qobserver import QObserver
|
||||
|
||||
|
||||
def update_options():
|
||||
"""Launch the options window given a model and parent widget."""
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
view = option.OptionsView(parent)
|
||||
ctl = OptionsController(view)
|
||||
view.show()
|
||||
return view.exec_() == QtGui.QDialog.Accepted
|
||||
|
||||
|
||||
class OptionsController(QObserver):
|
||||
"""Provides control to the options dialog."""
|
||||
|
||||
def __init__(self, view):
|
||||
## operate on a clone of the original model
|
||||
QObserver.__init__(self, cola.model().clone(), view)
|
||||
|
||||
## used to restore original values when cancelling
|
||||
self._backup_model = cola.model().clone()
|
||||
|
||||
## config params modified by the gui
|
||||
self.add_observables('local_user_email',
|
||||
'local_user_name',
|
||||
'local_merge_summary',
|
||||
'local_merge_diffstat',
|
||||
'local_merge_verbosity',
|
||||
'local_gui_diffcontext',
|
||||
'global_user_email',
|
||||
'global_user_name',
|
||||
'global_merge_keepbackup',
|
||||
'global_merge_summary',
|
||||
'global_merge_diffstat',
|
||||
'global_merge_verbosity',
|
||||
'global_gui_editor',
|
||||
'global_merge_tool',
|
||||
'global_diff_tool',
|
||||
'global_gui_diffcontext',
|
||||
'global_gui_historybrowser',
|
||||
'global_cola_fontdiff_size',
|
||||
'global_cola_fontdiff',
|
||||
'global_cola_savewindowsettings',
|
||||
'global_cola_tabwidth')
|
||||
|
||||
# Refresh before registering callbacks to avoid extra notifications
|
||||
self.refresh_view()
|
||||
|
||||
# Register actions
|
||||
self.add_actions(global_cola_fontdiff = self.tell_parent_model)
|
||||
self.add_callbacks(global_cola_fontdiff_size = self.update_size)
|
||||
self.add_actions(global_cola_tabwidth = self.tell_parent_model)
|
||||
self.add_callbacks(save_button = self.save_settings)
|
||||
self.connect(self.view, 'rejected()', self.restore_settings)
|
||||
|
||||
def refresh_view(self):
|
||||
"""Apply the configured font and update widgets."""
|
||||
# The fixed-width console font
|
||||
qtutils.set_diff_font(self.view.global_cola_fontdiff)
|
||||
# Label the group box around the local repository
|
||||
self.view.local_groupbox.setTitle(unicode(self.tr('%s Repository'))
|
||||
% self.model.project)
|
||||
QObserver.refresh_view(self)
|
||||
|
||||
def save_settings(self):
|
||||
"""Save updated config variables back to git."""
|
||||
params_to_save = []
|
||||
params = self.model.config_params()
|
||||
for param in params:
|
||||
value = self.model.param(param)
|
||||
backup = self._backup_model.param(param)
|
||||
if value != backup:
|
||||
params_to_save.append(param)
|
||||
for param in params_to_save:
|
||||
self.model.save_config_param(param)
|
||||
# Update the main model with any changed parameters
|
||||
cola.model().copy_params(self.model, params_to_save)
|
||||
self.view.done(QtGui.QDialog.Accepted)
|
||||
|
||||
def restore_settings(self):
|
||||
"""Reverts any changes done in the Options dialog."""
|
||||
params = (self._backup_model.config_params() +
|
||||
['global_cola_fontdiff_size'])
|
||||
self.model.copy_params(self._backup_model, params)
|
||||
self.tell_parent_model()
|
||||
|
||||
def tell_parent_model(self,*rest):
|
||||
"""Notifies the main app's model about changed parameters"""
|
||||
params= ('global_cola_fontdiff',
|
||||
'global_cola_fontdiff_size',
|
||||
'global_cola_savewindowsettings',
|
||||
'global_cola_tabwidth')
|
||||
for param in params:
|
||||
cola.model().set_param(param, self.model.param(param))
|
||||
|
||||
def update_size(self, *rest):
|
||||
"""Updates fonts whenever font sizes change"""
|
||||
# The fixed-width console font combobox
|
||||
font = str(self.view.global_cola_fontdiff.currentFont().toString())
|
||||
default = self.model.global_cola_fontdiff or font
|
||||
self.model.apply_diff_font_size(default)
|
||||
self.tell_parent_model()
|
||||
+123
-20
@@ -1,10 +1,15 @@
|
||||
"""This controller handles the remote dialog."""
|
||||
|
||||
|
||||
import os
|
||||
from PyQt4.QtGui import QDialog
|
||||
import fnmatch
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import Qt
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
import cola
|
||||
from cola import serializer
|
||||
from cola import gitcmds
|
||||
from cola import utils
|
||||
from cola import qtutils
|
||||
from cola.views import remote
|
||||
@@ -13,7 +18,7 @@ from cola.qobserver import QObserver
|
||||
def remote_action(parent, action):
|
||||
"""Launches fetch/push/pull dialogs."""
|
||||
# TODO: subclass model
|
||||
model = cola.model()
|
||||
model = serializer.clone(cola.model())
|
||||
model.remotename = ''
|
||||
model.tags_checkbox = False
|
||||
model.rebase_checkbox = False
|
||||
@@ -23,6 +28,7 @@ def remote_action(parent, action):
|
||||
controller = RemoteController(model, view, action)
|
||||
view.show()
|
||||
|
||||
|
||||
class RemoteController(QObserver):
|
||||
"""Provides control for the remote-action dialog."""
|
||||
def __init__(self, model, view, action):
|
||||
@@ -44,6 +50,12 @@ class RemoteController(QObserver):
|
||||
'push': self.gen_remote_callback(self.model.push_helper),
|
||||
'pull': self.gen_remote_callback(self.model.pull_helper),
|
||||
} [action]
|
||||
self._tasks = []
|
||||
self.progress = QtGui.QProgressDialog(self.view)
|
||||
self.progress.setRange(0, 0)
|
||||
self.progress.setCancelButton(None)
|
||||
self.progress.setWindowTitle('git ' + action)
|
||||
self.progress.setWindowModality(Qt.WindowModal)
|
||||
"""Callbacks corresponding to the 3 (fetch/push/pull) modes"""
|
||||
|
||||
self.add_actions(remotes = self.display_remotes)
|
||||
@@ -51,6 +63,8 @@ class RemoteController(QObserver):
|
||||
remotes = self.update_remotes,
|
||||
local_branches = self.update_local_branches,
|
||||
remote_branches = self.update_remote_branches)
|
||||
self.view.connect(self.view, SIGNAL('action_completed'),
|
||||
self._action_completed)
|
||||
self.refresh_view()
|
||||
remotes = self.model.remotes
|
||||
if 'origin' in remotes:
|
||||
@@ -61,22 +75,31 @@ class RemoteController(QObserver):
|
||||
if self.view.select_first_remote():
|
||||
self.model.set_remotename(remotes[0])
|
||||
|
||||
# Trim the remote list to just the default remote
|
||||
self.update_remotes()
|
||||
|
||||
# Default to "git fetch origin master"
|
||||
if action == 'fetch' or action == 'pull':
|
||||
self.model.set_local_branch('')
|
||||
self.model.set_remote_branch('')
|
||||
return
|
||||
|
||||
# Select the current branch by default for push
|
||||
if action != 'push':
|
||||
return
|
||||
branches = self.model.local_branches
|
||||
branch = self.model.currentbranch
|
||||
if branch not in branches:
|
||||
return
|
||||
idx = branches.index(branch)
|
||||
if self.view.select_local_branch(idx):
|
||||
self.model.set_local_branch(branch)
|
||||
if action == 'push':
|
||||
branch = self.model.currentbranch
|
||||
try:
|
||||
idx = self.model.local_branches.index(branch)
|
||||
except ValueError:
|
||||
return
|
||||
if self.view.select_local_branch(idx):
|
||||
self.model.set_local_branch(branch)
|
||||
self.model.set_remote_branch('')
|
||||
|
||||
def display_remotes(self, widget):
|
||||
"""Display the available remotes in a listwidget"""
|
||||
displayed = []
|
||||
for remotename in self.model.remotes:
|
||||
url = self.model.remote_url(remotename)
|
||||
url = self.model.remote_url(remotename, self.action)
|
||||
display = ('%s\t(%s %s)'
|
||||
% (remotename, unicode(self.tr('URL:')), url))
|
||||
displayed.append(display)
|
||||
@@ -92,6 +115,20 @@ class RemoteController(QObserver):
|
||||
self.model.set_remotename(selection)
|
||||
self.view.remotename.selectAll()
|
||||
|
||||
if self.action != 'pull':
|
||||
pass
|
||||
all_branches = gitcmds.branch_list(remote=True)
|
||||
branches = []
|
||||
pat = selection + '/*'
|
||||
for branch in all_branches:
|
||||
if fnmatch.fnmatch(branch, pat):
|
||||
branches.append(branch)
|
||||
if branches:
|
||||
self.model.set_remote_branches(branches)
|
||||
else:
|
||||
self.model.set_remote_branches(all_branches)
|
||||
self.model.set_remote_branch('')
|
||||
|
||||
def update_local_branches(self,*rest):
|
||||
"""Update the local/remote branch names when a branch is selected"""
|
||||
branches = self.model.local_branches
|
||||
@@ -141,7 +178,20 @@ class RemoteController(QObserver):
|
||||
errmsg = self.tr('No repository selected.')
|
||||
qtutils.log(1, errmsg)
|
||||
return
|
||||
remote, kwargs = self.common_args()
|
||||
action = self.action
|
||||
|
||||
# Check if we're about to create a new branch and warn.
|
||||
if action == 'push' and not self.model.remote_branch:
|
||||
branch = self.model.local_branch
|
||||
candidate = '%s/%s' % (remote, branch)
|
||||
if candidate not in self.model.remote_branches:
|
||||
msg = ('Branch "' + branch + '" does not exist in ' +
|
||||
remote + '.\n\nCreate a new branch?')
|
||||
if not qtutils.question(self.view, 'Create New Branch?',
|
||||
msg, default=False):
|
||||
return
|
||||
|
||||
if not self.model.ffwd_only_checkbox:
|
||||
if action == 'fetch':
|
||||
msg = ('Non-fast-forward fetch overwrites local '
|
||||
@@ -155,11 +205,64 @@ class RemoteController(QObserver):
|
||||
if not qtutils.question(self.view,
|
||||
'Force %s?' % action.title(), msg, default=False):
|
||||
return
|
||||
remote, kwargs = self.common_args()
|
||||
status, output = modelaction(remote, **kwargs)
|
||||
if not output: # git fetch --tags --verbose doesn't print anything...
|
||||
output = self.tr('Already up-to-date.')
|
||||
# Force the status to 1 so that we always display the log
|
||||
qtutils.log(1, output)
|
||||
self.view.accept()
|
||||
|
||||
# Disable the GUI by default
|
||||
self.view.setEnabled(False)
|
||||
self.progress.setEnabled(True)
|
||||
QtGui.QApplication.setOverrideCursor(Qt.WaitCursor)
|
||||
|
||||
# Show a nice progress bar
|
||||
self.progress.setLabelText('Updating...')
|
||||
self.progress.show()
|
||||
|
||||
# Use a thread to update in the background
|
||||
task = ActionTask(self.view, modelaction, remote, kwargs)
|
||||
self._tasks.append(task)
|
||||
QtCore.QThreadPool.globalInstance().start(task)
|
||||
|
||||
return remote_callback
|
||||
|
||||
|
||||
def _action_completed(self, task, status, output):
|
||||
# Grab the results of the action and finish up
|
||||
if task in self._tasks:
|
||||
self._tasks.remove(task)
|
||||
|
||||
if not output: # git fetch --tags --verbose doesn't print anything...
|
||||
output = self.tr('Already up-to-date.')
|
||||
# Force the status to 1 so that we always display the log
|
||||
qtutils.log(1, output)
|
||||
|
||||
self.progress.close()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if status != 0 and self.action == 'push':
|
||||
message = 'Error pushing to "%s".\n\nPull first?' % self.model.remotename
|
||||
qtutils.critical('Push Error',
|
||||
message=message, details=output)
|
||||
else:
|
||||
title = self.view.windowTitle()
|
||||
if status == 0:
|
||||
result = 'succeeded'
|
||||
else:
|
||||
result = 'returned exit status %d' % status
|
||||
|
||||
message = '"git %s" %s' % (self.action, result)
|
||||
qtutils.information(title,
|
||||
parent=self.view,
|
||||
message=message, details=output)
|
||||
self.view.accept()
|
||||
|
||||
|
||||
class ActionTask(QtCore.QRunnable):
|
||||
def __init__(self, sender, modelaction, remote, kwargs):
|
||||
QtCore.QRunnable.__init__(self)
|
||||
self._sender = sender
|
||||
self._modelaction = modelaction
|
||||
self._remote = remote
|
||||
self._kwargs = kwargs
|
||||
|
||||
def run(self):
|
||||
"""Runs the model action and captures the result"""
|
||||
status, output = self._modelaction(self._remote, **self._kwargs)
|
||||
self._sender.emit(SIGNAL('action_completed'), self, status, output)
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
"""This controller handles the repository file browser."""
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from PyQt4 import QtGui
|
||||
|
||||
import cola
|
||||
from cola import utils
|
||||
from cola import resources
|
||||
from cola import qtutils
|
||||
from cola.views.selectcommits import SelectCommitsView
|
||||
from cola.qobserver import QObserver
|
||||
|
||||
|
||||
def select_file_from_repo():
|
||||
"""Launche a dialog to selecting a filename from a branch."""
|
||||
model = cola.model().clone()
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
view = SelectCommitsView(parent, syntax=False)
|
||||
controller = RepoBrowserController(model, view,
|
||||
title='Select File',
|
||||
get_file=True)
|
||||
view.show()
|
||||
if view.exec_() == QtGui.QDialog.Accepted:
|
||||
return controller.filename
|
||||
else:
|
||||
return None
|
||||
|
||||
def browse_git_branch(branch):
|
||||
"""Launch a dialog to browse files in a specific branch."""
|
||||
if not branch:
|
||||
return
|
||||
# Clone the model to allow opening multiple browsers
|
||||
# with different sets of data
|
||||
model = cola.model().clone()
|
||||
model.set_currentbranch(branch)
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
view = SelectCommitsView(parent, syntax=False)
|
||||
controller = RepoBrowserController(model, view)
|
||||
view.show()
|
||||
return view.exec_() == QtGui.QDialog.Accepted
|
||||
|
||||
class RepoBrowserController(QObserver):
|
||||
"""Provides control to the Repository Browser."""
|
||||
def __init__(self, model, view,
|
||||
title='File Browser', get_file=False):
|
||||
QObserver.__init__(self, model, view)
|
||||
|
||||
self.get_file = get_file
|
||||
"""Whether we should returns a selected file"""
|
||||
|
||||
self.filename = None
|
||||
"""The last selected filename"""
|
||||
|
||||
view.setWindowTitle(title)
|
||||
self.add_signals('itemSelectionChanged()', view.commit_list,)
|
||||
self.add_actions(directory = self.action_directory_changed)
|
||||
self.add_callbacks(commit_list = self.item_changed)
|
||||
view.commit_list.contextMenuEvent = self.context_menu_event
|
||||
# Start at the root of the tree
|
||||
model.set_directory('')
|
||||
self.refresh_view()
|
||||
|
||||
def context_menu_event(self, event):
|
||||
"""Generate a context menu for the repository browser."""
|
||||
menu = QtGui.QMenu(self.view);
|
||||
menu.addAction(self.tr('Blame'), self.blame)
|
||||
menu.exec_(self.view.commit_list.mapToGlobal(event.pos()))
|
||||
|
||||
def blame(self):
|
||||
"""Show git-blame output for a file path."""
|
||||
current = self.view.commit_list.currentRow()
|
||||
item = self.view.commit_list.item(current)
|
||||
if item is None or not item.isSelected():
|
||||
return
|
||||
directories = self.model.directories
|
||||
directory_entries = self.model.directory_entries
|
||||
if current < len(directories):
|
||||
# ignore directories
|
||||
return
|
||||
idx = current - len(directories)
|
||||
if idx >= len(self.model.subtree_sha1s):
|
||||
return
|
||||
objtype, sha1, name = self.model.subtree_node(idx)
|
||||
curdir = self.model.directory
|
||||
if curdir:
|
||||
filename = os.path.join(curdir, name)
|
||||
else:
|
||||
filename = name
|
||||
blame = self.model.git.blame(self.model.currentbranch, filename)
|
||||
self.view.commit_text.setText(blame)
|
||||
|
||||
######################################################################
|
||||
# Actions
|
||||
def action_directory_changed(self):
|
||||
"""Called in response to a change in the model's directory."""
|
||||
self.model.init_browser_data()
|
||||
self._display_items()
|
||||
|
||||
######################################################################
|
||||
# Qt callbacks
|
||||
def item_changed(self,*rest):
|
||||
"""Called when the current item changes"""
|
||||
current = self.view.commit_list.currentRow()
|
||||
item = self.view.commit_list.item(current)
|
||||
if item is None or not item.isSelected():
|
||||
self.view.revision.setText('')
|
||||
self.view.commit_text.setText('')
|
||||
return
|
||||
directories = self.model.directories
|
||||
directory_entries = self.model.directory_entries
|
||||
if current < len(directories):
|
||||
# This is a directory...
|
||||
self.filename = None
|
||||
dirent = directories[current]
|
||||
if dirent != '..':
|
||||
# This is a real directory for which
|
||||
# we have child entries
|
||||
entries = directory_entries[dirent]
|
||||
else:
|
||||
# This is '..' which is a special case
|
||||
# since it doesn't really exist
|
||||
entries = []
|
||||
self.view.commit_text.setText('\n'.join(entries))
|
||||
self.view.revision.setText('')
|
||||
else:
|
||||
# This is a file entry. The current row is absolute,
|
||||
# so get a relative index by subtracting the number
|
||||
# of directory entries
|
||||
idx = current - len(directories)
|
||||
if idx >= len(self.model.subtree_sha1s):
|
||||
# This can happen when changing directories
|
||||
self.filename = None
|
||||
return
|
||||
objtype, sha1, name = self.model.subtree_node(idx)
|
||||
|
||||
curdir = self.model.directory
|
||||
if curdir:
|
||||
self.filename = os.path.join(curdir, name)
|
||||
else:
|
||||
self.filename = name
|
||||
|
||||
catguts = self.model.git.cat_file(objtype, sha1,
|
||||
with_raw_output=True)
|
||||
self.view.commit_text.setText(catguts)
|
||||
|
||||
self.view.revision.setText(sha1)
|
||||
self.view.revision.selectAll()
|
||||
|
||||
# Copy the sha1 into the clipboard
|
||||
qtutils.set_clipboard(sha1)
|
||||
|
||||
# automatically called by qobserver
|
||||
def commit_list_doubleclick(self,*rest):
|
||||
"""
|
||||
Called when an entry is double-clicked.
|
||||
|
||||
This callback changes the model's directory when
|
||||
invoked on a directory item. When invoked on a file
|
||||
it allows the file to be saved.
|
||||
|
||||
"""
|
||||
current = self.view.commit_list.currentRow()
|
||||
directories = self.model.directories
|
||||
|
||||
# A file item was double-clicked.
|
||||
# Create a save-as dialog and export the file,
|
||||
# or if in get_file mode, grab the filename and finish the dialog.
|
||||
if current >= len(directories):
|
||||
idx = current - len(directories)
|
||||
|
||||
objtype, sha1, name = self.model.subtree_node(idx)
|
||||
|
||||
if self.get_file:
|
||||
if self.model.directory:
|
||||
curdir = self.model.directory
|
||||
self.filename = os.path.join(curdir, name)
|
||||
else:
|
||||
self.filename = name
|
||||
self.view.accept()
|
||||
return
|
||||
|
||||
nameguess = os.path.join(self.model.directory, name)
|
||||
filename = qtutils.save_dialog(self.view, 'Save', nameguess)
|
||||
if not filename:
|
||||
return
|
||||
self.model.set_directory(os.path.dirname(filename))
|
||||
contents = self.model.git.cat_file(objtype, sha1,
|
||||
with_raw_output=True)
|
||||
utils.write(filename, contents)
|
||||
return
|
||||
|
||||
dirent = directories[current]
|
||||
curdir = self.model.directory
|
||||
|
||||
# "change directories"
|
||||
# '..' is a special case--it doesn't really exist...
|
||||
if dirent == '..':
|
||||
newdir = os.path.dirname(os.path.dirname(curdir))
|
||||
if newdir == '':
|
||||
self.model.set_directory(newdir)
|
||||
else:
|
||||
self.model.set_directory(newdir + os.sep)
|
||||
else:
|
||||
self.model.set_directory(curdir + dirent)
|
||||
|
||||
def _display_items(self):
|
||||
"""
|
||||
Populate the commit_list with the current directories and items
|
||||
|
||||
Directories are always listed first.
|
||||
|
||||
"""
|
||||
|
||||
# Clear commit/revision fields
|
||||
self.view.commit_text.setText('')
|
||||
self.view.revision.setText('')
|
||||
|
||||
dir_icon = resources.icon('dir.png')
|
||||
file_icon = resources.icon('generic.png')
|
||||
# Factory method for creating items
|
||||
creator = qtutils.create_listwidget_item
|
||||
|
||||
# First the directories,
|
||||
qtutils.set_items(self.view.commit_list,
|
||||
map(lambda d: creator(d, dir_icon),
|
||||
self.model.directories))
|
||||
# and now the filenames
|
||||
qtutils.add_items(self.view.commit_list,
|
||||
map(lambda s: creator(s, file_icon),
|
||||
self.model.subtree_names))
|
||||
+24
-28
@@ -4,9 +4,13 @@
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
from PyQt4 import QtGui
|
||||
|
||||
import cola
|
||||
from cola import gitcmds
|
||||
from cola import qtutils
|
||||
from cola.qobserver import QObserver
|
||||
from cola.models.search import SearchModel
|
||||
@@ -24,23 +28,22 @@ COMMITTER = 'radio_committer'
|
||||
DATE_RANGE = 'radio_daterange'
|
||||
|
||||
|
||||
def search(searchtype, browse=False):
|
||||
def search():
|
||||
"""Return a callback to handle various search actions."""
|
||||
def search_handler():
|
||||
search_commits(cola.model(),
|
||||
QtGui.QApplication.instance().activeWindow(),
|
||||
searchtype,
|
||||
browse)
|
||||
return search_handler
|
||||
|
||||
search_commits(cola.model(),
|
||||
QtGui.QApplication.instance().activeWindow())
|
||||
|
||||
class SearchEngine(object):
|
||||
def __init__(self, model):
|
||||
self.model = model
|
||||
|
||||
def rev_args(self):
|
||||
max = self.model.max_results
|
||||
return { 'max-count': max, 'pretty': 'format:%H %aN - %s - %ar' }
|
||||
max_count = self.model.max_results
|
||||
return {
|
||||
'no_color': True,
|
||||
'max-count': max_count,
|
||||
'pretty': 'format:%H %aN - %s - %ar',
|
||||
}
|
||||
|
||||
def common_args(self):
|
||||
return (self.model.query, self.rev_args())
|
||||
@@ -55,7 +58,7 @@ class SearchEngine(object):
|
||||
|
||||
def revisions(self, *args, **kwargs):
|
||||
revlist = self.model.git.log(*args, **kwargs)
|
||||
return self.model.parse_rev_list(revlist)
|
||||
return gitcmds.parse_rev_list(revlist)
|
||||
|
||||
def results(self):
|
||||
pass
|
||||
@@ -80,7 +83,10 @@ class RevisionRangeSearch(SearchEngine):
|
||||
class PathSearch(SearchEngine):
|
||||
def results(self):
|
||||
query, args = self.common_args()
|
||||
paths = ['--'] + query.split(':')
|
||||
try:
|
||||
paths = ['--'] + shlex.split(str(query))
|
||||
except UnicodeEncodeError:
|
||||
paths = ['--'] + query.split(' ')
|
||||
return self.revisions(all=True, *paths, **args)
|
||||
|
||||
class MessageSearch(SearchEngine):
|
||||
@@ -101,7 +107,7 @@ class CommitterSearch(SearchEngine):
|
||||
class DiffSearch(SearchEngine):
|
||||
def results(self):
|
||||
query, kwargs = self.common_args()
|
||||
return self.model.parse_rev_list(
|
||||
return gitcmds.parse_rev_list(
|
||||
self.model.git.log('-S'+query, all=True, **kwargs))
|
||||
|
||||
class DateRangeSearch(SearchEngine):
|
||||
@@ -157,14 +163,6 @@ class SearchController(QObserver):
|
||||
radio_committer = self.search_callback,
|
||||
radio_daterange = self.search_callback,
|
||||
)
|
||||
self.update_fonts()
|
||||
|
||||
def update_fonts(self):
|
||||
font = self.model.cola_config('fontdiff')
|
||||
if font:
|
||||
qfont = QtGui.QFont()
|
||||
qfont.fromString(font)
|
||||
self.view.commit_text.setFont(qfont)
|
||||
|
||||
def set_mode(self, mode):
|
||||
radio = getattr(self.view, mode)
|
||||
@@ -202,7 +200,7 @@ class SearchController(QObserver):
|
||||
if not path.startswith(os.getcwd()):
|
||||
continue
|
||||
filepaths.append(path[lenprefix:])
|
||||
query = ':'.join(filepaths)
|
||||
query = subprocess.list2cmdline(filepaths)
|
||||
self.model.set_query('')
|
||||
self.set_mode(PATH)
|
||||
self.model.set_query(query)
|
||||
@@ -219,7 +217,7 @@ class SearchController(QObserver):
|
||||
return
|
||||
revision = self.results[row][0]
|
||||
qtutils.set_clipboard(revision)
|
||||
diff = self.model.commit_diff(revision)
|
||||
diff = gitcmds.commit_diff(revision)
|
||||
self.view.commit_text.setText(diff)
|
||||
|
||||
def export_patch(self):
|
||||
@@ -240,7 +238,7 @@ class SearchController(QObserver):
|
||||
with_stderr=True,
|
||||
with_status=True))
|
||||
|
||||
def search_commits(model, parent, mode, browse):
|
||||
def search_commits(model, parent):
|
||||
def date(timespec):
|
||||
return '%04d-%02d-%02d' % time.localtime(timespec)[:3]
|
||||
|
||||
@@ -248,9 +246,7 @@ def search_commits(model, parent, mode, browse):
|
||||
model = SearchModel(cwd=model.git.worktree())
|
||||
view = SearchView(parent)
|
||||
ctl = SearchController(model, view)
|
||||
ctl.set_mode(mode)
|
||||
model.set_start_date(date(time.time()-(87640*7)))
|
||||
ctl.set_mode(MESSAGE)
|
||||
model.set_start_date(date(time.time()-(87640*31)))
|
||||
model.set_end_date(date(time.time()+87640))
|
||||
view.show()
|
||||
if browse:
|
||||
ctl.browse_callback()
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
"""
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
import cola
|
||||
from cola import gitcmds
|
||||
from cola import qtutils
|
||||
from cola import serializer
|
||||
from cola.prefs import diff_font
|
||||
from cola.views.selectcommits import SelectCommitsView
|
||||
from cola.qobserver import QObserver
|
||||
from cola.controllers.createbranch import create_new_branch
|
||||
@@ -24,26 +28,16 @@ class SelectCommitsController(QObserver):
|
||||
|
||||
def __init__(self, model, view, revs, summaries):
|
||||
self.orig_model = model
|
||||
QObserver.__init__(self, model.clone(), view)
|
||||
QObserver.__init__(self, serializer.clone(model), view)
|
||||
|
||||
self.model.set_revisions(revs)
|
||||
self.model.set_summaries(summaries)
|
||||
|
||||
self.connect(view.commit_list,
|
||||
'itemSelectionChanged()',
|
||||
SIGNAL('itemSelectionChanged()'),
|
||||
self.commit_sha1_selected)
|
||||
view.commit_list.contextMenuEvent = self.context_menu_event
|
||||
self.set_diff_font()
|
||||
|
||||
def set_diff_font(self):
|
||||
if not self.model.has_param('global_cola_fontdiff'):
|
||||
return
|
||||
font = self.model.param('global_cola_fontdiff')
|
||||
if not font:
|
||||
return
|
||||
qf = QtGui.QFont()
|
||||
qf.fromString(font)
|
||||
self.view.commit_text.setFont(qf)
|
||||
self.view.commit_text.setFont(diff_font())
|
||||
|
||||
def select_commits(self):
|
||||
summaries = self.model.summaries
|
||||
@@ -103,7 +97,7 @@ class SelectCommitsController(QObserver):
|
||||
self.view.revision.selectAll()
|
||||
|
||||
# Lookup the sha1's commit
|
||||
commit_diff = self.model.commit_diff(sha1)
|
||||
commit_diff = gitcmds.commit_diff(sha1)
|
||||
self.view.commit_text.setText(commit_diff)
|
||||
|
||||
# Copy the sha1 into the clipboard
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
"""This controller handles the stash dialog."""
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from PyQt4 import QtGui
|
||||
|
||||
import cola
|
||||
from cola import utils
|
||||
from cola import qtutils
|
||||
from cola import signals
|
||||
from cola.qobserver import QObserver
|
||||
from cola.views import stash as stashmod
|
||||
|
||||
def stash():
|
||||
"""Launches a stash dialog using the provided model + view
|
||||
"""
|
||||
model = cola.model()
|
||||
model.keep_index = True
|
||||
model.stash_list = []
|
||||
model.stash_revids = []
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
view = stashmod.StashView(parent)
|
||||
ctl = StashController(model, view)
|
||||
view.show()
|
||||
|
||||
|
||||
class StashController(QObserver):
|
||||
"""The StashController is the brains behind the 'Stash' dialog
|
||||
"""
|
||||
def __init__(self, model, view):
|
||||
QObserver.__init__(self, model, view)
|
||||
self.add_observables('stash_list', 'keep_index')
|
||||
self.add_callbacks(button_stash_show = self.stash_show,
|
||||
button_stash_apply = self.stash_apply,
|
||||
button_stash_drop = self.stash_drop,
|
||||
button_stash_clear = self.stash_clear,
|
||||
button_stash_save = self.stash_save)
|
||||
self.update_model()
|
||||
|
||||
def update_model(self):
|
||||
"""Initiates git queries on the model and updates the view
|
||||
"""
|
||||
self.model.set_stash_list(self.model.parse_stash_list())
|
||||
self.model.set_stash_revids(self.model.parse_stash_list(revids=True))
|
||||
self.refresh_view()
|
||||
|
||||
def selected_stash(self):
|
||||
"""Returns the stash name of the currently selected stash
|
||||
"""
|
||||
list_widget = self.view.stash_list
|
||||
stash_list = self.model.stash_revids
|
||||
return qtutils.selected_item(list_widget, stash_list)
|
||||
|
||||
def stash_save(self):
|
||||
"""Saves the worktree in a stash
|
||||
|
||||
This prompts the user for a stash name and creates
|
||||
a git stash named accordingly.
|
||||
"""
|
||||
if not qtutils.question(self.view,
|
||||
'Stash Changes?',
|
||||
'This will stash your current '
|
||||
'changes away for later use.\n'
|
||||
'Continue?'):
|
||||
return
|
||||
|
||||
stash_name, ok = qtutils.prompt('Enter a name for this stash')
|
||||
if not ok:
|
||||
return
|
||||
while stash_name in self.model.stash_list:
|
||||
qtutils.information('Oops!',
|
||||
'That name already exists. '
|
||||
'Please enter another name.')
|
||||
stash_name, ok = qtutils.prompt('Enter a name for this stash')
|
||||
if not ok:
|
||||
return
|
||||
|
||||
if not stash_name:
|
||||
return
|
||||
|
||||
# Sanitize the stash name
|
||||
stash_name = utils.sanitize(stash_name)
|
||||
args = []
|
||||
if self.model.keep_index:
|
||||
args.append('--keep-index')
|
||||
args.append(stash_name)
|
||||
|
||||
qtutils.log(*self.model.git.stash('save',
|
||||
with_stderr=True,
|
||||
with_status=True,
|
||||
*args))
|
||||
self.view.accept()
|
||||
|
||||
def stash_show(self):
|
||||
"""Shows the current stash in the main view."""
|
||||
selection = self.selected_stash()
|
||||
if not selection:
|
||||
return
|
||||
diffstat = self.model.git.stash('show', selection)
|
||||
diff = self.model.git.stash('show', '-p', selection)
|
||||
cola.notifier().broadcast(signals.diff_text, '%s\n\n%s' % (diffstat, diff))
|
||||
|
||||
def stash_apply(self):
|
||||
"""Applies the currently selected stash
|
||||
"""
|
||||
selection = self.selected_stash()
|
||||
if not selection:
|
||||
return
|
||||
qtutils.log(*self.model.git.stash('apply', '--index', selection,
|
||||
with_stderr=True,
|
||||
with_status=True))
|
||||
self.view.accept()
|
||||
|
||||
def stash_drop(self):
|
||||
"""Drops the currently selected stash
|
||||
"""
|
||||
selection = self.selected_stash()
|
||||
if not selection:
|
||||
return
|
||||
if not qtutils.question(self.view,
|
||||
'Drop Stash?',
|
||||
'This will permanently remove the '
|
||||
'selected stash.\n'
|
||||
'Recovering these changes may not '
|
||||
'be possible.\n\n'
|
||||
'Continue?'):
|
||||
return
|
||||
qtutils.log(*self.model.git.stash('drop', selection,
|
||||
with_stderr=True,
|
||||
with_status=True))
|
||||
self.update_model()
|
||||
|
||||
def stash_clear(self):
|
||||
"""Clears all stashes
|
||||
"""
|
||||
if not qtutils.question(self.view,
|
||||
'Drop All Stashes?',
|
||||
'This will permanently remove '
|
||||
'ALL stashed changes.\n'
|
||||
'Recovering these changes may not '
|
||||
'be possible.\n\n'
|
||||
'Continue?'):
|
||||
return
|
||||
self.model.git.stash('clear'),
|
||||
self.update_model()
|
||||
@@ -1,25 +0,0 @@
|
||||
"""This module provides utility controllers.
|
||||
"""
|
||||
from PyQt4 import QtGui
|
||||
|
||||
from cola import qtutils
|
||||
from cola.views import itemlist
|
||||
from cola.views import combo
|
||||
from cola.qobserver import QObserver
|
||||
|
||||
|
||||
def choose_from_combo(title, items):
|
||||
"""Quickly choose an item from a list using a combo box"""
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
return combo.ComboView(parent,
|
||||
title=title,
|
||||
items=items).selected()
|
||||
|
||||
|
||||
def choose_from_list(title, items=None, dblclick=None):
|
||||
"""Quickly choose an item from a list using a list widget"""
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
return itemlist.ListView(parent,
|
||||
title=title,
|
||||
items=items,
|
||||
dblclick=dblclick).selected()
|
||||
@@ -0,0 +1,34 @@
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
from cola.qtutils import SLOT
|
||||
|
||||
from cola.cmdfactory import CommandFactory
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self, model, view):
|
||||
self.model = model
|
||||
self.view = view
|
||||
self.commands = {}
|
||||
self.factory = CommandFactory(model)
|
||||
|
||||
def add_command_directory(self, command_directory):
|
||||
for signal, cmdclass in command_directory.items():
|
||||
self.factory.add_command(signal, cmdclass)
|
||||
|
||||
def add_command(self, signal, handler=None):
|
||||
if handler is None:
|
||||
handler = self.do_wrapper(signal)
|
||||
self.view.connect(self.view, SIGNAL(signal), handler)
|
||||
|
||||
def add_commands(self, command_directory, handler=None):
|
||||
for signal, cmdclass in command_directory.items():
|
||||
self.factory.add_command(signal, cmdclass)
|
||||
self.add_command(signal, handler=handler)
|
||||
|
||||
def add_global_command(self, signal):
|
||||
self.view.connect(self.view, SIGNAL(signal), SLOT(signal))
|
||||
|
||||
def do_wrapper(self, signal):
|
||||
def do(*args, **opts):
|
||||
return self.factory.do(signal, *args, **opts)
|
||||
return do
|
||||
@@ -0,0 +1,41 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PyQt4 import QtCore
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Find the source tree
|
||||
src = os.path.join(os.path.dirname(__file__), '..', '..')
|
||||
sys.path.insert(1, os.path.join(os.path.abspath(src), 'thirdparty'))
|
||||
sys.path.insert(1, os.path.abspath(src))
|
||||
|
||||
from cola.dag.view import DAGView
|
||||
from cola.dag.model import DAG
|
||||
from cola.dag.controller import DAGController
|
||||
|
||||
|
||||
def git_dag(model, parent, opts=None, args=None):
|
||||
"""Return a pre-populated git DAG widget."""
|
||||
dag = DAG(model.currentbranch, 1000)
|
||||
dag.set_options(opts, args)
|
||||
|
||||
view = DAGView(model, dag, parent=parent)
|
||||
ctl = DAGController(dag, view)
|
||||
view.show()
|
||||
view.raise_()
|
||||
if dag.ref:
|
||||
view.thread.start(QtCore.QThread.LowPriority)
|
||||
return ctl
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import cola
|
||||
from cola import app
|
||||
|
||||
model = cola.model()
|
||||
model.use_worktree(os.getcwd())
|
||||
model.update_status()
|
||||
|
||||
app = app.ColaApplication(sys.argv)
|
||||
ctl = git_dag(model, app.activeWindow())
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1,8 @@
|
||||
from cola.ctrl import Controller
|
||||
from cola.dag.model import command_directory
|
||||
|
||||
|
||||
class DAGController(Controller):
|
||||
def __init__(self, model, view):
|
||||
Controller.__init__(self, model, view)
|
||||
self.add_commands(command_directory)
|
||||
@@ -0,0 +1,264 @@
|
||||
import subprocess
|
||||
|
||||
import cola
|
||||
from cola import core
|
||||
from cola import git
|
||||
from cola import signals
|
||||
from cola import utils
|
||||
from cola.cmds import BaseCommand
|
||||
from cola.observable import Observable
|
||||
|
||||
# put subject at the end b/c it can contain
|
||||
# any number of funky characters
|
||||
logfmt = 'format:%H%x01%P%x01%d%x01%an%x01%aD%x01%s'
|
||||
git = git.instance()
|
||||
|
||||
archive = 'archive'
|
||||
|
||||
|
||||
class CommitFactory(object):
|
||||
root_generation = 0
|
||||
_commits = {}
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
cls._commits.clear()
|
||||
cls.root_generation = 0
|
||||
|
||||
@classmethod
|
||||
def new(cls, sha1=None, log_entry=None):
|
||||
if not sha1 and log_entry:
|
||||
sha1 = log_entry[:40]
|
||||
try:
|
||||
commit = cls._commits[sha1]
|
||||
if log_entry and not commit.parsed:
|
||||
commit.parse(log_entry)
|
||||
cls.root_generation = max(commit.generation,
|
||||
cls.root_generation)
|
||||
except KeyError:
|
||||
commit = Commit(sha1=sha1,
|
||||
log_entry=log_entry)
|
||||
if not log_entry:
|
||||
cls.root_generation += 1
|
||||
commit.generation = max(commit.generation,
|
||||
cls.root_generation)
|
||||
cls._commits[sha1] = commit
|
||||
return commit
|
||||
|
||||
|
||||
class DAG(Observable):
|
||||
ref_updated = 'ref_updated'
|
||||
count_updated = 'count_updated'
|
||||
|
||||
def __init__(self, ref, count):
|
||||
Observable.__init__(self)
|
||||
self.ref = ref
|
||||
self.count = count
|
||||
self.overrides = {}
|
||||
|
||||
def set_ref(self, ref):
|
||||
changed = ref != self.ref
|
||||
self.ref = ref
|
||||
self.notify_message_observers(self.ref_updated)
|
||||
return changed
|
||||
|
||||
def set_count(self, count):
|
||||
changed = count != self.count
|
||||
self.count = count
|
||||
self.notify_message_observers(self.count_updated)
|
||||
return changed
|
||||
|
||||
def set_options(self, opts, args):
|
||||
if opts is not None:
|
||||
if self.set_count(opts.count):
|
||||
self.overrides['count'] = opts.count
|
||||
|
||||
if args is not None:
|
||||
ref = subprocess.list2cmdline([core.decode(a) for a in args])
|
||||
if self.set_ref(ref):
|
||||
self.overrides['ref'] = ref
|
||||
|
||||
def overridden(self, opt):
|
||||
return opt in self.overrides
|
||||
|
||||
|
||||
class Commit(object):
|
||||
root_generation = 0
|
||||
|
||||
__slots__ = ('sha1',
|
||||
'subject',
|
||||
'parents',
|
||||
'children',
|
||||
'tags',
|
||||
'author',
|
||||
'authdate',
|
||||
'generation',
|
||||
'parsed')
|
||||
def __init__(self, sha1=None, log_entry=None):
|
||||
self.sha1 = sha1
|
||||
self.subject = None
|
||||
self.parents = []
|
||||
self.children = []
|
||||
self.tags = set()
|
||||
self.author = None
|
||||
self.authdate = None
|
||||
self.parsed = False
|
||||
self.generation = CommitFactory.root_generation
|
||||
if log_entry:
|
||||
self.parse(log_entry)
|
||||
|
||||
def parse(self, log_entry):
|
||||
self.sha1 = log_entry[:40]
|
||||
(parents, tags, author, authdate, subject) = \
|
||||
log_entry[41:].split(chr(0x01), 5)
|
||||
|
||||
if subject:
|
||||
self.subject = core.decode(subject)
|
||||
|
||||
if parents:
|
||||
generation = None
|
||||
for parent_sha1 in parents.split(' '):
|
||||
parent = CommitFactory.new(sha1=parent_sha1)
|
||||
parent.children.append(self)
|
||||
if generation is None:
|
||||
generation = parent.generation+1
|
||||
self.parents.append(parent)
|
||||
generation = max(parent.generation+1, generation)
|
||||
self.generation = generation
|
||||
|
||||
if tags:
|
||||
for tag in tags[2:-1].split(', '):
|
||||
if tag.startswith('tag: '):
|
||||
tag = tag[5:] # tag: refs/
|
||||
elif tag.startswith('refs/remotes/'):
|
||||
tag = tag[13:] # refs/remotes/
|
||||
elif tag.startswith('refs/heads/'):
|
||||
tag = tag[11:] # refs/heads/
|
||||
if tag.endswith('/HEAD'):
|
||||
continue
|
||||
self.tags.add(core.decode(tag))
|
||||
if author:
|
||||
self.author = core.decode(author)
|
||||
if authdate:
|
||||
self.authdate = authdate
|
||||
|
||||
self.parsed = True
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return self.sha1
|
||||
|
||||
def __repr__(self):
|
||||
return ("{\n"
|
||||
" sha1: " + self.sha1 + "\n"
|
||||
" subject: " + self.subject + "\n"
|
||||
" author: " + self.author + "\n"
|
||||
" authdate: " + self.authdate + "\n"
|
||||
" parents: [" + ', '.join(self.parents) + "]\n"
|
||||
" tags: [" + ', '.join(self.tags) + "]\n"
|
||||
"}")
|
||||
|
||||
|
||||
class RepoReader(object):
|
||||
def __init__(self, dag, git=git):
|
||||
self.dag = dag
|
||||
self.git = git
|
||||
self._proc = None
|
||||
self._objects = {}
|
||||
self._cmd = ['git', 'log',
|
||||
'--topo-order',
|
||||
'--reverse',
|
||||
'--pretty='+logfmt]
|
||||
self._cached = False
|
||||
"""Indicates that all data has been read"""
|
||||
self._idx = -1
|
||||
"""Index into the cached commits"""
|
||||
self._topo_list = []
|
||||
"""List of commits objects in topological order"""
|
||||
|
||||
cached = property(lambda self: self._cached)
|
||||
"""Return True when no commits remain to be read"""
|
||||
|
||||
|
||||
def __len__(self):
|
||||
return len(self._topo_list)
|
||||
|
||||
def reset(self):
|
||||
CommitFactory.reset()
|
||||
if self._proc:
|
||||
self._topo_list = []
|
||||
self._proc.kill()
|
||||
self._proc = None
|
||||
self._cached = False
|
||||
|
||||
def __iter__(self):
|
||||
if self._cached:
|
||||
return self
|
||||
self.reset()
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
if self._cached:
|
||||
try:
|
||||
self._idx += 1
|
||||
return self._topo_list[self._idx]
|
||||
except IndexError:
|
||||
self._idx = -1
|
||||
raise StopIteration
|
||||
if self._proc is None:
|
||||
ref_args = utils.shell_split(self.dag.ref)
|
||||
cmd = self._cmd + ['-%d' % self.dag.count] + ref_args
|
||||
self._proc = utils.start_command(cmd)
|
||||
self._topo_list = []
|
||||
log_entry = self._proc.stdout.readline().rstrip()
|
||||
if not log_entry:
|
||||
del self._proc
|
||||
self._cached = True
|
||||
self._proc = None
|
||||
raise StopIteration
|
||||
sha1 = log_entry[:40]
|
||||
try:
|
||||
return self._objects[sha1]
|
||||
except KeyError:
|
||||
c = CommitFactory.new(log_entry=log_entry)
|
||||
self._objects[c.sha1] = c
|
||||
self._topo_list.append(c)
|
||||
return c
|
||||
|
||||
def __getitem__(self, sha1):
|
||||
return self._objects[sha1]
|
||||
|
||||
def items(self):
|
||||
return self._objects.items()
|
||||
|
||||
|
||||
class Archive(BaseCommand):
|
||||
def __init__(self, ref, fmt, prefix, filename):
|
||||
BaseCommand.__init__(self)
|
||||
self.ref = ref
|
||||
self.fmt = fmt
|
||||
self.prefix = prefix
|
||||
self.filename = filename
|
||||
|
||||
def do(self):
|
||||
fp = open(core.encode(self.filename), 'wb')
|
||||
cmd = ['git', 'archive', '--format='+self.fmt]
|
||||
if self.fmt in ('tgz', 'tar.gz'):
|
||||
cmd.append('-9')
|
||||
if self.prefix:
|
||||
cmd.append('--prefix=' + self.prefix)
|
||||
cmd.append(self.ref)
|
||||
proc = utils.start_command(cmd, stdout=fp)
|
||||
out, err = proc.communicate()
|
||||
fp.close()
|
||||
if not out:
|
||||
out = ''
|
||||
if err:
|
||||
out += err
|
||||
status = proc.returncode
|
||||
cola.notifier().broadcast(signals.log_cmd, status, out)
|
||||
|
||||
|
||||
command_directory = {
|
||||
archive: Archive,
|
||||
}
|
||||
+1347
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
__all__ = ('decorator', 'deprecated', 'memoize')
|
||||
|
||||
|
||||
def decorator(caller, func=None):
|
||||
"""
|
||||
decorator(caller) converts a caller function into a decorator;
|
||||
decorator(caller, func) decorates a function using a caller.
|
||||
"""
|
||||
if func is None: # returns a decorator
|
||||
def _decorator(f, *args, **opts):
|
||||
def _caller(*args, **opts):
|
||||
return caller(f, *args, **opts)
|
||||
return _caller
|
||||
return _decorator
|
||||
|
||||
else: # returns a decorated function
|
||||
def _decorator(*args, **opts):
|
||||
return caller(func, *args, **opts)
|
||||
return _decorator
|
||||
|
||||
|
||||
@decorator
|
||||
def deprecated(func, *args, **kw):
|
||||
"A decorator for deprecated functions"
|
||||
import warnings
|
||||
warnings.warn('Calling deprecated function %r' % func.__name__,
|
||||
DeprecationWarning, stacklevel=3)
|
||||
return func(*args, **kw)
|
||||
|
||||
|
||||
def memoize(func):
|
||||
"""
|
||||
A decorator for memoizing function calls
|
||||
|
||||
http://en.wikipedia.org/wiki/Memoization
|
||||
|
||||
"""
|
||||
func.cache = {}
|
||||
return decorator(_memoize, func)
|
||||
|
||||
|
||||
def _memoize(func, *args, **opts):
|
||||
"""Implements memoized cache lookups"""
|
||||
if opts: # frozenset is used to ensure hashability
|
||||
key = args, frozenset(opts.items())
|
||||
else:
|
||||
key = args
|
||||
cache = func.cache # attribute added by memoize
|
||||
try:
|
||||
return cache[key]
|
||||
except KeyError:
|
||||
cache[key] = result = func(*args, **opts)
|
||||
return result
|
||||
+42
-27
@@ -1,7 +1,8 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from cola.utils import write
|
||||
from cola import utils
|
||||
from cola import gitcmds
|
||||
|
||||
|
||||
class DiffParser(object):
|
||||
@@ -23,11 +24,11 @@ class DiffParser(object):
|
||||
self.diffs = []
|
||||
self.selected = []
|
||||
|
||||
(header, diff) = model.diff_helper(filename=filename,
|
||||
branch=branch,
|
||||
with_diff_header=True,
|
||||
cached=cached and not bool(branch),
|
||||
reverse=cached or bool(branch) or reverse)
|
||||
(header, diff) = gitcmds.diff_helper(filename=filename,
|
||||
branch=branch,
|
||||
with_diff_header=True,
|
||||
cached=cached and not bool(branch),
|
||||
reverse=cached or bool(branch) or reverse)
|
||||
self.model = model
|
||||
self.diff = diff
|
||||
self.header = header
|
||||
@@ -35,17 +36,17 @@ class DiffParser(object):
|
||||
|
||||
# Always index into the non-reversed diff
|
||||
self.fwd_header, self.fwd_diff = \
|
||||
model.diff_helper(filename=filename,
|
||||
branch=branch,
|
||||
with_diff_header=True,
|
||||
cached=cached and not bool(branch),
|
||||
reverse=bool(branch))
|
||||
gitcmds.diff_helper(filename=filename,
|
||||
branch=branch,
|
||||
with_diff_header=True,
|
||||
cached=cached and not bool(branch),
|
||||
reverse=bool(branch))
|
||||
|
||||
def write_diff(self,filename,which,selected=False,noop=False):
|
||||
"""Writes a new diff corresponding to the user's selection."""
|
||||
if not noop and which < len(self.diffs):
|
||||
diff = self.diffs[which]
|
||||
write(filename, self.header + '\n' + diff + '\n')
|
||||
utils.write(filename, self.header + '\n' + diff + '\n')
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -205,32 +206,46 @@ class DiffParser(object):
|
||||
if special_selection in self.fwd_diff:
|
||||
selection = special_selection
|
||||
else:
|
||||
return
|
||||
return 0, ''
|
||||
start = self.fwd_diff.index(selection)
|
||||
end = start + len(selection)
|
||||
self.set_diffs_to_range(start, end)
|
||||
else:
|
||||
self.set_diff_to_offset(offset)
|
||||
selected = False
|
||||
|
||||
output = ''
|
||||
status = 0
|
||||
# Process diff selection only
|
||||
if selected:
|
||||
for idx in self.selected:
|
||||
contents = self.diff_subset(idx, start, end)
|
||||
if contents:
|
||||
tmpfile = self.model.tmp_filename()
|
||||
write(tmpfile, contents)
|
||||
if apply_to_worktree:
|
||||
self.model.apply_diff_to_worktree(tmpfile)
|
||||
else:
|
||||
self.model.apply_diff(tmpfile)
|
||||
os.unlink(tmpfile)
|
||||
if not contents:
|
||||
continue
|
||||
tmpfile = self.model.tmp_filename()
|
||||
utils.write(tmpfile, contents)
|
||||
if apply_to_worktree:
|
||||
stat, out = self.model.apply_diff_to_worktree(tmpfile)
|
||||
output += out
|
||||
status = max(status, stat)
|
||||
else:
|
||||
stat, out = self.model.apply_diff(tmpfile)
|
||||
output += out
|
||||
status = max(status, stat)
|
||||
os.unlink(tmpfile)
|
||||
# Process a complete hunk
|
||||
else:
|
||||
for idx, diff in enumerate(self.diffs):
|
||||
tmpfile = self.model.tmp_filename()
|
||||
if self.write_diff(tmpfile,idx):
|
||||
if apply_to_worktree:
|
||||
self.model.apply_diff_to_worktree(tmpfile)
|
||||
else:
|
||||
self.model.apply_diff(tmpfile)
|
||||
os.unlink(tmpfile)
|
||||
if not self.write_diff(tmpfile,idx):
|
||||
continue
|
||||
if apply_to_worktree:
|
||||
stat, out = self.model.apply_diff_to_worktree(tmpfile)
|
||||
output += out
|
||||
status = max(status, stat)
|
||||
else:
|
||||
stat, out = self.model.apply_diff(tmpfile)
|
||||
output += out
|
||||
status = max(status, stat)
|
||||
os.unlink(tmpfile)
|
||||
return status, output
|
||||
|
||||
@@ -1,7 +1,113 @@
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola import utils
|
||||
from cola import qtutils
|
||||
from cola import gitcmds
|
||||
|
||||
|
||||
def launch(args):
|
||||
"""Launches 'git difftool' with args"""
|
||||
difftool_args = ['git', 'difftool', '--no-prompt']
|
||||
difftool_args.extend(args)
|
||||
utils.fork(difftool_args)
|
||||
|
||||
|
||||
def diff_commits(parent, a, b):
|
||||
dlg = FileDiffDialog(parent, a, b)
|
||||
dlg.show()
|
||||
dlg.raise_()
|
||||
return dlg.exec_() == QtGui.QDialog.Accepted
|
||||
|
||||
|
||||
class FileDiffDialog(QtGui.QDialog):
|
||||
def __init__(self, parent, a, b):
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.a = a
|
||||
self.b = b
|
||||
|
||||
self.setWindowTitle('Select File(s)')
|
||||
self._tree = QtGui.QTreeWidget(self)
|
||||
self._tree.setAlternatingRowColors(True)
|
||||
self._tree.setRootIsDecorated(False)
|
||||
self._tree.setSelectionMode(self._tree.ExtendedSelection)
|
||||
self._tree.setUniformRowHeights(True)
|
||||
self._tree.setAllColumnsShowFocus(True)
|
||||
self._tree.setHeaderHidden(True)
|
||||
|
||||
self._diff_btn = QtGui.QPushButton('Compare')
|
||||
self._diff_btn.setIcon(qtutils.ok_icon())
|
||||
self._diff_btn.setEnabled(False)
|
||||
|
||||
self._close_btn = QtGui.QPushButton('Close')
|
||||
self._close_btn.setIcon(qtutils.close_icon())
|
||||
|
||||
self._button_layt = QtGui.QHBoxLayout()
|
||||
self._button_layt.setMargin(0)
|
||||
self._button_layt.addStretch()
|
||||
self._button_layt.addWidget(self._diff_btn)
|
||||
self._button_layt.addWidget(self._close_btn)
|
||||
|
||||
self._layt = QtGui.QVBoxLayout()
|
||||
self._layt.setMargin(4)
|
||||
self._layt.addWidget(self._tree)
|
||||
self._layt.addLayout(self._button_layt)
|
||||
self.setLayout(self._layt)
|
||||
|
||||
qtutils.add_close_action(self)
|
||||
|
||||
self.connect(self._tree, SIGNAL('itemSelectionChanged()'),
|
||||
self._tree_selection_changed)
|
||||
|
||||
self.connect(self._tree,
|
||||
SIGNAL('itemDoubleClicked(QTreeWidgetItem*,int)'),
|
||||
self._tree_double_clicked)
|
||||
|
||||
self.connect(self._diff_btn, SIGNAL('clicked()'), self.diff)
|
||||
self.connect(self._close_btn, SIGNAL('clicked()'), self.close)
|
||||
|
||||
self.diff_arg = '%s..%s' % (self.a, self.b)
|
||||
|
||||
self.resize(720, 420)
|
||||
|
||||
|
||||
def exec_(self):
|
||||
filenames = gitcmds.diff_filenames(self.diff_arg)
|
||||
if not filenames:
|
||||
details = ('"git diff --name-only %s" returned an empty list' %
|
||||
self.diff_arg)
|
||||
self.hide()
|
||||
qtutils.information('git cola',
|
||||
message='No changes to diff',
|
||||
details=details,
|
||||
parent=self)
|
||||
self.close()
|
||||
return self.Accepted
|
||||
|
||||
icon = qtutils.file_icon()
|
||||
items = []
|
||||
for filename in filenames:
|
||||
item = QtGui.QTreeWidgetItem()
|
||||
item.setIcon(0, icon)
|
||||
item.setText(0, filename)
|
||||
item.setData(0, QtCore.Qt.UserRole, QtCore.QVariant(filename))
|
||||
items.append(item)
|
||||
self._tree.addTopLevelItems(items)
|
||||
|
||||
return QtGui.QDialog.exec_(self)
|
||||
|
||||
def _tree_selection_changed(self):
|
||||
self._diff_btn.setEnabled(bool(self._tree.selectedItems()))
|
||||
|
||||
def _tree_double_clicked(self, item, column):
|
||||
path = item.data(0, QtCore.Qt.UserRole).toPyObject()
|
||||
launch([self.diff_arg, '--', unicode(path)])
|
||||
|
||||
def diff(self):
|
||||
items = self._tree.selectedItems()
|
||||
if not items:
|
||||
return
|
||||
paths = [i.data(0, QtCore.Qt.UserRole).toPyObject() for i in items]
|
||||
for path in paths:
|
||||
launch([self.diff_arg, '--', unicode(path)])
|
||||
|
||||
@@ -3,11 +3,20 @@ class ColaError(StandardError):
|
||||
"""The base class of all cola exceptions"""
|
||||
pass
|
||||
|
||||
|
||||
class GitCommandError(ColaError):
|
||||
"""Exception class for failed commands."""
|
||||
pass
|
||||
|
||||
|
||||
class GitInitError(ColaError):
|
||||
"""Exception class for errors related to just-initialized repositories."""
|
||||
pass
|
||||
|
||||
|
||||
class UsageError(ColaError):
|
||||
"""Exception class for usage errors."""
|
||||
def __init__(self, title, message):
|
||||
ColaError.__init__(self, message)
|
||||
self.title = title
|
||||
self.message = message
|
||||
|
||||
+103
-54
@@ -4,35 +4,27 @@
|
||||
# This module is part of GitPython and is released under
|
||||
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import errno
|
||||
import subprocess
|
||||
import threading
|
||||
cmdlock = threading.Lock()
|
||||
|
||||
import cola
|
||||
from cola import core
|
||||
from cola import errors
|
||||
from cola import signals
|
||||
from cola.decorators import memoize
|
||||
|
||||
cmdlock = threading.Lock()
|
||||
# Enables debugging of GitPython's git commands
|
||||
GIT_PYTHON_TRACE = os.getenv('GIT_PYTHON_TRACE', False)
|
||||
GIT_COLA_TRACE = False
|
||||
|
||||
|
||||
def dashify(string):
|
||||
return string.replace('_', '-')
|
||||
|
||||
# Enables debugging of GitPython's git commands
|
||||
GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
|
||||
|
||||
execute_kwargs = ('cwd',
|
||||
'istream',
|
||||
'with_exceptions',
|
||||
'with_raw_output',
|
||||
'with_status',
|
||||
'with_stderr')
|
||||
|
||||
extra = {}
|
||||
if sys.platform == 'win32':
|
||||
extra = {'shell': True}
|
||||
|
||||
class Git(object):
|
||||
"""
|
||||
@@ -40,6 +32,77 @@ class Git(object):
|
||||
"""
|
||||
def __init__(self):
|
||||
self._git_cwd = None #: The working directory used by execute()
|
||||
self.load_worktree(os.getcwd())
|
||||
|
||||
def load_worktree(self, path):
|
||||
self._git_dir = path
|
||||
self._worktree = None
|
||||
self.worktree()
|
||||
|
||||
def worktree(self):
|
||||
if self._worktree:
|
||||
return self._worktree
|
||||
self.git_dir()
|
||||
if self._git_dir:
|
||||
curdir = self._git_dir
|
||||
else:
|
||||
curdir = os.getcwd()
|
||||
|
||||
if self._is_git_dir(os.path.join(curdir, '.git')):
|
||||
return curdir
|
||||
|
||||
# Handle bare repositories
|
||||
if (len(os.path.basename(curdir)) > 4
|
||||
and curdir.endswith('.git')):
|
||||
return curdir
|
||||
if 'GIT_WORK_TREE' in os.environ:
|
||||
self._worktree = os.getenv('GIT_WORK_TREE')
|
||||
if not self._worktree or not os.path.isdir(self._worktree):
|
||||
if self._git_dir:
|
||||
gitparent = os.path.join(os.path.abspath(self._git_dir), '..')
|
||||
self._worktree = os.path.abspath(gitparent)
|
||||
self.set_cwd(self._worktree)
|
||||
return self._worktree
|
||||
|
||||
def is_valid(self):
|
||||
return self._git_dir and self._is_git_dir(self._git_dir)
|
||||
|
||||
def git_path(self, *paths):
|
||||
return os.path.join(self.git_dir(), *paths)
|
||||
|
||||
def git_dir(self):
|
||||
if self.is_valid():
|
||||
return self._git_dir
|
||||
if 'GIT_DIR' in os.environ:
|
||||
self._git_dir = os.getenv('GIT_DIR')
|
||||
if self._git_dir:
|
||||
curpath = os.path.abspath(self._git_dir)
|
||||
else:
|
||||
curpath = os.path.abspath(os.getcwd())
|
||||
# Search for a .git directory
|
||||
while curpath:
|
||||
if self._is_git_dir(curpath):
|
||||
self._git_dir = curpath
|
||||
break
|
||||
gitpath = os.path.join(curpath, '.git')
|
||||
if self._is_git_dir(gitpath):
|
||||
self._git_dir = gitpath
|
||||
break
|
||||
curpath, dummy = os.path.split(curpath)
|
||||
if not dummy:
|
||||
break
|
||||
return self._git_dir
|
||||
|
||||
def _is_git_dir(self, d):
|
||||
"""From git's setup.c:is_git_directory()."""
|
||||
if (os.path.isdir(d)
|
||||
and os.path.isdir(os.path.join(d, 'objects'))
|
||||
and os.path.isdir(os.path.join(d, 'refs'))):
|
||||
headref = os.path.join(d, 'HEAD')
|
||||
return (os.path.isfile(headref)
|
||||
or (os.path.islink(headref)
|
||||
and os.readlink(headref).startswith('refs')))
|
||||
return False
|
||||
|
||||
def set_cwd(self, path):
|
||||
"""Sets the current directory."""
|
||||
@@ -102,8 +165,10 @@ class Git(object):
|
||||
else:
|
||||
stderr = None
|
||||
|
||||
extra = {}
|
||||
if sys.platform == 'win32':
|
||||
command = map(replace_carot, command)
|
||||
extra = {'shell': True}
|
||||
|
||||
# Start the process
|
||||
# Guard against thread-unsafe .git/index.lock files
|
||||
@@ -119,7 +184,7 @@ class Git(object):
|
||||
break
|
||||
except OSError, e:
|
||||
# Some systems interrupt system calls and throw OSError
|
||||
if e.errno == errno.EINTR:
|
||||
if e.errno in (errno.EINTR, errno.ENOMEM):
|
||||
continue
|
||||
cmdlock.release()
|
||||
raise e
|
||||
@@ -148,6 +213,10 @@ class Git(object):
|
||||
else:
|
||||
print "%s -> %d" % (command, status)
|
||||
|
||||
if GIT_COLA_TRACE:
|
||||
msg = 'trace: ' + subprocess.list2cmdline(command)
|
||||
cola.notifier().broadcast(signals.log_cmd, status, msg)
|
||||
|
||||
# Allow access to the command's status code
|
||||
if with_status:
|
||||
return (status, output)
|
||||
@@ -193,11 +262,18 @@ class Git(object):
|
||||
|
||||
Returns
|
||||
Same as execute()
|
||||
|
||||
"""
|
||||
|
||||
# Handle optional arguments prior to calling transform_kwargs
|
||||
# otherwise they'll end up in args, which is bad.
|
||||
_kwargs = dict(cwd=self._git_cwd)
|
||||
execute_kwargs = ('cwd', 'istream',
|
||||
'with_exceptions',
|
||||
'with_raw_output',
|
||||
'with_status',
|
||||
'with_stderr')
|
||||
|
||||
for kwarg in execute_kwargs:
|
||||
if kwarg in kwargs:
|
||||
_kwargs[kwarg] = kwargs.pop(kwarg)
|
||||
@@ -227,45 +303,18 @@ def replace_carot(cmd_arg):
|
||||
return cmd_arg.replace('^', '^^')
|
||||
|
||||
|
||||
def shell_quote(*strings):
|
||||
"""
|
||||
Quote strings so that they can be suitably martialled
|
||||
off to the shell. This method supports POSIX sh syntax.
|
||||
This is crucial to properly handle command line arguments
|
||||
with spaces, quotes, double-quotes, etc. on darwin/win32...
|
||||
"""
|
||||
@memoize
|
||||
def instance():
|
||||
"""Return the Git singleton"""
|
||||
return Git()
|
||||
|
||||
regex = re.compile('[^\w!%+,\-./:@^]')
|
||||
quote_regex = re.compile("((?:'\\''){2,})")
|
||||
|
||||
ret = []
|
||||
for s in strings:
|
||||
if not s:
|
||||
continue
|
||||
git = instance()
|
||||
"""
|
||||
Git command singleton
|
||||
|
||||
if '\x00' in s:
|
||||
raise ValueError('No way to quote strings '
|
||||
'containing null(\\000) bytes')
|
||||
>>> from cola.git import git
|
||||
>>> 'git' == git.version()[:3]
|
||||
True
|
||||
|
||||
# = does need quoting else in command position it's a
|
||||
# program-local environment setting
|
||||
match = regex.search(s)
|
||||
if match and '=' not in s:
|
||||
# ' -> '\''
|
||||
s = s.replace("'", "'\\''")
|
||||
|
||||
# make multiple ' in a row look simpler
|
||||
# '\'''\'''\'' -> '"'''"'
|
||||
quote_match = quote_regex.match(s)
|
||||
if quote_match:
|
||||
quotes = match.group(1)
|
||||
s.replace(quotes, ("'" *(len(quotes)/4)) + "\"'")
|
||||
|
||||
s = "'%s'" % s
|
||||
if s.startswith("''"):
|
||||
s = s[2:]
|
||||
|
||||
if s.endswith("''"):
|
||||
s = s[:-2]
|
||||
ret.append(s)
|
||||
return ' '.join(ret)
|
||||
"""
|
||||
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import os
|
||||
import copy
|
||||
import fnmatch
|
||||
|
||||
from cola import core
|
||||
from cola import git
|
||||
from cola import observable
|
||||
from cola.decorators import memoize
|
||||
|
||||
|
||||
@memoize
|
||||
def instance():
|
||||
"""Return a static GitConfig instance."""
|
||||
return GitConfig()
|
||||
|
||||
|
||||
def _stat_info():
|
||||
# Try /etc/gitconfig as a fallback for the system config
|
||||
userconfig = os.path.expanduser(os.path.join('~', '.gitconfig'))
|
||||
paths = (('system', '/etc/gitconfig'),
|
||||
('user', core.decode(userconfig)),
|
||||
('repo', core.decode(git.instance().git_path('config'))))
|
||||
statinfo = []
|
||||
for category, path in paths:
|
||||
try:
|
||||
statinfo.append((category, path, os.stat(path).st_mtime))
|
||||
except OSError:
|
||||
continue
|
||||
return statinfo
|
||||
|
||||
|
||||
def _cache_key():
|
||||
# Try /etc/gitconfig as a fallback for the system config
|
||||
userconfig = os.path.expanduser(os.path.join('~', '.gitconfig'))
|
||||
paths = ('/etc/gitconfig',
|
||||
userconfig,
|
||||
git.instance().git_path('config'))
|
||||
mtimes = []
|
||||
for path in paths:
|
||||
try:
|
||||
mtimes.append(os.stat(path).st_mtime)
|
||||
except OSError:
|
||||
continue
|
||||
return mtimes
|
||||
|
||||
|
||||
class GitConfig(observable.Observable):
|
||||
"""Encapsulate access to git-config values."""
|
||||
|
||||
message_user_config_changed = 'user_config_changed'
|
||||
message_repo_config_changed = 'repo_config_changed'
|
||||
|
||||
def __init__(self):
|
||||
observable.Observable.__init__(self)
|
||||
self.git = git.instance()
|
||||
self._system = {}
|
||||
self._user = {}
|
||||
self._repo = {}
|
||||
self._all = {}
|
||||
self._cache_key = None
|
||||
self._configs = []
|
||||
self._config_files = {}
|
||||
self._find_config_files()
|
||||
|
||||
def reset(self):
|
||||
self._system = {}
|
||||
self._user = {}
|
||||
self._repo = {}
|
||||
self._all = {}
|
||||
self._cache_key = None
|
||||
self._configs = []
|
||||
self._config_files = {}
|
||||
self._find_config_files()
|
||||
|
||||
def user(self):
|
||||
return copy.deepcopy(self._user)
|
||||
|
||||
def repo(self):
|
||||
return copy.deepcopy(self._repo)
|
||||
|
||||
def all(self):
|
||||
return copy.deepcopy(self._all)
|
||||
|
||||
def _find_config_files(self):
|
||||
"""
|
||||
Classify git config files into 'system', 'user', and 'repo'.
|
||||
|
||||
Populates self._configs with a list of the files in
|
||||
reverse-precedence order. self._config_files is populated with
|
||||
{category: path} where category is one of 'system', 'user', or 'repo'.
|
||||
|
||||
"""
|
||||
# Try the git config in git's installation prefix
|
||||
statinfo = _stat_info()
|
||||
self._configs = map(lambda x: x[1], statinfo)
|
||||
self._config_files = {}
|
||||
for (cat, path, mtime) in statinfo:
|
||||
self._config_files[cat] = path
|
||||
|
||||
def update(self):
|
||||
"""Read config values from git."""
|
||||
if self._cached():
|
||||
return
|
||||
self._read_configs()
|
||||
|
||||
def _cached(self):
|
||||
"""
|
||||
Return True when the cache matches.
|
||||
|
||||
Updates the cache and returns False when the cache does not match.
|
||||
|
||||
"""
|
||||
cache_key = _cache_key()
|
||||
if not self._cache_key or cache_key != self._cache_key:
|
||||
self._cache_key = cache_key
|
||||
return False
|
||||
return True
|
||||
|
||||
def _read_configs(self):
|
||||
"""Read git config value into the system, user and repo dicts."""
|
||||
self._system = {}
|
||||
self._user = {}
|
||||
self._repo = {}
|
||||
self._all = {}
|
||||
|
||||
if 'system' in self._config_files:
|
||||
self._system = self.read_config(self._config_files['system'])
|
||||
|
||||
if 'user' in self._config_files:
|
||||
self._user = self.read_config(self._config_files['user'])
|
||||
|
||||
if 'repo' in self._config_files:
|
||||
self._repo = self.read_config(self._config_files['repo'])
|
||||
|
||||
self._all = {}
|
||||
for dct in (self._system, self._user, self._repo):
|
||||
self._all.update(dct)
|
||||
|
||||
def read_config(self, path):
|
||||
"""Return git config data from a path as a dictionary."""
|
||||
dest = {}
|
||||
args = ('--null', '--file', path, '--list')
|
||||
config_lines = self.git.config(*args).split('\0')
|
||||
for line in config_lines:
|
||||
try:
|
||||
k, v = line.split('\n')
|
||||
except:
|
||||
# the user has an invalid entry in their git config
|
||||
continue
|
||||
v = core.decode(v)
|
||||
if v == 'yes':
|
||||
v = 'true'
|
||||
elif v == 'no':
|
||||
v = 'false'
|
||||
if v == 'true' or v == 'false':
|
||||
v = (v == 'true')
|
||||
try:
|
||||
v = int(eval(v))
|
||||
except:
|
||||
pass
|
||||
dest[k.lower()] = v
|
||||
return dest
|
||||
|
||||
def get(self, key, default=None, source=None):
|
||||
"""Return the string value for a config key."""
|
||||
self.update()
|
||||
return self._all.get(key, default)
|
||||
|
||||
def get_user(self, key, default=None):
|
||||
self.update()
|
||||
return self._user.get(key, default)
|
||||
|
||||
def get_repo(self, key, default=None):
|
||||
self.update()
|
||||
return self._repo.get(key, default)
|
||||
|
||||
def python_to_git(self, value):
|
||||
if type(value) is bool:
|
||||
if value:
|
||||
return 'true'
|
||||
else:
|
||||
return 'false'
|
||||
if type(value) is int:
|
||||
return unicode(value)
|
||||
return value
|
||||
|
||||
def set_user(self, key, value):
|
||||
msg = self.message_user_config_changed
|
||||
self.git.config('--global', key, self.python_to_git(value))
|
||||
self.update()
|
||||
self.notify_message_observers(msg, key, value)
|
||||
|
||||
def set_repo(self, key, value):
|
||||
msg = self.message_repo_config_changed
|
||||
self.git.config(key, self.python_to_git(value))
|
||||
self.update()
|
||||
self.notify_message_observers(msg, key, value)
|
||||
|
||||
def find(self, pat):
|
||||
result = {}
|
||||
for key, val in self._all.items():
|
||||
if fnmatch.fnmatch(key, pat):
|
||||
result[key] = val
|
||||
return result
|
||||
|
||||
def get_encoding(self, default='utf-8'):
|
||||
return self.get('gui.encoding', default=default)
|
||||
|
||||
guitool_opts = ('cmd', 'needsfile', 'noconsole', 'norescan', 'confirm',
|
||||
'argprompt', 'revprompt', 'revunmerged', 'title', 'prompt')
|
||||
|
||||
def get_guitool_opts(self, name):
|
||||
"""Return the guitool.<name> namespace as a dict"""
|
||||
keyprefix = 'guitool.' + name + '.'
|
||||
opts = {}
|
||||
for cfg in self.guitool_opts:
|
||||
value = self.get(keyprefix + cfg)
|
||||
if value is None:
|
||||
continue
|
||||
opts[cfg] = value
|
||||
return opts
|
||||
|
||||
def get_guitool_names(self):
|
||||
cmds = []
|
||||
guitools = self.find('guitool.*.cmd')
|
||||
for name, cmd in guitools.items():
|
||||
name = name[len('guitool.'):-len('.cmd')]
|
||||
cmds.append(name)
|
||||
return cmds
|
||||
+628
-21
@@ -1,30 +1,637 @@
|
||||
"""Provides commands and queries for Git."""
|
||||
import os
|
||||
import re
|
||||
from cStringIO import StringIO
|
||||
|
||||
import cola
|
||||
from cola import core
|
||||
from cola import git
|
||||
from cola import gitcfg
|
||||
from cola import errors
|
||||
from cola import utils
|
||||
from cola import version
|
||||
from cola.compat import set
|
||||
|
||||
git = git.instance()
|
||||
config = gitcfg.instance()
|
||||
|
||||
class InvalidRepositoryError(StandardError):
|
||||
pass
|
||||
|
||||
|
||||
def default_remote():
|
||||
model = cola.model()
|
||||
branch = model.currentbranch
|
||||
branchconfig = 'branch.%s.remote' % branch
|
||||
return model.local_config(branchconfig, 'origin')
|
||||
def default_remote(config=None):
|
||||
"""Return the remote tracked by the current branch."""
|
||||
if config is None:
|
||||
config = gitcfg.instance()
|
||||
return config.get('branch.%s.remote' % current_branch())
|
||||
|
||||
def corresponding_remote_ref():
|
||||
model = cola.model()
|
||||
remote = default_remote()
|
||||
branch = model.currentbranch
|
||||
best_match = '%s/%s' % (remote, branch)
|
||||
remote_branches = model.remote_branches
|
||||
if not remote_branches:
|
||||
return remote
|
||||
for rb in remote_branches:
|
||||
if rb == best_match:
|
||||
return rb
|
||||
return remote_branches[0]
|
||||
|
||||
def diff_filenames(arg):
|
||||
def diff_filenames(arg, git=git):
|
||||
"""Return a list of filenames that have been modified"""
|
||||
model = cola.model()
|
||||
diff_zstr = model.git.diff(arg, name_only=True, z=True).rstrip('\0')
|
||||
diff_zstr = git.diff(arg, name_only=True, z=True,
|
||||
**_common_diff_opts()).rstrip('\0')
|
||||
return [core.decode(f) for f in diff_zstr.split('\0') if f]
|
||||
|
||||
|
||||
def all_files(git=git):
|
||||
"""Return the names of all files in the repository"""
|
||||
return [core.decode(f)
|
||||
for f in git.ls_files(z=True)
|
||||
.strip('\0').split('\0') if f]
|
||||
|
||||
|
||||
class _current_branch:
|
||||
"""Cache for current_branch()"""
|
||||
key = None
|
||||
value = None
|
||||
|
||||
|
||||
def clear_cache():
|
||||
_current_branch.key = None
|
||||
|
||||
|
||||
def current_branch(git=git):
|
||||
"""Return the current branch"""
|
||||
head = git.git_path('HEAD')
|
||||
try:
|
||||
key = os.stat(head).st_mtime
|
||||
if _current_branch.key == key:
|
||||
return _current_branch.value
|
||||
except OSError, e:
|
||||
pass
|
||||
data = git.rev_parse('HEAD', with_stderr=True, symbolic_full_name=True)
|
||||
if data.startswith('fatal:'):
|
||||
# git init -- read .git/HEAD. We could do this unconditionally
|
||||
# and avoid the subprocess call. It's probably time to start
|
||||
# using dulwich.
|
||||
data = _read_git_head(head)
|
||||
|
||||
for refs_prefix in ('refs/heads/', 'refs/remotes/'):
|
||||
if data.startswith(refs_prefix):
|
||||
value = core.decode(data[len(refs_prefix):])
|
||||
_current_branch.key = key
|
||||
_current_branch.value = value
|
||||
return value
|
||||
# Detached head
|
||||
return data
|
||||
|
||||
|
||||
def _read_git_head(head, default='master', git=git):
|
||||
"""Pure-python .git/HEAD reader"""
|
||||
# Legacy .git/HEAD symlinks
|
||||
if os.path.islink(head):
|
||||
refs_heads = os.path.realpath(git.git_path('refs', 'heads'))
|
||||
path = os.path.abspath(head).replace('\\', '/')
|
||||
if path.startswith(refs_heads + '/'):
|
||||
return path[len(refs_heads)+1:]
|
||||
|
||||
# Common .git/HEAD "ref: refs/heads/master" file
|
||||
elif os.path.isfile(head):
|
||||
data = utils.slurp(core.decode(head)).rstrip()
|
||||
ref_prefix = 'ref: '
|
||||
if data.startswith(ref_prefix):
|
||||
return data[len(ref_prefix):]
|
||||
# Detached head
|
||||
return data
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def branch_list(remote=False):
|
||||
"""
|
||||
Return a list of local or remote branches
|
||||
|
||||
This explicitly removes HEAD from the list of remote branches.
|
||||
|
||||
"""
|
||||
if remote:
|
||||
return for_each_ref_basename('refs/remotes')
|
||||
else:
|
||||
return for_each_ref_basename('refs/heads')
|
||||
|
||||
|
||||
def for_each_ref_basename(refs, git=git):
|
||||
"""Return refs starting with 'refs'."""
|
||||
git_output = git.for_each_ref(refs, format='%(refname)')
|
||||
output = core.decode(git_output).splitlines()
|
||||
non_heads = filter(lambda x: not x.endswith('/HEAD'), output)
|
||||
return map(lambda x: x[len(refs) + 1:], non_heads)
|
||||
|
||||
|
||||
def all_refs(split=False, git=git):
|
||||
"""Return a tuple of (local branches, remote branches, tags)."""
|
||||
local_branches = []
|
||||
remote_branches = []
|
||||
tags = []
|
||||
triple = lambda x, y: (x, len(x) + 1, y)
|
||||
query = (triple('refs/tags', tags),
|
||||
triple('refs/heads', local_branches),
|
||||
triple('refs/remotes', remote_branches))
|
||||
cmdout = core.decode(git.for_each_ref(format='%(refname)'))
|
||||
for ref in cmdout.splitlines():
|
||||
for prefix, prefix_len, dst in query:
|
||||
if ref.startswith(prefix) and not ref.endswith('/HEAD'):
|
||||
dst.append(ref[prefix_len:])
|
||||
continue
|
||||
if split:
|
||||
return local_branches, remote_branches, tags
|
||||
else:
|
||||
return local_branches + remote_branches + tags
|
||||
|
||||
|
||||
def tracked_branch(branch=None, config=None):
|
||||
"""Return the remote branch associated with 'branch'."""
|
||||
if config is None:
|
||||
config = gitcfg.instance()
|
||||
if branch is None:
|
||||
branch = current_branch()
|
||||
remote = config.get('branch.%s.remote' % branch)
|
||||
if not remote:
|
||||
return None
|
||||
merge_ref = config.get('branch.%s.merge' % branch)
|
||||
if not merge_ref:
|
||||
return None
|
||||
refs_heads = 'refs/heads/'
|
||||
if merge_ref.startswith(refs_heads):
|
||||
return remote + '/' + merge_ref[len(refs_heads):]
|
||||
return None
|
||||
|
||||
|
||||
def untracked_files(git=git):
|
||||
"""Returns a sorted list of untracked files."""
|
||||
ls_files = git.ls_files(z=True,
|
||||
others=True,
|
||||
exclude_standard=True)
|
||||
return [core.decode(f) for f in ls_files.split('\0') if f]
|
||||
|
||||
|
||||
def tag_list():
|
||||
"""Return a list of tags."""
|
||||
tags = for_each_ref_basename('refs/tags')
|
||||
tags.reverse()
|
||||
return tags
|
||||
|
||||
|
||||
def commit_diff(sha1, git=git):
|
||||
commit = git.show(sha1)
|
||||
first_newline = commit.index('\n')
|
||||
if commit[first_newline+1:].startswith('Merge:'):
|
||||
return (core.decode(commit) + '\n\n' +
|
||||
core.decode(diff_helper(commit=sha1,
|
||||
cached=False,
|
||||
suppress_header=False)))
|
||||
else:
|
||||
return core.decode(commit)
|
||||
|
||||
|
||||
def _common_diff_opts(config=config):
|
||||
submodule = version.check('diff-submodule', version.git_version())
|
||||
return {
|
||||
'patience': True,
|
||||
'submodule': submodule,
|
||||
'no_color': True,
|
||||
'no_ext_diff': True,
|
||||
'with_raw_output': True,
|
||||
'with_stderr': True,
|
||||
'unified': config.get('gui.diffcontext', 3),
|
||||
}
|
||||
|
||||
|
||||
def sha1_diff(sha1, git=git):
|
||||
return core.decode(git.diff(sha1 + '^!', **_common_diff_opts()))
|
||||
|
||||
|
||||
def diff_info(sha1, git=git, merge=True):
|
||||
|
||||
if merge:
|
||||
mergefmt = 'Merge: %P%n'
|
||||
else:
|
||||
mergefmt = ''
|
||||
|
||||
log = git.log('-1',
|
||||
'--pretty=format:'
|
||||
'commit %H%n' +
|
||||
mergefmt +
|
||||
'Author: %aN <%aE>%n'
|
||||
'Date: %aD%n'
|
||||
'%n%s%n%n%b',
|
||||
sha1)
|
||||
return core.decode(log) + '\n\n' + sha1_diff(sha1)
|
||||
|
||||
|
||||
def diff_helper(commit=None,
|
||||
branch=None,
|
||||
ref=None,
|
||||
endref=None,
|
||||
filename=None,
|
||||
cached=True,
|
||||
with_diff_header=False,
|
||||
suppress_header=True,
|
||||
reverse=False,
|
||||
git=git):
|
||||
"Invokes git diff on a filepath."
|
||||
if commit:
|
||||
ref, endref = commit+'^', commit
|
||||
argv = []
|
||||
if ref and endref:
|
||||
argv.append('%s..%s' % (ref, endref))
|
||||
elif ref:
|
||||
for r in ref.strip().split():
|
||||
argv.append(r)
|
||||
elif branch:
|
||||
argv.append(branch)
|
||||
|
||||
if filename:
|
||||
argv.append('--')
|
||||
if type(filename) is list:
|
||||
argv.extend(filename)
|
||||
else:
|
||||
argv.append(filename)
|
||||
|
||||
start = False
|
||||
del_tag = 'deleted file mode '
|
||||
|
||||
headers = []
|
||||
deleted = cached and not os.path.exists(core.encode(filename))
|
||||
|
||||
diffoutput = git.diff(R=reverse, M=True, cached=cached,
|
||||
*argv, **_common_diff_opts())
|
||||
# Handle 'git init'
|
||||
if diffoutput.startswith('fatal:'):
|
||||
if with_diff_header:
|
||||
return ('', '')
|
||||
else:
|
||||
return ''
|
||||
|
||||
if diffoutput.startswith('Submodule'):
|
||||
if with_diff_header:
|
||||
return ('', diffoutput)
|
||||
else:
|
||||
return diffoutput
|
||||
|
||||
output = StringIO()
|
||||
|
||||
diff = diffoutput.split('\n')
|
||||
for line in map(core.decode, diff):
|
||||
if not start and '@@' == line[:2] and '@@' in line[2:]:
|
||||
start = True
|
||||
if start or (deleted and del_tag in line):
|
||||
output.write(core.encode(line) + '\n')
|
||||
else:
|
||||
if with_diff_header:
|
||||
headers.append(core.encode(line))
|
||||
elif not suppress_header:
|
||||
output.write(core.encode(line) + '\n')
|
||||
|
||||
result = core.decode(output.getvalue())
|
||||
output.close()
|
||||
|
||||
if with_diff_header:
|
||||
return('\n'.join(headers), result)
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
def format_patchsets(to_export, revs, output='patches'):
|
||||
"""
|
||||
Group contiguous revision selection into patchsets
|
||||
|
||||
Exists to handle multi-selection.
|
||||
Multiple disparate ranges in the revision selection
|
||||
are grouped into continuous lists.
|
||||
|
||||
"""
|
||||
|
||||
outlines = []
|
||||
|
||||
cur_rev = to_export[0]
|
||||
cur_master_idx = revs.index(cur_rev)
|
||||
|
||||
patches_to_export = [[cur_rev]]
|
||||
patchset_idx = 0
|
||||
|
||||
# Group the patches into continuous sets
|
||||
for idx, rev in enumerate(to_export[1:]):
|
||||
# Limit the search to the current neighborhood for efficiency
|
||||
master_idx = revs[cur_master_idx:].index(rev)
|
||||
master_idx += cur_master_idx
|
||||
if master_idx == cur_master_idx + 1:
|
||||
patches_to_export[ patchset_idx ].append(rev)
|
||||
cur_master_idx += 1
|
||||
continue
|
||||
else:
|
||||
patches_to_export.append([ rev ])
|
||||
cur_master_idx = master_idx
|
||||
patchset_idx += 1
|
||||
|
||||
# Export each patchsets
|
||||
status = 0
|
||||
for patchset in patches_to_export:
|
||||
newstatus, out = export_patchset(patchset[0],
|
||||
patchset[-1],
|
||||
output='patches',
|
||||
n=len(patchset) > 1,
|
||||
thread=True,
|
||||
patch_with_stat=True)
|
||||
outlines.append(out)
|
||||
if status == 0:
|
||||
status += newstatus
|
||||
return (status, '\n'.join(outlines))
|
||||
|
||||
|
||||
def export_patchset(start, end, output='patches', **kwargs):
|
||||
"""Export patches from start^ to end."""
|
||||
return git.format_patch('-o', output, start + '^..' + end,
|
||||
with_stderr=True,
|
||||
with_status=True,
|
||||
**kwargs)
|
||||
|
||||
|
||||
def unstage_paths(args, head='HEAD'):
|
||||
status, output = git.reset(head, '--', with_stderr=True, with_status=True,
|
||||
*set(args))
|
||||
if status != 128:
|
||||
return (status, output)
|
||||
# handle git init: we have to use 'git rm --cached'
|
||||
# detect this condition by checking if the file is still staged
|
||||
status, output = git.update_index('--',
|
||||
force_remove=True,
|
||||
with_status=True,
|
||||
with_stderr=True,
|
||||
*set(args))
|
||||
return (status, output)
|
||||
|
||||
|
||||
|
||||
def worktree_state(head='HEAD', staged_only=False):
|
||||
"""Return a tuple of files in various states of being
|
||||
|
||||
Can be staged, unstaged, untracked, unmerged, or changed
|
||||
upstream.
|
||||
|
||||
"""
|
||||
state = worktree_state_dict(head=head, staged_only=staged_only)
|
||||
return(state.get('staged', []),
|
||||
state.get('modified', []),
|
||||
state.get('unmerged', []),
|
||||
state.get('untracked', []),
|
||||
state.get('upstream_changed', []))
|
||||
|
||||
|
||||
def worktree_state_dict(head='HEAD',
|
||||
staged_only=False,
|
||||
update_index=False,
|
||||
git=git):
|
||||
"""Return a dict of files in various states of being
|
||||
|
||||
:rtype: dict, keys are staged, unstaged, untracked, unmerged,
|
||||
changed_upstream, and submodule.
|
||||
|
||||
"""
|
||||
if update_index:
|
||||
git.update_index(refresh=True)
|
||||
|
||||
if staged_only:
|
||||
return _branch_status(head)
|
||||
|
||||
staged, unmerged, submodules = diff_index(head)
|
||||
modified, more_submods = diff_worktree()
|
||||
|
||||
submodules = submodules.union(more_submods)
|
||||
untracked = untracked_files()
|
||||
|
||||
# Look for upstream modified files if this is a tracking branch
|
||||
upstream_changed = diff_upstream(head)
|
||||
|
||||
# Keep stuff sorted
|
||||
staged.sort()
|
||||
modified.sort()
|
||||
unmerged.sort()
|
||||
untracked.sort()
|
||||
upstream_changed.sort()
|
||||
|
||||
return {'staged': staged,
|
||||
'modified': modified,
|
||||
'unmerged': unmerged,
|
||||
'untracked': untracked,
|
||||
'upstream_changed': upstream_changed,
|
||||
'submodules': submodules}
|
||||
|
||||
|
||||
def diff_index(head):
|
||||
submodules = set()
|
||||
staged = []
|
||||
unmerged = []
|
||||
|
||||
output = git.diff_index(head, z=True, cached=True, with_stderr=True)
|
||||
if output.startswith('fatal:'):
|
||||
# handle git init
|
||||
return all_files(), unmerged, submodules
|
||||
|
||||
while output:
|
||||
rest, output = output.split('\0', 1)
|
||||
name, output = output.split('\0', 1)
|
||||
status = rest[-1]
|
||||
name = core.decode(name)
|
||||
if '160000' in rest[1:14]:
|
||||
submodules.add(name)
|
||||
elif status in 'DAM':
|
||||
staged.append(name)
|
||||
elif status == 'U':
|
||||
unmerged.append(name)
|
||||
|
||||
return staged, unmerged, submodules
|
||||
|
||||
|
||||
def diff_worktree():
|
||||
modified = []
|
||||
submodules = set()
|
||||
|
||||
output = git.diff_files(z=True, with_stderr=True)
|
||||
if output.startswith('fatal:'):
|
||||
# handle git init
|
||||
ls_files = git.ls_files(modified=True, z=True)[:-1].split('\0')
|
||||
modified = [core.decode(f) for f in ls_files if f]
|
||||
return modified, submodules
|
||||
|
||||
while output:
|
||||
rest, output = output.split('\0', 1)
|
||||
name, output = output.split('\0', 1)
|
||||
status = rest[-1]
|
||||
name = core.decode(name)
|
||||
if '160000' in rest[1:14]:
|
||||
submodules.add(name)
|
||||
elif status in 'DAM':
|
||||
modified.append(name)
|
||||
|
||||
return modified, submodules
|
||||
|
||||
|
||||
def diff_upstream(head):
|
||||
tracked = tracked_branch()
|
||||
if not tracked:
|
||||
return []
|
||||
diff_expr = merge_base_to(head, tracked)
|
||||
output = git.diff(diff_expr, name_only=True, z=True,
|
||||
**_common_diff_opts())
|
||||
if output.startswith('fatal:'):
|
||||
return []
|
||||
return [core.decode(n) for n in output.split('\0') if n]
|
||||
|
||||
|
||||
def partial_worktree_state_dict(files, head='HEAD'):
|
||||
states = []
|
||||
for path in files:
|
||||
output = git.status(path, porcelain=True)
|
||||
if output == "" or output[1] == " ":
|
||||
states.append(('unmodified', path))
|
||||
status = None
|
||||
else:
|
||||
status = output[1]
|
||||
|
||||
if status == "M" or status == "D" or status == "R":
|
||||
states.append(('modified', path))
|
||||
elif status == "?":
|
||||
states.append(('untracked', path))
|
||||
|
||||
return states
|
||||
|
||||
|
||||
def _branch_status(branch, git=git):
|
||||
"""
|
||||
Returns a tuple of staged, unstaged, untracked, and unmerged files
|
||||
|
||||
This shows only the changes that were introduced in branch
|
||||
|
||||
"""
|
||||
status, output = git.diff(name_only=True,
|
||||
M=True, z=True,
|
||||
with_status=True,
|
||||
*branch.strip().split(),
|
||||
**_common_diff_opts())
|
||||
if status != 0:
|
||||
return {}
|
||||
|
||||
staged = map(core.decode, [n for n in output.split('\0') if n])
|
||||
return {'staged': staged,
|
||||
'upstream_changed': staged}
|
||||
|
||||
|
||||
def merge_base_to(head, ref):
|
||||
"""Given `ref`, return $(git merge-base ref HEAD)..ref."""
|
||||
base = git.merge_base(head, ref)
|
||||
return '%s..%s' % (base, ref)
|
||||
|
||||
|
||||
def merge_base_parent(branch):
|
||||
tracked = tracked_branch(branch=branch)
|
||||
if tracked:
|
||||
return '%s..%s' % (tracked, branch)
|
||||
return 'master..%s' % branch
|
||||
|
||||
|
||||
def eval_path(path):
|
||||
"""handles quoted paths."""
|
||||
if path.startswith('"') and path.endswith('"'):
|
||||
return core.decode(eval(path))
|
||||
else:
|
||||
return core.decode(path)
|
||||
|
||||
|
||||
def renamed_files(start, end, git=git):
|
||||
difflines = git.diff('%s..%s' % (start, end), M=True,
|
||||
**_common_diff_opts()).splitlines()
|
||||
return [eval_path(r[12:].rstrip())
|
||||
for r in difflines if r.startswith('rename from ')]
|
||||
|
||||
|
||||
def changed_files(start, end, git=git):
|
||||
zfiles_str = git.diff('%s..%s' % (start, end),
|
||||
name_only=True, z=True,
|
||||
**_common_diff_opts()).strip('\0')
|
||||
return [core.decode(enc) for enc in zfiles_str.split('\0') if enc]
|
||||
|
||||
|
||||
def parse_ls_tree(rev):
|
||||
"""Return a list of(mode, type, sha1, path) tuples."""
|
||||
lines = git.ls_tree(rev, r=True).splitlines()
|
||||
output = []
|
||||
regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
|
||||
for line in lines:
|
||||
match = regex.match(line)
|
||||
if match:
|
||||
mode = match.group(1)
|
||||
objtype = match.group(2)
|
||||
sha1 = match.group(3)
|
||||
filename = match.group(4)
|
||||
output.append((mode, objtype, sha1, filename,) )
|
||||
return output
|
||||
|
||||
|
||||
# A regex for matching the output of git(log|rev-list) --pretty=oneline
|
||||
REV_LIST_REGEX = re.compile('^([0-9a-f]{40}) (.*)$')
|
||||
|
||||
def parse_rev_list(raw_revs):
|
||||
"""Parse `git log --pretty=online` output into (SHA-1, summary) pairs."""
|
||||
revs = []
|
||||
for line in map(core.decode, raw_revs.splitlines()):
|
||||
match = REV_LIST_REGEX.match(line)
|
||||
if match:
|
||||
rev_id = match.group(1)
|
||||
summary = match.group(2)
|
||||
revs.append((rev_id, summary,))
|
||||
return revs
|
||||
|
||||
|
||||
def log_helper(all=False, extra_args=None):
|
||||
"""Return parallel arrays containing the SHA-1s and summaries."""
|
||||
revs = []
|
||||
summaries = []
|
||||
args = []
|
||||
if extra_args:
|
||||
args = extra_args
|
||||
output = git.log(pretty='oneline', no_color=True, all=all, *args)
|
||||
for line in map(core.decode, output.splitlines()):
|
||||
match = REV_LIST_REGEX.match(line)
|
||||
if match:
|
||||
revs.append(match.group(1))
|
||||
summaries.append(match.group(2))
|
||||
return (revs, summaries)
|
||||
|
||||
|
||||
def rev_list_range(start, end):
|
||||
"""Return a (SHA-1, summary) pairs between start and end."""
|
||||
revrange = '%s..%s' % (start, end)
|
||||
raw_revs = git.rev_list(revrange, pretty='oneline')
|
||||
return parse_rev_list(raw_revs)
|
||||
|
||||
|
||||
def merge_message_path():
|
||||
"""Return the path to .git/MERGE_MSG or .git/SQUASH_MSG."""
|
||||
for basename in ('MERGE_MSG', 'SQUASH_MSG'):
|
||||
path = git.git_path(basename)
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def abort_merge():
|
||||
"""Abort a merge by reading the tree at HEAD."""
|
||||
# Reset the worktree
|
||||
git.read_tree('HEAD', reset=True, u=True, v=True)
|
||||
# remove MERGE_HEAD
|
||||
merge_head = git.git_path('MERGE_HEAD')
|
||||
if os.path.exists(merge_head):
|
||||
os.unlink(merge_head)
|
||||
# remove MERGE_MESSAGE, etc.
|
||||
merge_msg_path = merge_message_path()
|
||||
while merge_msg_path:
|
||||
os.unlink(merge_msg_path)
|
||||
merge_msg_path = merge_message_path()
|
||||
|
||||
|
||||
def merge_message(revision):
|
||||
"""Return a merge message for FETCH_HEAD."""
|
||||
fetch_head = git.git_path('FETCH_HEAD')
|
||||
if os.path.exists(fetch_head):
|
||||
return git.fmt_merge_msg('--file', fetch_head)
|
||||
return "Merge branch '%s'" % revision
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import os
|
||||
|
||||
from cola import git
|
||||
|
||||
|
||||
class GitCola(git.Git):
|
||||
"""
|
||||
Subclass git.Git to provide custom behaviors.
|
||||
|
||||
GitPython throws exceptions by default.
|
||||
We suppress exceptions in favor of return values.
|
||||
"""
|
||||
def __init__(self):
|
||||
git.Git.__init__(self)
|
||||
self.load_worktree(os.getcwd())
|
||||
|
||||
def load_worktree(self, path):
|
||||
self._git_dir = path
|
||||
self._worktree = None
|
||||
self.worktree()
|
||||
|
||||
def worktree(self):
|
||||
if self._worktree:
|
||||
return self._worktree
|
||||
self.git_dir()
|
||||
if self._git_dir:
|
||||
curdir = self._git_dir
|
||||
else:
|
||||
curdir = os.getcwd()
|
||||
|
||||
if self._is_git_dir(os.path.join(curdir, '.git')):
|
||||
return curdir
|
||||
|
||||
# Handle bare repositories
|
||||
if (len(os.path.basename(curdir)) > 4
|
||||
and curdir.endswith('.git')):
|
||||
return curdir
|
||||
if 'GIT_WORK_TREE' in os.environ:
|
||||
self._worktree = os.getenv('GIT_WORK_TREE')
|
||||
if not self._worktree or not os.path.isdir(self._worktree):
|
||||
if self._git_dir:
|
||||
gitparent = os.path.join(os.path.abspath(self._git_dir), '..')
|
||||
self._worktree = os.path.abspath(gitparent)
|
||||
self.set_cwd(self._worktree)
|
||||
return self._worktree
|
||||
|
||||
def is_valid(self):
|
||||
return self._git_dir and self._is_git_dir(self._git_dir)
|
||||
|
||||
def git_dir(self):
|
||||
if self.is_valid():
|
||||
return self._git_dir
|
||||
if 'GIT_DIR' in os.environ:
|
||||
self._git_dir = os.getenv('GIT_DIR')
|
||||
if self._git_dir:
|
||||
curpath = os.path.abspath(self._git_dir)
|
||||
else:
|
||||
curpath = os.path.abspath(os.getcwd())
|
||||
# Search for a .git directory
|
||||
while curpath:
|
||||
if self._is_git_dir(curpath):
|
||||
self._git_dir = curpath
|
||||
break
|
||||
gitpath = os.path.join(curpath, '.git')
|
||||
if self._is_git_dir(gitpath):
|
||||
self._git_dir = gitpath
|
||||
break
|
||||
curpath, dummy = os.path.split(curpath)
|
||||
if not dummy:
|
||||
break
|
||||
return self._git_dir
|
||||
|
||||
def _is_git_dir(self, d):
|
||||
""" This is taken from the git setup.c:is_git_directory
|
||||
function."""
|
||||
if (os.path.isdir(d)
|
||||
and os.path.isdir(os.path.join(d, 'objects'))
|
||||
and os.path.isdir(os.path.join(d, 'refs'))):
|
||||
headref = os.path.join(d, 'HEAD')
|
||||
return (os.path.isfile(headref)
|
||||
or (os.path.islink(headref)
|
||||
and os.readlink(headref).startswith('refs')))
|
||||
return False
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
import os
|
||||
from PyQt4 import QtGui
|
||||
|
||||
import cola
|
||||
import cola.app
|
||||
from cola import core
|
||||
from cola import git
|
||||
from cola import gitcmds
|
||||
from cola import qtutils
|
||||
from cola import signals
|
||||
from cola.widgets.browse import BrowseDialog
|
||||
from cola.widgets.combodlg import ComboDialog
|
||||
from cola.widgets.listview import ListView
|
||||
|
||||
|
||||
def install_command_wrapper(parent):
|
||||
wrapper = CommandWrapper(parent)
|
||||
cola.factory().add_command_wrapper(wrapper)
|
||||
|
||||
|
||||
class CommandWrapper(object):
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.callbacks = {
|
||||
signals.question: self._question,
|
||||
signals.information: self._information,
|
||||
}
|
||||
|
||||
def _question(self, title, msg):
|
||||
return qtutils.question(self.parent, title, msg)
|
||||
|
||||
def _information(self, title, msg):
|
||||
if msg is None:
|
||||
msg = title
|
||||
title = qtutils.tr(title)
|
||||
msg = qtutils.tr(msg)
|
||||
QtGui.QMessageBox.information(self.parent, title, msg)
|
||||
|
||||
|
||||
def choose_from_combo(title, items):
|
||||
"""Quickly choose an item from a list using a combo box"""
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
return ComboDialog(parent, title=title, items=items).selected()
|
||||
|
||||
|
||||
def choose_from_list(title, items=None, dblclick=None):
|
||||
"""Quickly choose an item from a list using a list widget"""
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
return ListView(parent,
|
||||
title=title, items=items, dblclick=dblclick).selected()
|
||||
|
||||
|
||||
def slot_with_parent(fn, parent):
|
||||
"""Return an argument-less method for calling fn(parent=parent)
|
||||
|
||||
:param fn: - Function reference, must accept 'parent' as a keyword
|
||||
:param parent: - Qt parent widget
|
||||
|
||||
"""
|
||||
def slot():
|
||||
fn(parent=parent)
|
||||
return slot
|
||||
|
||||
|
||||
def branch_delete():
|
||||
"""Launch the 'Delete Branch' dialog."""
|
||||
branch = choose_from_combo('Delete Branch',
|
||||
cola.model().local_branches)
|
||||
if not branch:
|
||||
return
|
||||
cola.notifier().broadcast(signals.delete_branch, branch)
|
||||
|
||||
|
||||
def branch_diff():
|
||||
"""Diff against an arbitrary revision, branch, tag, etc."""
|
||||
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
|
||||
['HEAD^'] +
|
||||
cola.model().all_branches() +
|
||||
cola.model().tags)
|
||||
if not branch:
|
||||
return
|
||||
cola.notifier().broadcast(signals.diff_mode, branch)
|
||||
|
||||
|
||||
def browse_current():
|
||||
"""Launch the 'Browse Current Branch' dialog."""
|
||||
branch = gitcmds.current_branch()
|
||||
BrowseDialog.browse(branch)
|
||||
|
||||
|
||||
def browse_other():
|
||||
"""Prompt for a branch and inspect content at that point in time."""
|
||||
# Prompt for a branch to browse
|
||||
branch = choose_from_combo('Browse Revision...', gitcmds.all_refs())
|
||||
if not branch:
|
||||
return
|
||||
BrowseDialog.browse(branch)
|
||||
|
||||
|
||||
def checkout_branch():
|
||||
"""Launch the 'Checkout Branch' dialog."""
|
||||
branch = choose_from_combo('Checkout Branch',
|
||||
cola.model().local_branches)
|
||||
if not branch:
|
||||
return
|
||||
cola.notifier().broadcast(signals.checkout_branch, branch)
|
||||
|
||||
|
||||
def cherry_pick():
|
||||
"""Launch the 'Cherry-Pick' dialog."""
|
||||
from cola.controllers.selectcommits import select_commits
|
||||
|
||||
revs, summaries = gitcmds.log_helper(all=True)
|
||||
commits = select_commits('Cherry-Pick Commit',
|
||||
revs, summaries, multiselect=False)
|
||||
if not commits:
|
||||
return
|
||||
cola.notifier().broadcast(signals.cherry_pick, commits)
|
||||
|
||||
|
||||
def clone_repo(spawn=True):
|
||||
"""
|
||||
Present GUI controls for cloning a repository
|
||||
|
||||
A new cola session is invoked when 'spawn' is True.
|
||||
|
||||
"""
|
||||
url, ok = qtutils.prompt('Path or URL to clone (Env. $VARS okay)')
|
||||
url = os.path.expandvars(core.encode(url))
|
||||
if not ok or not url:
|
||||
return None
|
||||
try:
|
||||
# Pick a suitable basename by parsing the URL
|
||||
newurl = url.replace('\\', '/')
|
||||
default = newurl.rsplit('/', 1)[-1]
|
||||
if default == '.git':
|
||||
# The end of the URL is /.git, so assume it's a file path
|
||||
default = os.path.basename(os.path.dirname(newurl))
|
||||
if default.endswith('.git'):
|
||||
# The URL points to a bare repo
|
||||
default = default[:-4]
|
||||
if url == '.':
|
||||
# The URL is the current repo
|
||||
default = os.path.basename(os.getcwd())
|
||||
if not default:
|
||||
raise
|
||||
except:
|
||||
cola.notifier().broadcast(signals.information,
|
||||
'Error Cloning',
|
||||
'Could not parse: "%s"' % url)
|
||||
qtutils.log(1, 'Oops, could not parse git url: "%s"' % url)
|
||||
return None
|
||||
|
||||
# Prompt the user for a directory to use as the parent directory
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
msg = 'Select a parent directory for the new clone'
|
||||
dirname = qtutils.opendir_dialog(parent, msg, cola.model().getcwd())
|
||||
if not dirname:
|
||||
return None
|
||||
count = 1
|
||||
dirname = core.decode(dirname)
|
||||
destdir = os.path.join(dirname, core.decode(default))
|
||||
olddestdir = destdir
|
||||
if os.path.exists(destdir):
|
||||
# An existing path can be specified
|
||||
msg = ('"%s" already exists, cola will create a new directory' %
|
||||
destdir)
|
||||
cola.notifier().broadcast(signals.information,
|
||||
'Directory Exists', msg)
|
||||
|
||||
# Make sure the new destdir doesn't exist
|
||||
while os.path.exists(destdir):
|
||||
destdir = olddestdir + str(count)
|
||||
count += 1
|
||||
cola.notifier().broadcast(signals.clone, core.decode(url), destdir,
|
||||
spawn=spawn)
|
||||
return destdir
|
||||
|
||||
|
||||
def export_patches():
|
||||
"""Run 'git format-patch' on a list of commits."""
|
||||
from cola.controllers.selectcommits import select_commits
|
||||
|
||||
revs, summaries = gitcmds.log_helper()
|
||||
to_export = select_commits('Export Patches', revs, summaries)
|
||||
if not to_export:
|
||||
return
|
||||
to_export.reverse()
|
||||
revs.reverse()
|
||||
cola.notifier().broadcast(signals.format_patch, to_export, revs)
|
||||
|
||||
|
||||
def diff_branch():
|
||||
"""Launches a diff against a branch."""
|
||||
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
|
||||
['HEAD^'] +
|
||||
cola.model().all_branches() +
|
||||
cola.model().tags)
|
||||
if not branch:
|
||||
return
|
||||
zfiles_str = git.instance().diff(branch, name_only=True,
|
||||
no_color=True,
|
||||
z=True).rstrip('\0')
|
||||
files = zfiles_str.split('\0')
|
||||
filename = choose_from_list('Select File', files)
|
||||
if not filename:
|
||||
return
|
||||
cola.notifier().broadcast(signals.branch_mode, branch, filename)
|
||||
|
||||
|
||||
def diff_expression():
|
||||
"""Diff using an arbitrary expression."""
|
||||
expr = choose_from_combo('Enter Diff Expression',
|
||||
cola.model().all_branches() +
|
||||
cola.model().tags)
|
||||
if not expr:
|
||||
return
|
||||
cola.notifier().broadcast(signals.diff_expr_mode, expr)
|
||||
|
||||
|
||||
def goto_grep(line):
|
||||
"""Called when Search -> Grep's right-click 'goto' action."""
|
||||
filename, line_number, contents = line.split(':', 2)
|
||||
filename = core.encode(filename)
|
||||
cola.notifier().broadcast(signals.edit, [filename], line_number=line_number)
|
||||
|
||||
|
||||
def grep():
|
||||
"""Prompt and use 'git grep' to find the content."""
|
||||
# This should be a command in cola.cmds.
|
||||
txt, ok = qtutils.prompt('grep')
|
||||
if not ok:
|
||||
return
|
||||
cola.notifier().broadcast(signals.grep, txt)
|
||||
|
||||
|
||||
def open_repo(parent):
|
||||
"""Spawn a new cola session."""
|
||||
dirname = qtutils.opendir_dialog(parent,
|
||||
'Open Git Repository...',
|
||||
cola.model().getcwd())
|
||||
if not dirname:
|
||||
return
|
||||
cola.notifier().broadcast(signals.open_repo, dirname)
|
||||
|
||||
def open_repo_slot(parent):
|
||||
return slot_with_parent(open_repo, parent)
|
||||
|
||||
|
||||
def load_commitmsg(parent):
|
||||
"""Load a commit message from a file."""
|
||||
filename = qtutils.open_dialog(parent,
|
||||
'Load Commit Message...',
|
||||
cola.model().getcwd())
|
||||
if filename:
|
||||
cola.notifier().broadcast(signals.load_commit_message, filename)
|
||||
|
||||
def load_commitmsg_slot(parent):
|
||||
return slot_with_parent(load_commitmsg, parent)
|
||||
|
||||
|
||||
def rebase():
|
||||
"""Rebase onto a branch."""
|
||||
branch = choose_from_combo('Rebase Branch',
|
||||
cola.model().all_branches())
|
||||
if not branch:
|
||||
return
|
||||
#TODO cmd
|
||||
status, output = git.instance().rebase(branch,
|
||||
with_stderr=True,
|
||||
with_status=True)
|
||||
qtutils.log(status, output)
|
||||
|
||||
|
||||
def review_branch():
|
||||
"""Diff against an arbitrary revision, branch, tag, etc."""
|
||||
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
|
||||
cola.model().all_branches() +
|
||||
cola.model().tags)
|
||||
if not branch:
|
||||
return
|
||||
cola.notifier().broadcast(signals.review_branch_mode, branch)
|
||||
|
||||
|
||||
def fetch(parent):
|
||||
"""Launch the 'fetch' remote dialog."""
|
||||
from cola.controllers.remote import remote_action
|
||||
|
||||
remote_action(parent, 'fetch')
|
||||
|
||||
def fetch_slot(parent):
|
||||
return slot_with_parent(fetch, parent)
|
||||
|
||||
|
||||
def push(parent):
|
||||
"""Launch the 'push' remote dialog."""
|
||||
from cola.controllers.remote import remote_action
|
||||
|
||||
remote_action(parent, 'push')
|
||||
|
||||
def push_slot(parent):
|
||||
return slot_with_parent(push, parent)
|
||||
|
||||
|
||||
def pull(parent):
|
||||
"""Launch the 'pull' remote dialog."""
|
||||
from cola.controllers.remote import remote_action
|
||||
|
||||
remote_action(parent, 'pull')
|
||||
|
||||
def pull_slot(parent):
|
||||
return slot_with_parent(pull, parent)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""i18n and l10n support for git-cola"""
|
||||
|
||||
import gettext as _gettext
|
||||
import os
|
||||
import sys
|
||||
|
||||
from cola import resources
|
||||
|
||||
_null_translation = _gettext.NullTranslations()
|
||||
_translation = _null_translation
|
||||
|
||||
|
||||
def gettext(s):
|
||||
return _translation.ugettext(s)
|
||||
|
||||
|
||||
def ngettext(s, p, n):
|
||||
return _translation.ungettext(s, p, n)
|
||||
|
||||
|
||||
def N_(s):
|
||||
return s
|
||||
|
||||
|
||||
def install(locale):
|
||||
global _translation
|
||||
if sys.platform == 'win32':
|
||||
_check_win32_locale()
|
||||
if locale:
|
||||
os.environ['LANG'] = locale
|
||||
os.environ['LC_MESSAGES'] = locale
|
||||
_gettext.textdomain('messages')
|
||||
_translation = _gettext.translation('git-cola',
|
||||
localedir=_get_locale_dir(),
|
||||
fallback=True)
|
||||
|
||||
def uninstall():
|
||||
global _translation
|
||||
_translation = _null_translation
|
||||
|
||||
|
||||
def _get_locale_dir():
|
||||
return resources.prefix('share', 'locale')
|
||||
|
||||
|
||||
def _check_win32_locale():
|
||||
for i in ('LANGUAGE','LC_ALL','LC_MESSAGES','LANG'):
|
||||
if os.environ.get(i):
|
||||
break
|
||||
else:
|
||||
lang = None
|
||||
import locale
|
||||
try:
|
||||
import ctypes
|
||||
except ImportError:
|
||||
# use only user's default locale
|
||||
lang = locale.getdefaultlocale()[0]
|
||||
else:
|
||||
# using ctypes to determine all locales
|
||||
lcid_user = ctypes.windll.kernel32.GetUserDefaultLCID()
|
||||
lcid_system = ctypes.windll.kernel32.GetSystemDefaultLCID()
|
||||
if lcid_user != lcid_system:
|
||||
lcid = [lcid_user, lcid_system]
|
||||
else:
|
||||
lcid = [lcid_user]
|
||||
lang = [locale.windows_locale.get(i) for i in lcid]
|
||||
lang = ':'.join([i for i in lang if i])
|
||||
# set lang code for gettext
|
||||
if lang:
|
||||
os.environ['LANGUAGE'] = lang
|
||||
|
||||
|
||||
# additional strings for translation
|
||||
if 0:
|
||||
# file kinds
|
||||
N_('file')
|
||||
N_('directory')
|
||||
N_('symlink')
|
||||
# bugs status
|
||||
N_('fixed')
|
||||
# qcat titles for various file types
|
||||
N_('View text file')
|
||||
N_('View image file')
|
||||
N_('View binary file')
|
||||
N_('View symlink')
|
||||
N_('View directory')
|
||||
#
|
||||
N_("No changes selected to commit")
|
||||
N_("No changes selected to revert")
|
||||
+144
-70
@@ -2,7 +2,9 @@
|
||||
"""Provides an inotify plugin for Linux and other systems with pyinotify"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from threading import Timer
|
||||
from threading import Lock
|
||||
from cola import utils
|
||||
|
||||
try:
|
||||
import pyinotify
|
||||
@@ -14,25 +16,37 @@ try:
|
||||
except ImportError:
|
||||
ProcessEvent = object
|
||||
AVAILABLE = False
|
||||
pass
|
||||
|
||||
if utils.is_win32():
|
||||
try:
|
||||
import win32file
|
||||
import win32con
|
||||
import pywintypes
|
||||
import win32event
|
||||
AVAILABLE = True
|
||||
except ImportError:
|
||||
ProcessEvent = object
|
||||
AVAILABLE = False
|
||||
|
||||
from PyQt4 import QtCore
|
||||
|
||||
import cola
|
||||
from cola import core
|
||||
from cola import signals
|
||||
from cola import utils
|
||||
|
||||
INOTIFY_EVENT = QtCore.QEvent.User + 0
|
||||
|
||||
from cola.compat import set
|
||||
|
||||
_thread = None
|
||||
def start():
|
||||
global _thread
|
||||
if not AVAILABLE:
|
||||
if not utils.is_linux():
|
||||
if utils.is_win32():
|
||||
msg = ('file notification: disabled\n'
|
||||
'Note: install pywin32 to enable.\n')
|
||||
elif utils.is_linux():
|
||||
msg = ('inotify: disabled\n'
|
||||
'Note: install python-pyinotify to enable inotify.\n')
|
||||
else:
|
||||
return
|
||||
msg = ('inotify: disabled\n'
|
||||
'Note: install python-pyinotify to enable inotify.\n')
|
||||
|
||||
if utils.is_debian():
|
||||
msg += ('On Debian systems '
|
||||
@@ -43,65 +57,75 @@ def start():
|
||||
# Start the notification thread
|
||||
_thread = GitNotifier()
|
||||
_thread.start()
|
||||
msg = 'inotify support: enabled'
|
||||
if utils.is_win32():
|
||||
msg = 'file notification: enabled'
|
||||
else:
|
||||
msg = 'inotify support: enabled'
|
||||
cola.notifier().broadcast(signals.log_cmd, 0, msg)
|
||||
cola.notifier().broadcast(signals.inotify, True)
|
||||
|
||||
|
||||
def stop():
|
||||
if not has_inotify():
|
||||
return
|
||||
_thread.set_abort(True)
|
||||
_thread.quit()
|
||||
_thread.stop(True)
|
||||
_thread.wait()
|
||||
|
||||
|
||||
|
||||
def has_inotify():
|
||||
"""Return True if pyinotify is available."""
|
||||
return AVAILABLE and _thread and _thread.isRunning()
|
||||
|
||||
|
||||
class EventReceiver(QtCore.QObject):
|
||||
def event(self, msg):
|
||||
"""Overrides event() to handle custom inotify events."""
|
||||
if not AVAILABLE:
|
||||
return
|
||||
if msg.type() == INOTIFY_EVENT:
|
||||
cola.notifier().broadcast(signals.rescan)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
class Handler():
|
||||
"""Queues filesystem events for broadcast"""
|
||||
|
||||
def __init__(self):
|
||||
"""Create an event handler"""
|
||||
## Timer used to prevent notification floods
|
||||
self._timer = None
|
||||
## The files that have changed since the last broadcast
|
||||
self._files = set()
|
||||
## Lock to protect files and timer from threading issues
|
||||
self._lock = Lock()
|
||||
|
||||
def broadcast(self):
|
||||
"""Broadcasts a list of all files touched since last broadcast"""
|
||||
with self._lock:
|
||||
cola.notifier().broadcast(signals.update_status, self._files)
|
||||
self._files = set()
|
||||
self._timer = None
|
||||
|
||||
def handle(self, path):
|
||||
"""Queues up filesystem events for broadcast"""
|
||||
with self._lock:
|
||||
self._files.add(path)
|
||||
if self._timer is None:
|
||||
self._timer = Timer(0.333, self.broadcast)
|
||||
self._timer.start()
|
||||
|
||||
|
||||
class FileSysEvent(ProcessEvent):
|
||||
"""Generated by GitNotifier in response to inotify events"""
|
||||
|
||||
def __init__(self, parent):
|
||||
"""Keep a reference to the QThread parent and maintain event state"""
|
||||
def __init__(self):
|
||||
"""Maintain event state"""
|
||||
ProcessEvent.__init__(self)
|
||||
## The Qt parent
|
||||
self._parent = parent
|
||||
## Timer used to prevents notification floods
|
||||
self._last_event_time = time.time()
|
||||
## Takes care of Queueing events for broadcast
|
||||
self._handler = Handler()
|
||||
|
||||
def process_default(self, event):
|
||||
"""Notifies the Qt parent when actions occur"""
|
||||
## Limit the notification frequency to 1 per second
|
||||
if time.time() - self._last_event_time > 1.0:
|
||||
self._parent.notify()
|
||||
self._last_event_time = time.time()
|
||||
"""Queues up inotify events for broadcast"""
|
||||
if event.name is not None:
|
||||
path = os.path.join(event.path, event.name)
|
||||
path = os.path.relpath(path)
|
||||
self._handler.handle(path)
|
||||
|
||||
|
||||
class GitNotifier(QtCore.QThread):
|
||||
"""Polls inotify for changes and generates FileSysEvents"""
|
||||
|
||||
def __init__(self, timeout=250):
|
||||
def __init__(self, timeout=333):
|
||||
"""Set up the pyinotify thread"""
|
||||
QtCore.QThread.__init__(self)
|
||||
self.setTerminationEnabled(True)
|
||||
|
||||
## QApplication receiver of Qt events
|
||||
self._receiver = EventReceiver()
|
||||
## Git command object
|
||||
self._git = cola.model().git
|
||||
## pyinotify timeout
|
||||
@@ -109,33 +133,31 @@ class GitNotifier(QtCore.QThread):
|
||||
## Path to monitor
|
||||
self._path = self._git.worktree()
|
||||
## Signals thread termination
|
||||
self._abort = False
|
||||
self._running = True
|
||||
## Directories to watching
|
||||
self._dirs_seen = set()
|
||||
## The inotify watch manager instantiated in run()
|
||||
self._wmgr = None
|
||||
## Events to capture
|
||||
self._mask = (EventsCodes.ALL_FLAGS['IN_CREATE'] |
|
||||
EventsCodes.ALL_FLAGS['IN_DELETE'] |
|
||||
EventsCodes.ALL_FLAGS['IN_MODIFY'] |
|
||||
EventsCodes.ALL_FLAGS['IN_MOVED_TO'])
|
||||
if utils.is_linux():
|
||||
self._mask = (EventsCodes.ALL_FLAGS['IN_ATTRIB'] |
|
||||
EventsCodes.ALL_FLAGS['IN_CLOSE_WRITE'] |
|
||||
EventsCodes.ALL_FLAGS['IN_DELETE'] |
|
||||
EventsCodes.ALL_FLAGS['IN_MODIFY'] |
|
||||
EventsCodes.ALL_FLAGS['IN_MOVED_TO'])
|
||||
|
||||
def set_abort(self, abort):
|
||||
"""Tells the GitNotifier to abort"""
|
||||
self._abort = abort
|
||||
|
||||
def notify(self):
|
||||
"""Post a Qt event in response to inotify updates"""
|
||||
if not self._abort:
|
||||
event_type = QtCore.QEvent.Type(INOTIFY_EVENT)
|
||||
event = QtCore.QEvent(event_type)
|
||||
QtCore.QCoreApplication.postEvent(self._receiver, event)
|
||||
def stop(self, stopped):
|
||||
"""Tells the GitNotifier to stop"""
|
||||
self._timeout = 0
|
||||
self._running = not stopped
|
||||
|
||||
def _watch_directory(self, directory):
|
||||
"""Set up a directory for monitoring by inotify"""
|
||||
if not self._wmgr:
|
||||
return
|
||||
directory = os.path.realpath(directory)
|
||||
if not os.path.exists(directory):
|
||||
return
|
||||
if directory not in self._dirs_seen:
|
||||
self._wmgr.add_watch(directory, self._mask)
|
||||
self._dirs_seen.add(directory)
|
||||
@@ -153,30 +175,82 @@ class GitNotifier(QtCore.QThread):
|
||||
|
||||
def run(self):
|
||||
"""Create the inotify WatchManager and generate FileSysEvents"""
|
||||
|
||||
if utils.is_win32():
|
||||
self.run_win32()
|
||||
return
|
||||
|
||||
# Only capture events that git cares about
|
||||
self._wmgr = WatchManager()
|
||||
if self._is_pyinotify_08x():
|
||||
notifier = Notifier(self._wmgr, FileSysEvent(self),
|
||||
notifier = Notifier(self._wmgr, FileSysEvent(),
|
||||
timeout=self._timeout)
|
||||
else:
|
||||
notifier = Notifier(self._wmgr, FileSysEvent(self))
|
||||
dirs_seen = set()
|
||||
added_flag = False
|
||||
# self._abort signals app termination. The timeout is a tradeoff
|
||||
notifier = Notifier(self._wmgr, FileSysEvent())
|
||||
|
||||
self._watch_directory(self._path)
|
||||
|
||||
# Register files/directories known to git
|
||||
for filename in core.decode(self._git.ls_files()).splitlines():
|
||||
filename = os.path.realpath(filename)
|
||||
directory = os.path.dirname(filename)
|
||||
self._watch_directory(directory)
|
||||
|
||||
# self._running signals app termination. The timeout is a tradeoff
|
||||
# between fast notification response and waiting too long to exit.
|
||||
while not self._abort:
|
||||
if not added_flag:
|
||||
self._watch_directory(self._path)
|
||||
# Register files/directories known to git
|
||||
for filename in self._git.ls_files().splitlines():
|
||||
directory = os.path.dirname(filename)
|
||||
self._watch_directory(directory)
|
||||
added_flag = True
|
||||
notifier.process_events()
|
||||
while self._running:
|
||||
if self._is_pyinotify_08x():
|
||||
check = notifier.check_events()
|
||||
else:
|
||||
check = notifier.check_events(timeout=self._timeout)
|
||||
if not self._running:
|
||||
break
|
||||
if check:
|
||||
notifier.read_events()
|
||||
notifier.process_events()
|
||||
notifier.stop()
|
||||
|
||||
def run_win32(self):
|
||||
"""Generate notifications using pywin32"""
|
||||
|
||||
hDir = win32file.CreateFile(
|
||||
self._path,
|
||||
0x0001,
|
||||
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE,
|
||||
None,
|
||||
win32con.OPEN_EXISTING,
|
||||
win32con.FILE_FLAG_BACKUP_SEMANTICS |
|
||||
win32con.FILE_FLAG_OVERLAPPED,
|
||||
None)
|
||||
|
||||
flags = (win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
|
||||
win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
|
||||
win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
|
||||
win32con.FILE_NOTIFY_CHANGE_SIZE |
|
||||
win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
|
||||
win32con.FILE_NOTIFY_CHANGE_SECURITY)
|
||||
|
||||
buf = win32file.AllocateReadBuffer(8192)
|
||||
overlapped = pywintypes.OVERLAPPED()
|
||||
overlapped.hEvent = win32event.CreateEvent(None, 0, 0, None)
|
||||
|
||||
handler = Handler()
|
||||
while self._running:
|
||||
win32file.ReadDirectoryChangesW(hDir,
|
||||
buf,
|
||||
True,
|
||||
flags,
|
||||
overlapped)
|
||||
|
||||
rc = win32event.WaitForSingleObject(overlapped.hEvent, self._timeout)
|
||||
if rc == win32event.WAIT_OBJECT_0:
|
||||
nbytes = win32file.GetOverlappedResult(hDir, overlapped, True)
|
||||
if nbytes:
|
||||
results = win32file.FILE_NOTIFY_INFORMATION(buf, nbytes)
|
||||
for action, path in results:
|
||||
if not self._running:
|
||||
break
|
||||
|
||||
path = path.replace('\\', '/')
|
||||
if not path.startswith('.git/') and os.path.isfile(path):
|
||||
handler.handle(path)
|
||||
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
# Copyright (C) 2009, David Aguilar <davvid@gmail.com>
|
||||
"""Provides the main() routine used by the git-cola script"""
|
||||
|
||||
import optparse
|
||||
import signal
|
||||
import glob
|
||||
import sys
|
||||
import os
|
||||
|
||||
from cola import resources
|
||||
from cola import utils
|
||||
from cola import core
|
||||
from cola import git
|
||||
from cola import inotify
|
||||
from cola import version
|
||||
|
||||
# Spoof an X11 display for SSH
|
||||
os.environ.setdefault('DISPLAY', ':0')
|
||||
|
||||
# Provide an SSH_ASKPASS fallback
|
||||
if sys.platform == 'darwin':
|
||||
os.environ.setdefault('SSH_ASKPASS',
|
||||
resources.share('bin', 'ssh-askpass-darwin'))
|
||||
else:
|
||||
os.environ.setdefault('SSH_ASKPASS',
|
||||
resources.share('bin', 'ssh-askpass'))
|
||||
|
||||
|
||||
def main():
|
||||
"""Parses the command-line arguments and starts git-cola
|
||||
"""
|
||||
parser = optparse.OptionParser(usage='%prog [options]')
|
||||
|
||||
# We also accept 'git cola version'
|
||||
parser.add_option('-v', '--version',
|
||||
help='Show cola version',
|
||||
dest='version',
|
||||
default=False,
|
||||
action='store_true')
|
||||
|
||||
# Accept git cola --classic
|
||||
parser.add_option('--classic',
|
||||
help='Launch cola classic',
|
||||
dest='classic',
|
||||
default=False,
|
||||
action='store_true')
|
||||
|
||||
# Accept --style=/path/to/style.qss or --style=dark for built-in styles
|
||||
parser.add_option('-s', '--style',
|
||||
help='Applies an alternate stylesheet. '
|
||||
'The allowed values are: "dark" or a file path.',
|
||||
dest='style',
|
||||
metavar='PATH or STYLE',
|
||||
default='')
|
||||
|
||||
# Specifies a git repository to open
|
||||
parser.add_option('-r', '--repo',
|
||||
help='Specifies the path to a git repository.',
|
||||
dest='repo',
|
||||
metavar='PATH',
|
||||
default=os.getcwd())
|
||||
|
||||
# Used on Windows for adding 'git' to the path
|
||||
parser.add_option('-g', '--git-path',
|
||||
help='Specifies the path to the git binary',
|
||||
dest='git',
|
||||
metavar='PATH',
|
||||
default='')
|
||||
opts, args = parser.parse_args()
|
||||
|
||||
if opts.version or 'version' in args:
|
||||
# Accept 'git cola --version' or 'git cola version'
|
||||
git.Git.execute(['git', 'update-index', '--refresh'])
|
||||
print 'cola version', version.version()
|
||||
sys.exit(0)
|
||||
|
||||
if opts.git:
|
||||
# Adds git to the PATH. This is needed on Windows.
|
||||
path_entries = os.environ.get('PATH', '').split(os.pathsep)
|
||||
path_entries.insert(0, os.path.dirname(opts.git))
|
||||
os.environ['PATH'] = os.pathsep.join(path_entries)
|
||||
|
||||
# Bail out if --repo is not a directory
|
||||
repo = os.path.realpath(opts.repo)
|
||||
if not os.path.isdir(repo):
|
||||
print >> sys.stderr, "fatal: '%s' is not a directory. Consider supplying -r <path>.\n" % repo
|
||||
sys.exit(-1)
|
||||
|
||||
# We do everything relative to the repo root
|
||||
os.chdir(opts.repo)
|
||||
|
||||
try:
|
||||
# Defer these imports to allow git cola --version without pyqt installed
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
except ImportError:
|
||||
print >> sys.stderr, 'Sorry, you do not seem to have PyQt4 installed.'
|
||||
print >> sys.stderr, 'Please install it before using cola.'
|
||||
print >> sys.stderr, 'e.g.: sudo apt-get install python-qt4'
|
||||
sys.exit(-1)
|
||||
|
||||
# Import cola modules
|
||||
import cola
|
||||
from cola.models.gitrepo import GitRepoModel
|
||||
from cola.views.main import MainView
|
||||
from cola.views.repo import RepoTreeView
|
||||
from cola.controllers.main import MainController
|
||||
from cola.controllers.classic import ClassicController
|
||||
from cola.app import ColaApplication
|
||||
from cola import qtutils
|
||||
from cola import commands
|
||||
|
||||
|
||||
# Allow Ctrl-C to exit
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
# Initialize the app
|
||||
app = ColaApplication(sys.argv)
|
||||
|
||||
style = None
|
||||
if opts.style:
|
||||
# This loads the built-in and user-specified stylesheets.
|
||||
# We allows absolute and relative paths to a stylesheet
|
||||
# by assuming that non-file arguments refer to a built-in style.
|
||||
if os.path.isabs(opts.style) or os.path.isfile(opts.style):
|
||||
filename = opts.style
|
||||
else:
|
||||
filename = resources.stylesheet(opts.style)
|
||||
|
||||
if filename and os.path.exists(filename):
|
||||
# Automatically register each subdirectory in the style dir
|
||||
# as a Qt resource directory.
|
||||
_setup_resource_dir(os.path.dirname(filename))
|
||||
stylesheet = open(filename, 'r')
|
||||
style = core.read_nointr(stylesheet)
|
||||
stylesheet.close()
|
||||
app.setStyleSheet(style)
|
||||
else:
|
||||
_setup_resource_dir(resources.style_dir())
|
||||
print >> sys.stderr, ("warn: '%s' is not a valid style."
|
||||
% opts.style)
|
||||
else:
|
||||
# Add the default style dir so that we find our icons
|
||||
_setup_resource_dir(resources.style_dir())
|
||||
|
||||
# Ensure that we're working in a valid git repository.
|
||||
# If not, try to find one. When found, chdir there.
|
||||
model = cola.model()
|
||||
valid = model.use_worktree(repo)
|
||||
while not valid:
|
||||
gitdir = qtutils.opendir_dialog(app.activeWindow(),
|
||||
'Open Git Repository...',
|
||||
os.getcwd())
|
||||
if not gitdir:
|
||||
sys.exit(-1)
|
||||
valid = model.use_worktree(gitdir)
|
||||
|
||||
# Finally, go to the root of the git repo
|
||||
os.chdir(model.git.worktree())
|
||||
|
||||
# Register model commands
|
||||
commands.register()
|
||||
|
||||
# Show the GUI and start the event loop
|
||||
if opts.classic:
|
||||
view = RepoTreeView()
|
||||
view.setModel(GitRepoModel(view))
|
||||
controller = ClassicController(view)
|
||||
else:
|
||||
view = MainView()
|
||||
controller = MainController(model, view)
|
||||
|
||||
# Scan for the first time
|
||||
model.update_status()
|
||||
|
||||
# Start the inotify thread
|
||||
inotify.start()
|
||||
|
||||
# Make sure that we start out on top
|
||||
view.raise_()
|
||||
|
||||
# Show the view and start the main event loop
|
||||
view.show()
|
||||
result = app.exec_()
|
||||
|
||||
# All done, cleanup
|
||||
inotify.stop()
|
||||
QtCore.QThreadPool.globalInstance().waitForDone()
|
||||
|
||||
pattern = cola.model().tmp_file_pattern()
|
||||
for filename in glob.glob(pattern):
|
||||
os.unlink(filename)
|
||||
sys.exit(result)
|
||||
|
||||
|
||||
def _setup_resource_dir(dirname):
|
||||
"""Adds resource directories to Qt's search path"""
|
||||
from PyQt4 import QtCore
|
||||
resource_paths = resources.resource_dirs(dirname)
|
||||
for r in resource_paths:
|
||||
basename = os.path.basename(r)
|
||||
QtCore.QDir.setSearchPaths(basename, [r])
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Provides the main application controller."""
|
||||
|
||||
from cola.ctrl import Controller
|
||||
from cola import signals
|
||||
|
||||
|
||||
class MainController(Controller):
|
||||
def __init__(self, model, view):
|
||||
Controller.__init__(self, model, view)
|
||||
|
||||
self.add_global_command(signals.amend_mode)
|
||||
self.add_global_command(signals.diffstat)
|
||||
self.add_global_command(signals.load_commit_message)
|
||||
self.add_global_command(signals.load_commit_template)
|
||||
self.add_global_command(signals.load_previous_message)
|
||||
self.add_global_command(signals.rescan)
|
||||
self.add_global_command(signals.reset_mode)
|
||||
self.add_global_command(signals.run_config_action)
|
||||
self.add_global_command(signals.signoff)
|
||||
self.add_global_command(signals.stage_untracked)
|
||||
self.add_global_command(signals.stage_modified)
|
||||
self.add_global_command(signals.stage_untracked)
|
||||
self.add_global_command(signals.unstage_all)
|
||||
self.add_global_command(signals.unstage_selected)
|
||||
self.add_global_command(signals.visualize_all)
|
||||
self.add_global_command(signals.visualize_current)
|
||||
@@ -0,0 +1,558 @@
|
||||
# Copyright (c) 2008 David Aguilar
|
||||
"""This module provides the central cola model.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import copy
|
||||
|
||||
from cola import core
|
||||
from cola import utils
|
||||
from cola import git
|
||||
from cola import gitcfg
|
||||
from cola import gitcmds
|
||||
from cola import serializer
|
||||
from cola.compat import set
|
||||
from cola.obsmodel import ObservableModel, OMSerializer
|
||||
from cola.decorators import memoize
|
||||
|
||||
|
||||
# Static GitConfig instance
|
||||
_config = gitcfg.instance()
|
||||
|
||||
|
||||
# Provides access to a global MainModel instance
|
||||
@memoize
|
||||
def model():
|
||||
"""Returns the main model singleton"""
|
||||
return MainModel()
|
||||
|
||||
|
||||
class MainSerializer(OMSerializer):
|
||||
def post_decode_hook(self):
|
||||
OMSerializer.post_decode_hook(self)
|
||||
self.obj.generate_remote_helpers()
|
||||
|
||||
|
||||
class MainModel(ObservableModel):
|
||||
"""Provides a friendly wrapper for doing common git operations."""
|
||||
|
||||
# Observable messages
|
||||
message_about_to_update = 'about_to_update'
|
||||
message_commit_message_changed = 'commit_message_changed'
|
||||
message_diff_text_changed = 'diff_text_changed'
|
||||
message_filename_changed = 'filename_changed'
|
||||
message_head_changed = 'head_changed'
|
||||
message_mode_changed = 'mode_changed'
|
||||
message_updated = 'updated'
|
||||
|
||||
# States
|
||||
mode_none = 'none' # Default: nothing's happened, do nothing
|
||||
mode_worktree = 'worktree' # Comparing index to worktree
|
||||
mode_index = 'index' # Comparing index to last commit
|
||||
mode_amend = 'amend' # Amending a commit
|
||||
mode_grep = 'grep' # We ran Search -> Grep
|
||||
mode_branch = 'branch' # Applying changes from a branch
|
||||
mode_diff = 'diff' # Diffing against an arbitrary branch
|
||||
mode_diff_expr = 'diff_expr' # Diffing using arbitrary expression
|
||||
mode_review = 'review' # Reviewing a branch
|
||||
|
||||
# Modes where we don't do anything like staging, etc.
|
||||
modes_read_only = (mode_branch, mode_grep,
|
||||
mode_diff, mode_diff_expr, mode_review)
|
||||
# Modes where we can checkout files from the $head
|
||||
modes_undoable = (mode_none, mode_index, mode_worktree)
|
||||
|
||||
unstaged = property(lambda self: self.modified + self.unmerged + self.untracked)
|
||||
"""An aggregate of the modified, unmerged, and untracked file lists."""
|
||||
|
||||
def __init__(self, cwd=None):
|
||||
"""Reads git repository settings and sets several methods
|
||||
so that they refer to the git module. This object
|
||||
encapsulates cola's interaction with git."""
|
||||
ObservableModel.__init__(self)
|
||||
|
||||
# Initialize the git command object
|
||||
self.git = git.instance()
|
||||
|
||||
#####################################################
|
||||
self.head = 'HEAD'
|
||||
self.diff_text = ''
|
||||
self.mode = self.mode_none
|
||||
self.filename = None
|
||||
self.currentbranch = ''
|
||||
self.trackedbranch = ''
|
||||
self.directory = ''
|
||||
self.git_version = self.git.version()
|
||||
self.remotes = []
|
||||
self.remotename = ''
|
||||
self.local_branch = ''
|
||||
self.remote_branch = ''
|
||||
|
||||
#####################################################
|
||||
# Status info
|
||||
self.commitmsg = ''
|
||||
self.modified = []
|
||||
self.staged = []
|
||||
self.untracked = []
|
||||
self.unmerged = []
|
||||
self.upstream_changed = []
|
||||
self.submodules = set()
|
||||
|
||||
#####################################################
|
||||
# Refs
|
||||
self.revision = ''
|
||||
self.local_branches = []
|
||||
self.remote_branches = []
|
||||
self.tags = []
|
||||
self.revisions = []
|
||||
self.summaries = []
|
||||
|
||||
self.fetch_helper = None
|
||||
self.push_helper = None
|
||||
self.pull_helper = None
|
||||
self.generate_remote_helpers()
|
||||
if cwd:
|
||||
self.use_worktree(cwd)
|
||||
|
||||
#####################################################
|
||||
# Dag
|
||||
self._commits = []
|
||||
|
||||
def read_only(self):
|
||||
return self.mode in self.modes_read_only
|
||||
|
||||
def undoable(self):
|
||||
"""Whether we can checkout files from the $head."""
|
||||
return self.mode in self.modes_undoable
|
||||
|
||||
def enable_staging(self):
|
||||
"""Whether staging should be allowed."""
|
||||
return self.mode == self.mode_worktree
|
||||
|
||||
def generate_remote_helpers(self):
|
||||
"""Generates helper methods for fetch, push and pull"""
|
||||
self.push_helper = self.gen_remote_helper(self.git.push, push=True)
|
||||
self.fetch_helper = self.gen_remote_helper(self.git.fetch)
|
||||
self.pull_helper = self.gen_remote_helper(self.git.pull)
|
||||
|
||||
def use_worktree(self, worktree):
|
||||
self.git.load_worktree(worktree)
|
||||
is_valid = self.git.is_valid()
|
||||
if is_valid:
|
||||
basename = os.path.basename(self.git.worktree())
|
||||
self.set_project(core.decode(basename))
|
||||
return is_valid
|
||||
|
||||
def editor(self):
|
||||
app = _config.get('gui.editor', 'gvim')
|
||||
return {'vim': 'gvim'}.get(app, app)
|
||||
|
||||
def history_browser(self):
|
||||
return _config.get('gui.historybrowser', 'gitk')
|
||||
|
||||
def all_branches(self):
|
||||
return (self.local_branches + self.remote_branches)
|
||||
|
||||
def set_commitmsg(self, msg):
|
||||
self.commitmsg = msg
|
||||
self.notify_message_observers(self.message_commit_message_changed, msg)
|
||||
|
||||
def set_diff_text(self, txt):
|
||||
self.diff_text = txt
|
||||
self.notify_message_observers(self.message_diff_text_changed, txt)
|
||||
|
||||
def set_filename(self, filename):
|
||||
self.filename = filename
|
||||
self.notify_message_observers(self.message_filename_changed, filename)
|
||||
|
||||
def set_head(self, head):
|
||||
self.head = head
|
||||
self.notify_message_observers(self.message_head_changed, head)
|
||||
|
||||
def set_mode(self, mode):
|
||||
self.mode = mode
|
||||
self.notify_message_observers(self.message_mode_changed, mode)
|
||||
|
||||
def set_remote(self, remote):
|
||||
if not remote:
|
||||
return
|
||||
self.set_param('remote', remote)
|
||||
branches = utils.grep('%s/\S+$' % remote,
|
||||
gitcmds.branch_list(remote=True),
|
||||
squash=False)
|
||||
self.set_remote_branches(branches)
|
||||
|
||||
def apply_diff(self, filename):
|
||||
return self.git.apply(filename, index=True, cached=True,
|
||||
with_stderr=True, with_status=True)
|
||||
|
||||
def apply_diff_to_worktree(self, filename):
|
||||
return self.git.apply(filename,
|
||||
with_stderr=True, with_status=True)
|
||||
|
||||
def prev_commitmsg(self, *args):
|
||||
"""Queries git for the latest commit message."""
|
||||
log = self.git.log('-1', no_color=True, pretty='format:%s%n%n%b', *args)
|
||||
return core.decode(log)
|
||||
|
||||
def update_file_status(self, update_index=False):
|
||||
self.notify_message_observers(self.message_about_to_update)
|
||||
self.notification_enabled = False
|
||||
self._update_files(update_index=update_index)
|
||||
self.notification_enabled = True
|
||||
self.notify_observers('staged', 'unstaged')
|
||||
self.broadcast_updated()
|
||||
|
||||
def update_status(self, update_index=False):
|
||||
# Give observers a chance to respond
|
||||
self.notify_message_observers(self.message_about_to_update)
|
||||
# This allows us to defer notification until the
|
||||
# we finish processing data
|
||||
self.notification_enabled = False
|
||||
|
||||
self._update_files(update_index=update_index)
|
||||
self._update_refs()
|
||||
self._update_branches_and_tags()
|
||||
self._update_branch_heads()
|
||||
|
||||
# Re-enable notifications and emit changes
|
||||
self.notification_enabled = True
|
||||
self.notify_observers('staged', 'unstaged')
|
||||
self.broadcast_updated()
|
||||
|
||||
def update_status_of_files(self, files):
|
||||
self.notify_message_observers(self.message_about_to_update)
|
||||
self.notification_enabled = False
|
||||
|
||||
states = gitcmds.partial_worktree_state_dict(files, self.head)
|
||||
for status, path in states:
|
||||
if status == 'unmodified':
|
||||
if path in self.modified:
|
||||
self.modified.remove(path)
|
||||
if path in self.untracked:
|
||||
self.untracked.remove(path)
|
||||
elif status == 'modified':
|
||||
if path in self.untracked:
|
||||
self.untracked.remove(path)
|
||||
if path not in self.modified:
|
||||
self.modified.append(path)
|
||||
elif status == 'untracked':
|
||||
if path in self.modified:
|
||||
self.modified.remove(path)
|
||||
if path not in self.untracked:
|
||||
self.untracked.append(path)
|
||||
|
||||
self.modified.sort()
|
||||
self.untracked.sort()
|
||||
|
||||
self.notification_enabled = True
|
||||
self.notify_observers('staged', 'unstaged')
|
||||
self.broadcast_updated()
|
||||
|
||||
def broadcast_updated(self):
|
||||
self.notify_message_observers(self.message_updated)
|
||||
|
||||
def _update_files(self, worktree_only=False, update_index=False):
|
||||
staged_only = self.read_only()
|
||||
state = gitcmds.worktree_state_dict(head=self.head,
|
||||
update_index=update_index,
|
||||
staged_only=staged_only)
|
||||
self.staged = state.get('staged', [])
|
||||
self.modified = state.get('modified', [])
|
||||
self.unmerged = state.get('unmerged', [])
|
||||
self.untracked = state.get('untracked', [])
|
||||
self.submodules = state.get('submodules', set())
|
||||
self.upstream_changed = state.get('upstream_changed', [])
|
||||
|
||||
def _update_refs(self):
|
||||
self.set_remotes(self.git.remote().splitlines())
|
||||
self.set_revision('')
|
||||
self.set_local_branch('')
|
||||
self.set_remote_branch('')
|
||||
|
||||
|
||||
def _update_branch_heads(self):
|
||||
# Set these early since they are used to calculate 'upstream_changed'.
|
||||
self.set_trackedbranch(gitcmds.tracked_branch())
|
||||
self.set_currentbranch(gitcmds.current_branch())
|
||||
|
||||
def _update_branches_and_tags(self):
|
||||
local_branches, remote_branches, tags = gitcmds.all_refs(split=True)
|
||||
self.set_local_branches(local_branches)
|
||||
self.set_remote_branches(remote_branches)
|
||||
self.set_tags(tags)
|
||||
|
||||
def delete_branch(self, branch):
|
||||
return self.git.branch(branch,
|
||||
D=True,
|
||||
with_stderr=True,
|
||||
with_status=True)
|
||||
|
||||
def revision_sha1(self, idx):
|
||||
return self.revisions[idx]
|
||||
|
||||
def _sliced_op(self, input_items, map_fn, size=42):
|
||||
items = copy.copy(input_items)
|
||||
full_status = 0
|
||||
full_output = []
|
||||
while len(items) > 0:
|
||||
status, output = map_fn(items[:size])
|
||||
full_status = full_status or status
|
||||
full_output.append(output)
|
||||
items = items[size:]
|
||||
return (full_status, '\n'.join(full_output))
|
||||
|
||||
def _sliced_add(self, input_items, size=42):
|
||||
lambda_fn = lambda x: self.git.add('--',
|
||||
v=True,
|
||||
with_stderr=True,
|
||||
with_status=True,
|
||||
*x)
|
||||
return self._sliced_op(input_items, lambda_fn)
|
||||
|
||||
def stage_modified(self):
|
||||
status, output = self._sliced_add(self.modified)
|
||||
self.update_file_status()
|
||||
return (status, output)
|
||||
|
||||
def stage_untracked(self):
|
||||
status, output = self._sliced_add(self.untracked)
|
||||
self.update_file_status()
|
||||
return (status, output)
|
||||
|
||||
def reset(self, *items):
|
||||
lambda_fn = lambda x: self.git.reset('--',
|
||||
with_stderr=True,
|
||||
with_status=True,
|
||||
*x)
|
||||
status, output = self._sliced_op(items, lambda_fn)
|
||||
self.update_file_status()
|
||||
return (status, output)
|
||||
|
||||
def unstage_all(self):
|
||||
status, output = self.git.reset(self.head, '--', '.',
|
||||
with_stderr=True,
|
||||
with_status=True)
|
||||
self.update_file_status()
|
||||
return (status, output)
|
||||
|
||||
def stage_all(self):
|
||||
status, output = self.git.add(v=True,
|
||||
u=True,
|
||||
with_stderr=True,
|
||||
with_status=True)
|
||||
self.update_file_status()
|
||||
return (status, output)
|
||||
|
||||
def config_set(self, key, value, local=True):
|
||||
# git config category.key value
|
||||
strval = unicode(value)
|
||||
if type(value) is bool:
|
||||
# git uses "true" and "false"
|
||||
strval = strval.lower()
|
||||
if local:
|
||||
argv = [key, strval]
|
||||
else:
|
||||
argv = ['--global', key, strval]
|
||||
return self.git.config(*argv)
|
||||
|
||||
def config_dict(self, local=True):
|
||||
"""parses the lines from git config --list into a dictionary"""
|
||||
|
||||
kwargs = {
|
||||
'list': True,
|
||||
'global': not local, # global is a python keyword
|
||||
}
|
||||
config_lines = self.git.config(**kwargs).splitlines()
|
||||
newdict = {}
|
||||
for line in config_lines:
|
||||
try:
|
||||
k, v = line.split('=', 1)
|
||||
except:
|
||||
# value-less entry in .gitconfig
|
||||
continue
|
||||
v = core.decode(v)
|
||||
k = k.replace('.','_') # git -> model
|
||||
if v == 'true' or v == 'false':
|
||||
v = bool(eval(v.title()))
|
||||
try:
|
||||
v = int(eval(v))
|
||||
except:
|
||||
pass
|
||||
newdict[k]=v
|
||||
return newdict
|
||||
|
||||
def commit_with_msg(self, msg, amend=False):
|
||||
"""Creates a git commit."""
|
||||
|
||||
if not msg.endswith('\n'):
|
||||
msg += '\n'
|
||||
# Sure, this is a potential "security risk," but if someone
|
||||
# is trying to intercept/re-write commit messages on your system,
|
||||
# then you probably have bigger problems to worry about.
|
||||
tmpfile = self.tmp_filename()
|
||||
|
||||
# Create the commit message file
|
||||
fh = open(tmpfile, 'w')
|
||||
core.write_nointr(fh, msg)
|
||||
fh.close()
|
||||
|
||||
# Run 'git commit'
|
||||
status, out = self.git.commit(F=tmpfile, v=True, amend=amend,
|
||||
with_status=True,
|
||||
with_stderr=True)
|
||||
os.unlink(tmpfile)
|
||||
return (status, out)
|
||||
|
||||
def tmp_dir(self):
|
||||
# Allow TMPDIR/TMP with a fallback to /tmp
|
||||
return os.environ.get('TMP', os.environ.get('TMPDIR', '/tmp'))
|
||||
|
||||
def tmp_file_pattern(self):
|
||||
return os.path.join(self.tmp_dir(), '*.git-cola.%s.*' % os.getpid())
|
||||
|
||||
def tmp_filename(self, prefix=''):
|
||||
basename = ((prefix+'.git-cola.%s.%s'
|
||||
% (os.getpid(), time.time())))
|
||||
basename = basename.replace('/', '-')
|
||||
basename = basename.replace('\\', '-')
|
||||
tmpdir = self.tmp_dir()
|
||||
return os.path.join(tmpdir, basename)
|
||||
|
||||
def remote_url(self, name, action):
|
||||
if action == 'push':
|
||||
url = self.git.config('remote.%s.pushurl' % name, get=True)
|
||||
if url:
|
||||
return url
|
||||
return self.git.config('remote.%s.url' % name, get=True)
|
||||
|
||||
def remote_args(self, remote,
|
||||
local_branch='',
|
||||
remote_branch='',
|
||||
ffwd=True,
|
||||
tags=False,
|
||||
rebase=False,
|
||||
push=False):
|
||||
# Swap the branches in push mode (reverse of fetch)
|
||||
if push:
|
||||
tmp = local_branch
|
||||
local_branch = remote_branch
|
||||
remote_branch = tmp
|
||||
if ffwd:
|
||||
branch_arg = '%s:%s' % ( remote_branch, local_branch )
|
||||
else:
|
||||
branch_arg = '+%s:%s' % ( remote_branch, local_branch )
|
||||
args = [remote]
|
||||
if local_branch and remote_branch:
|
||||
args.append(branch_arg)
|
||||
elif local_branch:
|
||||
args.append(local_branch)
|
||||
elif remote_branch:
|
||||
args.append(remote_branch)
|
||||
kwargs = {
|
||||
'verbose': True,
|
||||
'tags': tags,
|
||||
'rebase': rebase,
|
||||
'with_stderr': True,
|
||||
'with_status': True,
|
||||
}
|
||||
return (args, kwargs)
|
||||
|
||||
def gen_remote_helper(self, gitaction, push=False):
|
||||
"""Generates a closure that calls git fetch, push or pull
|
||||
"""
|
||||
def remote_helper(remote, **kwargs):
|
||||
args, kwargs = self.remote_args(remote, push=push, **kwargs)
|
||||
return gitaction(*args, **kwargs)
|
||||
return remote_helper
|
||||
|
||||
def create_branch(self, name, base, track=False):
|
||||
"""Create a branch named 'name' from revision 'base'
|
||||
|
||||
Pass track=True to create a local tracking branch.
|
||||
"""
|
||||
return self.git.branch(name, base, track=track,
|
||||
with_stderr=True,
|
||||
with_status=True)
|
||||
|
||||
def cherry_pick_list(self, revs, **kwargs):
|
||||
"""Cherry-picks each revision into the current branch.
|
||||
Returns a list of command output strings (1 per cherry pick)"""
|
||||
if not revs:
|
||||
return []
|
||||
cherries = []
|
||||
status = 0
|
||||
for rev in revs:
|
||||
newstatus, out = self.git.cherry_pick(rev,
|
||||
with_stderr=True,
|
||||
with_status=True)
|
||||
if status == 0:
|
||||
status += newstatus
|
||||
cherries.append(out)
|
||||
return (status, '\n'.join(cherries))
|
||||
|
||||
def pad(self, pstr, num=22):
|
||||
topad = num-len(pstr)
|
||||
if topad > 0:
|
||||
return pstr + ' '*topad
|
||||
else:
|
||||
return pstr
|
||||
|
||||
def is_commit_published(self):
|
||||
head = self.git.rev_parse('HEAD')
|
||||
return bool(self.git.branch(r=True, contains=head))
|
||||
|
||||
def everything(self):
|
||||
"""Returns a sorted list of all files, including untracked files."""
|
||||
ls_files = self.git.ls_files(z=True,
|
||||
cached=True,
|
||||
others=True,
|
||||
exclude_standard=True)
|
||||
return sorted(map(core.decode, [f for f in ls_files.split('\0') if f]))
|
||||
|
||||
def stage_paths(self, paths):
|
||||
"""Stages add/removals to git."""
|
||||
if not paths:
|
||||
self.stage_all()
|
||||
return
|
||||
|
||||
add = []
|
||||
remove = []
|
||||
|
||||
for path in set(paths):
|
||||
if os.path.exists(core.encode(path)):
|
||||
add.append(path)
|
||||
else:
|
||||
remove.append(path)
|
||||
|
||||
self.notify_message_observers(self.message_about_to_update)
|
||||
|
||||
# `git add -u` doesn't work on untracked files
|
||||
if add:
|
||||
self._sliced_add(add)
|
||||
|
||||
# If a path doesn't exist then that means it should be removed
|
||||
# from the index. We use `git add -u` for that.
|
||||
if remove:
|
||||
while remove:
|
||||
self.git.add('--', u=True, with_stderr=True, *remove[:42])
|
||||
remove = remove[42:]
|
||||
|
||||
self._update_files()
|
||||
self.notify_message_observers(self.message_updated)
|
||||
|
||||
def unstage_paths(self, paths):
|
||||
if not paths:
|
||||
self.unstage_all()
|
||||
return
|
||||
gitcmds.unstage_paths(paths, head=self.head)
|
||||
self.update_file_status()
|
||||
|
||||
def getcwd(self):
|
||||
"""If we've chosen a directory then use it, otherwise os.getcwd()."""
|
||||
if self.directory:
|
||||
return self.directory
|
||||
return os.getcwd()
|
||||
|
||||
serializer.handlers[MainModel] = MainSerializer
|
||||
@@ -0,0 +1,556 @@
|
||||
"""This view provides the main git-cola user interface.
|
||||
"""
|
||||
import os
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.QtCore import Qt
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
import cola
|
||||
from cola import core
|
||||
from cola import gitcmds
|
||||
from cola import guicmds
|
||||
from cola import merge
|
||||
from cola import settings
|
||||
from cola import signals
|
||||
from cola import gitcfg
|
||||
from cola import qtutils
|
||||
from cola import qtcompat
|
||||
from cola import qt
|
||||
from cola import resources
|
||||
from cola import stash
|
||||
from cola import utils
|
||||
from cola import version
|
||||
from cola.classic import cola_classic
|
||||
from cola.classic import classic_widget
|
||||
from cola.controllers import compare
|
||||
from cola.controllers import createtag
|
||||
from cola.controllers import search
|
||||
from cola.controllers.bookmark import manage_bookmarks
|
||||
from cola.controllers.bookmark import save_bookmark
|
||||
from cola.controllers.createbranch import create_new_branch
|
||||
from cola.dag import git_dag
|
||||
from cola.prefs import diff_font
|
||||
from cola.prefs import PreferencesModel
|
||||
from cola.prefs import preferences
|
||||
from cola.qt import create_button
|
||||
from cola.qt import create_dock
|
||||
from cola.qt import create_menu
|
||||
from cola.qtutils import add_action
|
||||
from cola.qtutils import connect_button
|
||||
from cola.qtutils import emit
|
||||
from cola.qtutils import log
|
||||
from cola.qtutils import relay_signal
|
||||
from cola.qtutils import SLOT
|
||||
from cola.qtutils import tr
|
||||
from cola.views import standard
|
||||
from cola.widgets import cfgactions
|
||||
from cola.widgets.about import launch_about_dialog
|
||||
from cola.widgets.commitmsg import CommitMessageEditor
|
||||
from cola.widgets.diff import DiffTextEdit
|
||||
from cola.widgets.status import StatusWidget
|
||||
|
||||
|
||||
class MainView(standard.MainWindow):
|
||||
def __init__(self, model, parent=None,
|
||||
add_action=add_action,
|
||||
connect_button=connect_button,
|
||||
create_button=create_button,
|
||||
create_dock=create_dock,
|
||||
create_menu=create_menu,
|
||||
emit=emit,
|
||||
relay_signal=relay_signal,
|
||||
SLOT=SLOT,
|
||||
tr=tr):
|
||||
standard.MainWindow.__init__(self, parent)
|
||||
# Default size; this is thrown out when save/restore is used
|
||||
self.resize(987, 610)
|
||||
self.model = model
|
||||
self.prefs_model = prefs_model = PreferencesModel()
|
||||
|
||||
# Internal field used by import/export_state().
|
||||
# Change this whenever dockwidgets are removed.
|
||||
self.widget_version = 1
|
||||
|
||||
# Keeps track of merge messages we've seen
|
||||
self.merge_message_hash = ''
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
# Dockwidget options
|
||||
qtcompat.set_common_dock_options(self)
|
||||
|
||||
self.classic_dockable = gitcfg.instance().get('cola.classicdockable')
|
||||
|
||||
if self.classic_dockable:
|
||||
self.classicdockwidget = create_dock('Cola Classic', self)
|
||||
self.classicwidget = classic_widget(parent=self)
|
||||
self.classicdockwidget.setWidget(self.classicwidget)
|
||||
|
||||
# "Actions" widget
|
||||
self.actiondockwidget = create_dock('Actions', self)
|
||||
self.actiondockwidgetcontents = qt.QFlowLayoutWidget(parent=self)
|
||||
layout = self.actiondockwidgetcontents.layout()
|
||||
self.stage_button = create_button('Stage', layout)
|
||||
self.unstage_button = create_button('Unstage', layout)
|
||||
self.rescan_button = create_button('Rescan', layout)
|
||||
self.fetch_button = create_button('Fetch...', layout)
|
||||
self.push_button = create_button('Push...', layout)
|
||||
self.pull_button = create_button('Pull...', layout)
|
||||
self.stash_button = create_button('Stash...', layout)
|
||||
self.alt_button = create_button('Exit Diff Mode', layout)
|
||||
self.alt_button.hide()
|
||||
layout.addStretch()
|
||||
self.actiondockwidget.setWidget(self.actiondockwidgetcontents)
|
||||
|
||||
# "Repository Status" widget
|
||||
self.statusdockwidget = create_dock('Repository Status', self)
|
||||
self.statusdockwidget.setWidget(StatusWidget(self))
|
||||
|
||||
# "Commit Message Editor" widget
|
||||
self.commitdockwidget = create_dock('Commit Message Editor', self)
|
||||
self.commitmsgeditor = CommitMessageEditor(model, self)
|
||||
relay_signal(self, self.commitmsgeditor, SIGNAL(signals.amend_mode))
|
||||
relay_signal(self, self.commitmsgeditor, SIGNAL(signals.signoff))
|
||||
relay_signal(self, self.commitmsgeditor,
|
||||
SIGNAL(signals.load_previous_message))
|
||||
self.commitdockwidget.setWidget(self.commitmsgeditor)
|
||||
|
||||
# "Command Output" widget
|
||||
logwidget = qtutils.logger()
|
||||
logwidget.setFont(diff_font())
|
||||
self.logdockwidget = create_dock('Command Output', self)
|
||||
self.logdockwidget.setWidget(logwidget)
|
||||
|
||||
# "Diff Viewer" widget
|
||||
self.diffdockwidget = create_dock('Diff Viewer', self)
|
||||
self.diff_viewer = DiffTextEdit(self.diffdockwidget)
|
||||
self.diffdockwidget.setWidget(self.diff_viewer)
|
||||
|
||||
# All Actions
|
||||
self.menu_unstage_selected = add_action(self,
|
||||
'Unstage From Commit', emit(self, signals.unstage_selected))
|
||||
self.menu_show_diffstat = add_action(self,
|
||||
'Diffstat', emit(self, signals.diffstat), 'Ctrl+D')
|
||||
self.menu_stage_modified = add_action(self,
|
||||
'Stage Changed Files To Commit',
|
||||
emit(self, signals.stage_modified), 'Alt+A')
|
||||
self.menu_stage_untracked = add_action(self,
|
||||
'Stage All Untracked', emit(self, signals.stage_untracked), 'Alt+U')
|
||||
self.menu_export_patches = add_action(self,
|
||||
'Export Patches...', guicmds.export_patches, 'Ctrl+E')
|
||||
self.menu_preferences = add_action(self,
|
||||
'Preferences', lambda: preferences(model=prefs_model),
|
||||
QtGui.QKeySequence.Preferences, 'Ctrl+O')
|
||||
self.menu_rescan = add_action(self,
|
||||
'Rescan', emit(self, signals.rescan), 'Ctrl+R')
|
||||
self.menu_cherry_pick = add_action(self,
|
||||
'Cherry-Pick...', guicmds.cherry_pick, 'Ctrl+P')
|
||||
self.menu_unstage_all = add_action(self,
|
||||
'Unstage All', emit(self, signals.unstage_all))
|
||||
self.menu_load_commitmsg = add_action(self,
|
||||
'Load Commit Message...', guicmds.load_commitmsg_slot(self))
|
||||
self.menu_quit = add_action(self,
|
||||
'Quit', self.close, 'Ctrl+Q')
|
||||
self.menu_manage_bookmarks = add_action(self,
|
||||
'Bookmarks...', manage_bookmarks)
|
||||
self.menu_save_bookmark = add_action(self,
|
||||
'Bookmark Current...', save_bookmark)
|
||||
self.menu_grep = add_action(self,
|
||||
'Grep', guicmds.grep)
|
||||
self.menu_merge_local = add_action(self,
|
||||
'Merge...', merge.local_merge)
|
||||
self.menu_merge_abort = add_action(self,
|
||||
'Abort Merge...', merge.abort_merge)
|
||||
self.menu_fetch = add_action(self,
|
||||
'Fetch...', guicmds.fetch_slot(self))
|
||||
self.menu_push = add_action(self,
|
||||
'Push...', guicmds.push_slot(self))
|
||||
self.menu_pull = add_action(self,
|
||||
'Pull...', guicmds.pull_slot(self))
|
||||
self.menu_open_repo = add_action(self,
|
||||
'Open...', guicmds.open_repo_slot(self))
|
||||
self.menu_stash = add_action(self,
|
||||
'Stash...', lambda: stash.stash(parent=self), 'Alt+Shift+S')
|
||||
self.menu_diff_branch = add_action(self,
|
||||
'Apply Changes From Branch...', guicmds.diff_branch)
|
||||
self.menu_branch_compare = add_action(self,
|
||||
'Branches...', compare.branch_compare)
|
||||
self.menu_clone_repo = add_action(self,
|
||||
'Clone...', guicmds.clone_repo)
|
||||
self.menu_help_docs = add_action(self,
|
||||
'Documentation',
|
||||
lambda: self.model.git.web__browse(resources.html_docs()),
|
||||
QtGui.QKeySequence.HelpContents)
|
||||
self.menu_commit_compare = add_action(self,
|
||||
'Commits...', compare.compare)
|
||||
self.menu_commit_compare_file = add_action(self,
|
||||
'Commits Touching File...', compare.compare_file)
|
||||
self.menu_visualize_current = add_action(self,
|
||||
'Visualize Current Branch...',
|
||||
emit(self, signals.visualize_current))
|
||||
self.menu_visualize_all = add_action(self,
|
||||
'Visualize All Branches...',
|
||||
emit(self, signals.visualize_all))
|
||||
self.menu_search_commits = add_action(self,
|
||||
'Search...', search.search)
|
||||
self.menu_browse_branch = add_action(self,
|
||||
'Browse Current Branch...', guicmds.browse_current)
|
||||
self.menu_browse_other_branch = add_action(self,
|
||||
'Browse Other Branch...', guicmds.browse_other)
|
||||
self.menu_load_commitmsg_template = add_action(self,
|
||||
'Get Commit Message Template',
|
||||
emit(self, signals.load_commit_template))
|
||||
self.menu_help_about = add_action(self,
|
||||
'About', launch_about_dialog)
|
||||
self.menu_branch_diff = add_action(self,
|
||||
'SHA-1...', guicmds.branch_diff)
|
||||
self.menu_diff_expression = add_action(self,
|
||||
'Expression...', guicmds.diff_expression)
|
||||
self.menu_create_tag = add_action(self,
|
||||
'Create Tag...', createtag.create_tag)
|
||||
self.menu_create_branch = add_action(self,
|
||||
'Create...', create_new_branch, 'Ctrl+B')
|
||||
self.menu_delete_branch = add_action(self,
|
||||
'Delete...', guicmds.branch_delete)
|
||||
self.menu_checkout_branch = add_action(self,
|
||||
'Checkout...', guicmds.checkout_branch, 'Alt+B')
|
||||
self.menu_rebase_branch = add_action(self,
|
||||
'Rebase...', guicmds.rebase)
|
||||
self.menu_branch_review = add_action(self,
|
||||
'Review...', guicmds.review_branch)
|
||||
self.menu_classic = add_action(self,
|
||||
'Cola Classic...', cola_classic)
|
||||
self.menu_dag = add_action(self,
|
||||
'DAG...', lambda: git_dag(self.model, self))
|
||||
|
||||
# Create the application menu
|
||||
self.menubar = QtGui.QMenuBar(self)
|
||||
|
||||
# File Menu
|
||||
self.file_menu = create_menu('&File', self.menubar)
|
||||
self.file_menu.addAction(self.menu_preferences)
|
||||
self.file_menu.addSeparator()
|
||||
self.file_menu.addAction(self.menu_open_repo)
|
||||
self.file_menu.addAction(self.menu_clone_repo)
|
||||
self.file_menu.addSeparator()
|
||||
self.file_menu.addAction(self.menu_rescan)
|
||||
self.file_menu.addSeparator()
|
||||
self.file_menu.addAction(self.menu_manage_bookmarks)
|
||||
self.file_menu.addAction(self.menu_save_bookmark)
|
||||
self.file_menu.addSeparator()
|
||||
self.file_menu.addAction(self.menu_load_commitmsg)
|
||||
self.file_menu.addAction(self.menu_load_commitmsg_template)
|
||||
self.file_menu.addSeparator()
|
||||
self.file_menu.addAction(self.menu_quit)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.file_menu.menuAction())
|
||||
|
||||
# Commit Menu
|
||||
self.commit_menu = create_menu('Co&mmit', self.menubar)
|
||||
self.commit_menu.setTitle(tr('Commit@@verb'))
|
||||
self.commit_menu.addAction(self.menu_stage_modified)
|
||||
self.commit_menu.addAction(self.menu_stage_untracked)
|
||||
self.commit_menu.addSeparator()
|
||||
self.commit_menu.addAction(self.menu_unstage_all)
|
||||
self.commit_menu.addAction(self.menu_unstage_selected)
|
||||
self.commit_menu.addSeparator()
|
||||
self.commit_menu.addAction(self.menu_search_commits)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.commit_menu.menuAction())
|
||||
|
||||
# Branch Menu
|
||||
self.branch_menu = create_menu('B&ranch', self.menubar)
|
||||
self.branch_menu.addAction(self.menu_branch_review)
|
||||
self.branch_menu.addSeparator()
|
||||
self.branch_menu.addAction(self.menu_create_branch)
|
||||
self.branch_menu.addAction(self.menu_checkout_branch)
|
||||
self.branch_menu.addAction(self.menu_rebase_branch)
|
||||
self.branch_menu.addAction(self.menu_delete_branch)
|
||||
self.branch_menu.addSeparator()
|
||||
self.branch_menu.addAction(self.menu_browse_branch)
|
||||
self.branch_menu.addAction(self.menu_browse_other_branch)
|
||||
self.branch_menu.addSeparator()
|
||||
self.branch_menu.addAction(self.menu_visualize_current)
|
||||
self.branch_menu.addAction(self.menu_visualize_all)
|
||||
self.branch_menu.addSeparator()
|
||||
self.branch_menu.addAction(self.menu_diff_branch)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.branch_menu.menuAction())
|
||||
|
||||
# Actions menu
|
||||
self.actions_menu = create_menu('Act&ions', self.menubar)
|
||||
self.actions_menu.addAction(self.menu_merge_local)
|
||||
self.actions_menu.addAction(self.menu_stash)
|
||||
self.actions_menu.addSeparator()
|
||||
self.actions_menu.addAction(self.menu_fetch)
|
||||
self.actions_menu.addAction(self.menu_push)
|
||||
self.actions_menu.addAction(self.menu_pull)
|
||||
self.actions_menu.addSeparator()
|
||||
self.actions_menu.addAction(self.menu_create_tag)
|
||||
self.actions_menu.addSeparator()
|
||||
self.actions_menu.addAction(self.menu_export_patches)
|
||||
self.actions_menu.addAction(self.menu_cherry_pick)
|
||||
self.actions_menu.addSeparator()
|
||||
self.actions_menu.addAction(self.menu_merge_abort)
|
||||
self.actions_menu.addAction(self.menu_grep)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.actions_menu.menuAction())
|
||||
|
||||
# Diff Menu
|
||||
self.diff_menu = create_menu('&Diff', self.menubar)
|
||||
self.diff_menu.addAction(self.menu_branch_diff)
|
||||
self.diff_menu.addAction(self.menu_diff_expression)
|
||||
self.diff_menu.addSeparator()
|
||||
self.diff_menu.addAction(self.menu_branch_compare)
|
||||
self.diff_menu.addAction(self.menu_commit_compare)
|
||||
self.diff_menu.addAction(self.menu_commit_compare_file)
|
||||
self.diff_menu.addSeparator()
|
||||
self.diff_menu.addAction(self.menu_show_diffstat)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.diff_menu.menuAction())
|
||||
|
||||
# Tools Menu
|
||||
self.tools_menu = create_menu('&Tools', self.menubar)
|
||||
self.tools_menu.addAction(self.menu_classic)
|
||||
self.tools_menu.addAction(self.menu_dag)
|
||||
self.tools_menu.addSeparator()
|
||||
if self.classic_dockable:
|
||||
self.tools_menu.addAction(self.classicdockwidget.toggleViewAction())
|
||||
self.tools_menu.addAction(self.diffdockwidget.toggleViewAction())
|
||||
self.tools_menu.addAction(self.actiondockwidget.toggleViewAction())
|
||||
self.tools_menu.addAction(self.commitdockwidget.toggleViewAction())
|
||||
self.tools_menu.addAction(self.statusdockwidget.toggleViewAction())
|
||||
self.tools_menu.addAction(self.logdockwidget.toggleViewAction())
|
||||
self.menubar.addAction(self.tools_menu.menuAction())
|
||||
|
||||
# Help Menu
|
||||
self.help_menu = create_menu('&Help', self.menubar)
|
||||
self.help_menu.addAction(self.menu_help_docs)
|
||||
self.help_menu.addAction(self.menu_help_about)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.help_menu.menuAction())
|
||||
|
||||
# Set main menu
|
||||
self.setMenuBar(self.menubar)
|
||||
|
||||
# Arrange dock widgets
|
||||
top = Qt.TopDockWidgetArea
|
||||
bottom = Qt.BottomDockWidgetArea
|
||||
|
||||
self.addDockWidget(top, self.commitdockwidget)
|
||||
if self.classic_dockable:
|
||||
self.addDockWidget(top, self.classicdockwidget)
|
||||
self.addDockWidget(top, self.statusdockwidget)
|
||||
self.addDockWidget(top, self.actiondockwidget)
|
||||
self.addDockWidget(bottom, self.logdockwidget)
|
||||
if self.classic_dockable:
|
||||
self.tabifyDockWidget(self.classicdockwidget, self.commitdockwidget)
|
||||
self.tabifyDockWidget(self.logdockwidget, self.diffdockwidget)
|
||||
|
||||
# Listen for model notifications
|
||||
model.add_message_observer(model.message_mode_changed,
|
||||
self._mode_changed)
|
||||
|
||||
model.add_message_observer(model.message_updated,
|
||||
self._update_view)
|
||||
|
||||
prefs_model.add_message_observer(prefs_model.message_config_updated,
|
||||
self._config_updated)
|
||||
|
||||
|
||||
# Add button callbacks
|
||||
connect_button(self.rescan_button, emit(self, signals.rescan))
|
||||
connect_button(self.alt_button, emit(self, signals.reset_mode))
|
||||
connect_button(self.fetch_button, guicmds.fetch_slot(self))
|
||||
connect_button(self.push_button, guicmds.push_slot(self))
|
||||
connect_button(self.pull_button, guicmds.pull_slot(self))
|
||||
connect_button(self.stash_button, lambda: stash.stash(parent=self))
|
||||
|
||||
connect_button(self.stage_button, self.stage)
|
||||
connect_button(self.unstage_button, self.unstage)
|
||||
|
||||
self.connect(self, SIGNAL('update'), self._update_callback)
|
||||
self.connect(self, SIGNAL('apply_state'), self.apply_state)
|
||||
self.connect(self, SIGNAL('install_config_actions'),
|
||||
self._install_config_actions)
|
||||
|
||||
# Install .git-config-defined actions
|
||||
self._config_task = None
|
||||
self.install_config_actions()
|
||||
|
||||
# Restore saved settings
|
||||
self._gui_state_task = None
|
||||
self._load_gui_state()
|
||||
|
||||
log(0, self.model.git_version + '\ncola version ' + version.version())
|
||||
|
||||
# Qt overrides
|
||||
def closeEvent(self, event):
|
||||
"""Save state in the settings manager."""
|
||||
qtutils.save_state(self)
|
||||
standard.MainWindow.closeEvent(self, event)
|
||||
|
||||
# Accessors
|
||||
mode = property(lambda self: self.model.mode)
|
||||
|
||||
def _mode_changed(self, mode):
|
||||
"""React to mode changes; hide/show the "Exit Diff Mode" button."""
|
||||
if mode in (self.model.mode_branch,
|
||||
self.model.mode_review,
|
||||
self.model.mode_diff,
|
||||
self.model.mode_diff_expr):
|
||||
height = self.stage_button.minimumHeight()
|
||||
self.alt_button.setMinimumHeight(height)
|
||||
self.alt_button.show()
|
||||
else:
|
||||
self.alt_button.setMinimumHeight(1)
|
||||
self.alt_button.hide()
|
||||
|
||||
def _config_updated(self, source, config, value):
|
||||
if config == 'cola.fontdiff':
|
||||
font = QtGui.QFont()
|
||||
if not font.fromString(value):
|
||||
return
|
||||
self._set_diff_font(font)
|
||||
qtutils.logger().setFont(font)
|
||||
self.diff_viewer.setFont(font)
|
||||
self.commitmsgeditor.commitmsg.setFont(font)
|
||||
|
||||
elif config == 'cola.tabwidth':
|
||||
# variable-tab-width setting
|
||||
self.diff_viewer.set_tab_width(value)
|
||||
|
||||
def install_config_actions(self):
|
||||
"""Install .gitconfig-defined actions"""
|
||||
self._config_task = self._start_config_actions_task()
|
||||
|
||||
def _start_config_actions_task(self):
|
||||
"""Do the expensive "get_config_actions()" call in the background"""
|
||||
class ConfigActionsTask(QtCore.QRunnable):
|
||||
def __init__(self, sender):
|
||||
QtCore.QRunnable.__init__(self)
|
||||
self._sender = sender
|
||||
def run(self):
|
||||
names = cfgactions.get_config_actions()
|
||||
self._sender.emit(SIGNAL('install_config_actions'), names)
|
||||
|
||||
task = ConfigActionsTask(self)
|
||||
QtCore.QThreadPool.globalInstance().start(task)
|
||||
return task
|
||||
|
||||
def _install_config_actions(self, names):
|
||||
"""Install .gitconfig-defined actions"""
|
||||
if not names:
|
||||
return
|
||||
menu = self.actions_menu
|
||||
menu.addSeparator()
|
||||
for name in names:
|
||||
menu.addAction(name, emit(self, signals.run_config_action, name))
|
||||
|
||||
def _update_view(self):
|
||||
self.emit(SIGNAL('update'))
|
||||
|
||||
def _update_callback(self):
|
||||
"""Update the title with the current branch and directory name."""
|
||||
branch = self.model.currentbranch
|
||||
curdir = core.decode(os.getcwd())
|
||||
msg = 'Repository: %s\nBranch: %s' % (curdir, branch)
|
||||
self.commitdockwidget.setToolTip(msg)
|
||||
|
||||
title = '%s: %s' % (self.model.project, branch)
|
||||
if self.mode in (self.model.mode_diff, self.model.mode_diff_expr):
|
||||
title += ' *** diff mode***'
|
||||
elif self.mode == self.model.mode_review:
|
||||
title += ' *** review mode***'
|
||||
elif self.mode == self.model.mode_amend:
|
||||
title += ' *** amending ***'
|
||||
self.setWindowTitle(title)
|
||||
|
||||
self.commitmsgeditor.set_mode(self.mode)
|
||||
|
||||
if not self.model.read_only() and self.mode != self.model.mode_amend:
|
||||
# Check if there's a message file in .git/
|
||||
merge_msg_path = gitcmds.merge_message_path()
|
||||
if merge_msg_path is None:
|
||||
return
|
||||
merge_msg_hash = utils.checksum(core.decode(merge_msg_path))
|
||||
if merge_msg_hash == self.merge_message_hash:
|
||||
return
|
||||
self.merge_message_hash = merge_msg_hash
|
||||
cola.notifier().broadcast(signals.load_commit_message,
|
||||
core.decode(merge_msg_path))
|
||||
|
||||
def apply_state(self, state):
|
||||
"""Imports data for save/restore"""
|
||||
# 1 is the widget version; change when widgets are added/removed
|
||||
standard.MainWindow.apply_state(self, state)
|
||||
qtutils.apply_window_state(self, state, 1)
|
||||
|
||||
def export_state(self):
|
||||
"""Exports data for save/restore"""
|
||||
state = standard.MainWindow.export_state(self)
|
||||
return qtutils.export_window_state(self, state, self.widget_version)
|
||||
|
||||
def _load_gui_state(self):
|
||||
"""Restores the gui from the preferences file."""
|
||||
self._gui_state_task = self._start_gui_state_loading_thread()
|
||||
|
||||
def _start_gui_state_loading_thread(self):
|
||||
"""Do expensive file reading and json decoding in the background"""
|
||||
class LoadGUIStateTask(QtCore.QRunnable):
|
||||
def __init__(self, sender):
|
||||
QtCore.QRunnable.__init__(self)
|
||||
self._sender = sender
|
||||
def run(self):
|
||||
state = settings.SettingsManager.gui_state(self._sender)
|
||||
self._sender.emit(SIGNAL('apply_state'), state)
|
||||
|
||||
task = LoadGUIStateTask(self)
|
||||
QtCore.QThreadPool.globalInstance().start(task)
|
||||
return task
|
||||
|
||||
def stage(self):
|
||||
"""Stage selected files, or all files if no selection exists."""
|
||||
paths = cola.selection_model().unstaged
|
||||
if not paths:
|
||||
cola.notifier().broadcast(signals.stage_modified)
|
||||
else:
|
||||
cola.notifier().broadcast(signals.stage, paths)
|
||||
|
||||
def unstage(self):
|
||||
"""Unstage selected files, or all files if no selection exists."""
|
||||
paths = cola.selection_model().staged
|
||||
if not paths:
|
||||
cola.notifier().broadcast(signals.unstage_all)
|
||||
else:
|
||||
cola.notifier().broadcast(signals.unstage, paths)
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
"""Accepts drops"""
|
||||
standard.MainWindow.dragEnterEvent(self, event)
|
||||
event.acceptProposedAction()
|
||||
|
||||
def dropEvent(self, event):
|
||||
"""Apply dropped patches with git-am"""
|
||||
event.accept()
|
||||
urls = event.mimeData().urls()
|
||||
if not urls:
|
||||
return
|
||||
paths = map(lambda x: unicode(x.path()), urls)
|
||||
patches = [p for p in paths if p.endswith('.patch')]
|
||||
dirs = [p for p in paths if os.path.isdir(p)]
|
||||
dirs.sort()
|
||||
for d in dirs:
|
||||
patches.extend(self._gather_patches(d))
|
||||
# Broadcast the patches to apply
|
||||
cola.notifier().broadcast(signals.apply_patches, patches)
|
||||
|
||||
def _gather_patches(self, path):
|
||||
"""Find patches in a subdirectory"""
|
||||
patches = []
|
||||
for root, subdirs, files in os.walk(path):
|
||||
for name in [f for f in files if f.endswith('.patch')]:
|
||||
patches.append(os.path.join(root, name))
|
||||
return patches
|
||||
@@ -0,0 +1,47 @@
|
||||
from PyQt4 import QtGui
|
||||
|
||||
from cola import gitcmds
|
||||
from cola import qtutils
|
||||
from cola.merge.controller import MergeController
|
||||
from cola.merge.model import MergeModel
|
||||
from cola.merge.view import MergeView
|
||||
|
||||
|
||||
def local_merge():
|
||||
"""Provides a dialog for merging branches"""
|
||||
model = MergeModel()
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
view = MergeView(model, parent)
|
||||
ctl = MergeController(model, view)
|
||||
view.show()
|
||||
view.raise_()
|
||||
return ctl
|
||||
|
||||
|
||||
def abort_merge():
|
||||
"""Prompts before aborting a merge in progress
|
||||
"""
|
||||
title = 'Abort Merge...'
|
||||
txt = ('Abort merge?\n\n'
|
||||
'Aborting the current merge will cause '
|
||||
'*ALL* uncommitted changes to be lost.\n\n'
|
||||
'Continue with aborting the current merge?')
|
||||
info = 'Recovering uncommitted changes will not be possible'
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
ok_text = qtutils.tr('Abort Merge...')
|
||||
if ok_text.endswith(unichr(0x2026)):
|
||||
ok_text = ok_text[:-1]
|
||||
elif ok_text.endswith('...'):
|
||||
ok_text = ok_text[:-3]
|
||||
answer = qtutils.confirm(parent, title, txt, info, ok_text)
|
||||
if answer:
|
||||
gitcmds.abort_merge()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from cola import app
|
||||
|
||||
app = app.ColaApplication([])
|
||||
ctl = local_merge()
|
||||
ctl.model.update_status()
|
||||
app.exec_()
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Translate signals from the merge dialog."""
|
||||
from cola.ctrl import Controller
|
||||
from cola.merge.model import command_directory
|
||||
|
||||
class MergeController(Controller):
|
||||
def __init__(self, model, view):
|
||||
Controller.__init__(self, model, view)
|
||||
self.add_commands(command_directory)
|
||||
@@ -0,0 +1,70 @@
|
||||
import cola
|
||||
from cola import gitcmds
|
||||
from cola import observable
|
||||
from cola import signals
|
||||
from cola.cmds import BaseCommand, VisualizeRevision, visualize_revision
|
||||
|
||||
|
||||
class MergeModel(observable.Observable):
|
||||
message_updated = 'updated'
|
||||
|
||||
def __init__(self):
|
||||
observable.Observable.__init__(self)
|
||||
self.model = cola.model()
|
||||
# Relay the "updated" message
|
||||
msg = self.model.message_updated
|
||||
self.model.add_message_observer(msg, self.notify_updated)
|
||||
|
||||
def notify_updated(self):
|
||||
self.notify_message_observers(self.message_updated)
|
||||
|
||||
def update_status(self):
|
||||
self.model.update_status()
|
||||
|
||||
def current_branch(self):
|
||||
return self.model.currentbranch
|
||||
|
||||
def local_branches(self):
|
||||
return self.model.local_branches
|
||||
|
||||
def remote_branches(self):
|
||||
return self.model.remote_branches
|
||||
|
||||
def tags(self):
|
||||
return self.model.tags
|
||||
|
||||
def merge(self, revision, no_commit, squash):
|
||||
msg = gitcmds.merge_message(revision)
|
||||
status, output = self.model.git.merge('-m'+msg,
|
||||
revision,
|
||||
no_commit=no_commit,
|
||||
squash=squash,
|
||||
with_stderr=True,
|
||||
with_status=True)
|
||||
return status, output
|
||||
|
||||
|
||||
# Merge command
|
||||
merge = 'merge'
|
||||
|
||||
class Merge(BaseCommand):
|
||||
def __init__(self, revision, no_commit, squash):
|
||||
BaseCommand.__init__(self)
|
||||
self.revision = revision
|
||||
self.no_commit = no_commit
|
||||
self.squash = squash
|
||||
|
||||
def do(self):
|
||||
squash = self.squash
|
||||
revision = self.revision
|
||||
no_commit = self.no_commit
|
||||
status, output = self.context.merge(revision, no_commit, squash)
|
||||
notifier = cola.notifier()
|
||||
notifier.broadcast(signals.log_cmd, status, output)
|
||||
self.context.update_status()
|
||||
|
||||
|
||||
command_directory = {
|
||||
merge: Merge,
|
||||
visualize_revision: VisualizeRevision,
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.QtCore import Qt
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola import qtutils
|
||||
from cola.qt import GitRefLineEdit
|
||||
from cola.qtutils import tr
|
||||
from cola.merge.model import merge, visualize_revision
|
||||
|
||||
|
||||
class MergeView(QtGui.QDialog):
|
||||
"""Provides a dialog for merging branches."""
|
||||
def __init__(self, model, parent=None):
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.model = model
|
||||
self.setWindowModality(Qt.WindowModal)
|
||||
self.resize(700, 400)
|
||||
|
||||
# Widgets
|
||||
self.title_label = QtGui.QLabel()
|
||||
self.revision_label = QtGui.QLabel()
|
||||
self.revision_label.setText(tr('Revision To Merge'))
|
||||
|
||||
self.revision = GitRefLineEdit()
|
||||
|
||||
self.radio_local = QtGui.QRadioButton()
|
||||
self.radio_local.setText(tr('Local Branch'))
|
||||
self.radio_local.setChecked(True)
|
||||
self.radio_remote = QtGui.QRadioButton()
|
||||
self.radio_remote.setText(tr('Tracking Branch'))
|
||||
self.radio_tag = QtGui.QRadioButton()
|
||||
self.radio_tag.setText(tr('Tag'))
|
||||
|
||||
self.revisions = QtGui.QListWidget()
|
||||
self.revisions.setAlternatingRowColors(True)
|
||||
|
||||
self.button_viz = QtGui.QPushButton(self)
|
||||
self.button_viz.setText(tr('Visualize'))
|
||||
|
||||
self.checkbox_squash = QtGui.QCheckBox(self)
|
||||
self.checkbox_squash.setText(tr('Squash'))
|
||||
|
||||
self.checkbox_commit = QtGui.QCheckBox(self)
|
||||
self.checkbox_commit.setText(tr('Commit'))
|
||||
self.checkbox_commit.setChecked(True)
|
||||
self.checkbox_commit_state = True
|
||||
|
||||
self.button_cancel = QtGui.QPushButton(self)
|
||||
self.button_cancel.setText(tr('Cancel'))
|
||||
|
||||
self.button_merge = QtGui.QPushButton(self)
|
||||
self.button_merge.setText(tr('Merge'))
|
||||
|
||||
# Layouts
|
||||
self.revlayt = QtGui.QHBoxLayout()
|
||||
self.revlayt.addWidget(self.revision_label)
|
||||
self.revlayt.addWidget(self.revision)
|
||||
self.revlayt.addStretch()
|
||||
self.revlayt.addWidget(self.title_label)
|
||||
|
||||
self.radiolayt = QtGui.QHBoxLayout()
|
||||
self.radiolayt.addWidget(self.radio_local)
|
||||
self.radiolayt.addWidget(self.radio_remote)
|
||||
self.radiolayt.addWidget(self.radio_tag)
|
||||
|
||||
self.buttonlayt = QtGui.QHBoxLayout()
|
||||
self.buttonlayt.addWidget(self.button_viz)
|
||||
self.buttonlayt.addStretch()
|
||||
self.buttonlayt.addWidget(self.checkbox_squash)
|
||||
self.buttonlayt.addWidget(self.checkbox_commit)
|
||||
self.buttonlayt.addWidget(self.button_cancel)
|
||||
self.buttonlayt.addWidget(self.button_merge)
|
||||
|
||||
self.mainlayt = QtGui.QVBoxLayout()
|
||||
self.mainlayt.setMargin(4)
|
||||
self.mainlayt.addLayout(self.radiolayt)
|
||||
self.mainlayt.addWidget(self.revisions)
|
||||
self.mainlayt.addLayout(self.revlayt)
|
||||
self.mainlayt.addLayout(self.buttonlayt)
|
||||
self.setLayout(self.mainlayt)
|
||||
|
||||
self.revision.setFocus()
|
||||
|
||||
# Signal/slot connections
|
||||
self.connect(self.button_cancel, SIGNAL('clicked()'),
|
||||
self.reject)
|
||||
|
||||
self.connect(self.checkbox_squash, SIGNAL('clicked()'),
|
||||
self.toggle_squash)
|
||||
|
||||
self.connect(self.revision, SIGNAL('textChanged(QString)'),
|
||||
self.update_title)
|
||||
|
||||
self.connect(self.revisions, SIGNAL('itemSelectionChanged()'),
|
||||
self.revision_selected)
|
||||
|
||||
self.connect(self.radio_local, SIGNAL('clicked()'),
|
||||
self.update_revisions)
|
||||
|
||||
self.connect(self.radio_remote, SIGNAL('clicked()'),
|
||||
self.update_revisions)
|
||||
|
||||
self.connect(self.radio_tag, SIGNAL('clicked()'),
|
||||
self.update_revisions)
|
||||
|
||||
self.connect(self.button_merge, SIGNAL('clicked()'),
|
||||
self.merge_revision)
|
||||
|
||||
self.connect(self.button_viz, SIGNAL('clicked()'),
|
||||
self.viz_revision)
|
||||
|
||||
# Observer messages
|
||||
model.add_message_observer(model.message_updated, self.update_all)
|
||||
self.update_all()
|
||||
|
||||
def update_all(self):
|
||||
"""Set the branch name for the window title and label."""
|
||||
self.update_title()
|
||||
self.update_revisions()
|
||||
|
||||
def update_title(self, dummy_txt=None):
|
||||
branch = self.model.current_branch()
|
||||
revision = unicode(self.revision.text())
|
||||
if revision:
|
||||
txt = unicode(tr('Merge "%s" into "%s"')) % (revision, branch)
|
||||
else:
|
||||
txt = unicode(tr('Merge into "%s"')) % branch
|
||||
self.title_label.setText(txt)
|
||||
self.setWindowTitle(txt)
|
||||
|
||||
def toggle_squash(self):
|
||||
"""Toggles the commit checkbox based on the squash checkbox."""
|
||||
if self.checkbox_squash.isChecked():
|
||||
self.checkbox_commit_state =\
|
||||
self.checkbox_commit.checkState()
|
||||
self.checkbox_commit.setCheckState(Qt.Unchecked)
|
||||
self.checkbox_commit.setDisabled(True)
|
||||
else:
|
||||
self.checkbox_commit.setDisabled(False)
|
||||
oldstate = self.checkbox_commit_state
|
||||
self.checkbox_commit.setCheckState(oldstate)
|
||||
|
||||
def update_revisions(self):
|
||||
"""Update the revision list whenever a radio button is clicked"""
|
||||
self.revisions.clear()
|
||||
self.revisions.addItems(self.current_revisions())
|
||||
|
||||
def revision_selected(self):
|
||||
"""Update the revision field when a list item is selected"""
|
||||
revlist = self.current_revisions()
|
||||
widget = self.revisions
|
||||
row, selected = qtutils.selected_row(widget)
|
||||
if selected and row < len(revlist):
|
||||
revision = revlist[row]
|
||||
self.revision.setText(revision)
|
||||
|
||||
def current_revisions(self):
|
||||
"""Retrieve candidate items to merge"""
|
||||
if self.radio_local.isChecked():
|
||||
return self.model.local_branches()
|
||||
elif self.radio_remote.isChecked():
|
||||
return self.model.remote_branches()
|
||||
elif self.radio_tag.isChecked():
|
||||
return self.model.tags()
|
||||
return []
|
||||
|
||||
def viz_revision(self):
|
||||
"""Launch a gitk-like viewer on the selection revision"""
|
||||
revision = unicode(self.revision.text())
|
||||
if not revision:
|
||||
qtutils.information('No Revision Specified',
|
||||
'You must specify a revision to view')
|
||||
return
|
||||
self.emit(SIGNAL(visualize_revision), revision)
|
||||
|
||||
def merge_revision(self):
|
||||
"""Merge the selected revision/branch"""
|
||||
revision = unicode(self.revision.text())
|
||||
if not revision:
|
||||
qtutils.information('No Revision Specified',
|
||||
'You must specify a revision to merge')
|
||||
return
|
||||
|
||||
do_commit = self.checkbox_commit.isChecked()
|
||||
squash = self.checkbox_squash.isChecked()
|
||||
self.emit(SIGNAL(merge), revision, not(do_commit), squash)
|
||||
self.accept()
|
||||
@@ -1,421 +0,0 @@
|
||||
# Copyright (c) 2009 David Aguilar
|
||||
"""
|
||||
This module provides the BaseModel class, a generic, serializable
|
||||
data container.
|
||||
"""
|
||||
|
||||
import os
|
||||
import imp
|
||||
import types
|
||||
from cStringIO import StringIO
|
||||
|
||||
import jsonpickle
|
||||
|
||||
from cola import core
|
||||
|
||||
|
||||
class BaseModel(object):
|
||||
"""
|
||||
BaseModel is a base class that implements convenient
|
||||
serialization methods.
|
||||
"""
|
||||
def __getitem__(self, item):
|
||||
"""Adds support for retrieving a parameter via model['param'].
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> m['answer']
|
||||
42
|
||||
|
||||
"""
|
||||
return self.__dict__[item]
|
||||
|
||||
def __setitem__(self, item, value):
|
||||
"""
|
||||
Adds support for setting a parameter via model['param'] = value.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m['answer'] = 42
|
||||
>>> m.answer
|
||||
42
|
||||
|
||||
"""
|
||||
self.__dict__[item] = value
|
||||
|
||||
def __iter__(self):
|
||||
"""Provides iterator access to a model class.
|
||||
|
||||
This allows you to iterate over a model's parameters as if it
|
||||
were a list.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> key = 'nada'
|
||||
>>> for k, v in m: key = k
|
||||
>>> key
|
||||
'answer'
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> value = 'nada'
|
||||
>>> for k, v in m: value = v
|
||||
>>> value
|
||||
42
|
||||
|
||||
"""
|
||||
return BaseModelIterator(self)
|
||||
|
||||
def _filter_dict(self, dct):
|
||||
"""This method removes all items that begin with an underscore.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m._filter_dict({'a': 1, '_b': 2, 'c_': 3})
|
||||
{'a': 1}
|
||||
|
||||
"""
|
||||
# Create a set of (key, value) pairs
|
||||
included = set([(k, v) for k, v in dct.iteritems()
|
||||
if not k.startswith('_') and
|
||||
not k.endswith('_') and
|
||||
not k.startswith('py/')])
|
||||
# Anything left over passed the filter
|
||||
filtered = {}
|
||||
for k, v in included:
|
||||
filtered[k] = v
|
||||
return filtered
|
||||
|
||||
def items(self, raw=False):
|
||||
"""Provides a dictionary-like items() iterator.
|
||||
|
||||
>>> m = BaseModel().from_dict(dict(a='a', b='b'))
|
||||
>>> value = 'nada'
|
||||
>>> for k, v in m.items(): value = v;
|
||||
>>> value
|
||||
'b'
|
||||
|
||||
"""
|
||||
if raw:
|
||||
return self.to_dict().items()
|
||||
return self._filter_dict(self.to_dict()).items()
|
||||
|
||||
def iteritems(self, raw=False):
|
||||
"""Provides a dictionary-like iteritems() iterator.
|
||||
|
||||
>>> m = BaseModel().from_dict(dict(a='a', b='b'))
|
||||
>>> value = 'nada'
|
||||
>>> for k, v in m.iteritems(): value = v;
|
||||
>>> value
|
||||
'b'
|
||||
|
||||
"""
|
||||
if raw:
|
||||
return self.to_dict().iteritems()
|
||||
return self._filter_dict(self.to_dict()).iteritems()
|
||||
|
||||
def param_names(self, export=False):
|
||||
"""Returns a list of serializable attribute names.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m._question = 'unknown'
|
||||
>>> m.answer = 42
|
||||
|
||||
>>> m.param_names()
|
||||
['answer']
|
||||
|
||||
>>> m.param_names(export=True)
|
||||
['_question', 'answer']
|
||||
|
||||
"""
|
||||
names = []
|
||||
for k, v in self.__dict__.iteritems():
|
||||
if not export and (k.startswith('_') or k.endswith('_') or
|
||||
k.startswith('py/')):
|
||||
continue
|
||||
if is_function(v):
|
||||
continue
|
||||
if is_atom(v) or is_list(v) or is_dict(v) or is_model(v):
|
||||
names.append(k)
|
||||
names.sort()
|
||||
return names
|
||||
|
||||
def clone(self):
|
||||
"""Creates a clone of the current object.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> clone = m.clone()
|
||||
>>> clone.answer
|
||||
42
|
||||
|
||||
>>> type(clone)
|
||||
<class 'cola.models.base.BaseModel'>
|
||||
|
||||
"""
|
||||
# Go in and out of jsonpickle to create a clone
|
||||
return jsonpickle.decode(jsonpickle.encode(self))
|
||||
|
||||
def has_param(self,param):
|
||||
"""Returns true if a parameter exists in a model.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> m.has_param('answer')
|
||||
True
|
||||
|
||||
>>> m.has_param('question')
|
||||
False
|
||||
|
||||
"""
|
||||
return param in self.__dict__
|
||||
|
||||
def param(self, param, default=None):
|
||||
"""Returns the value of a model parameter.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> m.param('answer')
|
||||
42
|
||||
|
||||
>>> m.param('another answer', 42)
|
||||
42
|
||||
"""
|
||||
return self.__dict__.get(param, default)
|
||||
|
||||
def __getattr__(self, param):
|
||||
"""Provides set_<attribute>(value) append methods.
|
||||
|
||||
This provides automatic convenience methods for handling
|
||||
setattrs with notification.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> m.set_answer(41)
|
||||
>>> # Observers are notified
|
||||
>>> m.answer
|
||||
41
|
||||
|
||||
"""
|
||||
# Base case: we actually have this param
|
||||
if param in self.__dict__:
|
||||
return getattr(self, param)
|
||||
|
||||
# Return a closure over param for calling set_param;
|
||||
# Concrete classes subclass set_param to provide notification
|
||||
if param.startswith('set_'):
|
||||
return lambda v: self.set_param(param[4:], v)
|
||||
|
||||
errmsg = ("'%s' object has no attribute '%s'" %
|
||||
(self.__class__.__name__, param))
|
||||
raise AttributeError(errmsg)
|
||||
|
||||
def set_param(self, param, value):
|
||||
"""Wrapper around setattr()
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 41
|
||||
>>> m.set_param('answer', 42)
|
||||
>>> m.answer
|
||||
42
|
||||
|
||||
"""
|
||||
setattr(self, param, value)
|
||||
|
||||
def copy_params(self, model, params_to_copy=None):
|
||||
"""Copies params from one model to another.
|
||||
|
||||
>>> m = BaseModel()
|
||||
>>> m.answer = 42
|
||||
>>> m._question = 'unknown'
|
||||
>>> m2 = BaseModel()
|
||||
>>> m2.copy_params(m)
|
||||
>>> m2._question
|
||||
'unknown'
|
||||
|
||||
>>> m2.answer
|
||||
42
|
||||
|
||||
"""
|
||||
# Loop over all attributes and copy them over
|
||||
for k in params_to_copy or model.param_names(export=True):
|
||||
self[k] = model.param(k)
|
||||
|
||||
def save(self, filename):
|
||||
"""Saves a model to a file.
|
||||
"""
|
||||
fh = open(filename, 'w')
|
||||
jsonpickle.set_encoder_options('simplejson', indent=4)
|
||||
try:
|
||||
core.write_nointr(fh, jsonpickle.encode(self))
|
||||
except:
|
||||
pass
|
||||
fh.close()
|
||||
jsonpickle.set_encoder_options('simplejson', indent=None)
|
||||
|
||||
def load(self, filename):
|
||||
"""Loads model state from a file.
|
||||
"""
|
||||
fh = open(filename, 'r')
|
||||
contents = core.read_nointr(fh)
|
||||
fh.close()
|
||||
try:
|
||||
ddict = jsonpickle.decode(contents)
|
||||
return self.from_dict(ddict)
|
||||
except:
|
||||
pass
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def instance(filename):
|
||||
"""Instances a model from a filename.
|
||||
"""
|
||||
fh = open(filename, 'r')
|
||||
contents = core.read_nointr(fh)
|
||||
fh.close()
|
||||
obj = jsonpickle.decode(contents)
|
||||
if is_dict(obj):
|
||||
return BaseModel().from_dict(obj)
|
||||
else:
|
||||
return obj
|
||||
|
||||
def from_dict(self, source_dict):
|
||||
"""Import a complex model from a dictionary.
|
||||
If it looks like a duck, it's a duck.
|
||||
|
||||
>>> BaseModel().from_dict({'answer': 42}).answer
|
||||
42
|
||||
"""
|
||||
# Load parameters in-place
|
||||
unpickler = jsonpickle.unpickler.Unpickler()
|
||||
obj = unpickler.restore(source_dict)
|
||||
if is_model(obj):
|
||||
self.copy_params(obj)
|
||||
elif is_dict(obj):
|
||||
for k, v in obj.iteritems():
|
||||
self[k] = unpickler.restore(v)
|
||||
return self
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Exports a model to a dictionary.
|
||||
This simplifies serialization.
|
||||
|
||||
>>> is_dict(BaseModel().to_dict())
|
||||
True
|
||||
|
||||
"""
|
||||
return jsonpickle.pickler.Pickler().flatten(self)
|
||||
|
||||
__indent__ = 0
|
||||
__preindent__ = True
|
||||
__strstack__ = set()
|
||||
|
||||
@staticmethod
|
||||
def _indent(i=0):
|
||||
BaseModel.__indent__ += i
|
||||
return ' ' * BaseModel.__indent__
|
||||
|
||||
def __str__(self):
|
||||
"""A convenient, recursively-defined stringification method."""
|
||||
# This avoid infinite recursion on cyclical structures
|
||||
if self in BaseModel.__strstack__:
|
||||
return '__self__'
|
||||
else:
|
||||
BaseModel.__strstack__.add(self)
|
||||
# IO object for output
|
||||
io = StringIO()
|
||||
# Handle indentation
|
||||
if BaseModel.__preindent__:
|
||||
io.write(BaseModel._indent())
|
||||
|
||||
# Class name and opening parenthesis
|
||||
io.write(self.__class__.__name__ + '(')
|
||||
|
||||
# Go one level deeper
|
||||
BaseModel._indent(1)
|
||||
|
||||
# Output each attribute
|
||||
for param in self.param_names():
|
||||
if param.startswith('_') or param.endswith('_'):
|
||||
continue
|
||||
# Go to the next line
|
||||
io.write('\n')
|
||||
|
||||
# e.g. foo = bar
|
||||
inner = BaseModel._indent() + param + " = "
|
||||
value = self[param]
|
||||
|
||||
# Lists use a new line for each item
|
||||
if type(value) == types.ListType:
|
||||
indent = BaseModel._indent(1)
|
||||
io.write(inner + "[\n")
|
||||
for val in value:
|
||||
# Nested models need special treatment
|
||||
if is_model(val):
|
||||
io.write(str(val)+'\n')
|
||||
else:
|
||||
io.write(indent)
|
||||
io.write(str(val))
|
||||
io.write(",\n")
|
||||
# Unindent, closing bracket
|
||||
io.write(BaseModel._indent(-1))
|
||||
io.write("],")
|
||||
else:
|
||||
# It's not a list, so just output its str() representation
|
||||
BaseModel.__preindent__ = False
|
||||
io.write(inner)
|
||||
io.write(str(value))
|
||||
io.write(',')
|
||||
BaseModel.__preindent__ = True
|
||||
|
||||
# Finish this item, closing parenthesis
|
||||
io.write('\n' + BaseModel._indent(-1) + ')')
|
||||
value = io.getvalue()
|
||||
io.close()
|
||||
|
||||
# Remove ourselves from the recursion-avoidance stack
|
||||
BaseModel.__strstack__.remove(self)
|
||||
return value
|
||||
|
||||
|
||||
#############################################################################
|
||||
class BaseModelIterator(object):
|
||||
"""Provides an iterator over model (key, value) pairs.
|
||||
"""
|
||||
def __init__(self, model):
|
||||
self.model = model
|
||||
self.params = model.param_names()
|
||||
self.idx = -1
|
||||
def next(self):
|
||||
try:
|
||||
self.idx += 1
|
||||
name = self.params[self.idx]
|
||||
return (name, self.model[name])
|
||||
except IndexError:
|
||||
raise StopIteration
|
||||
|
||||
|
||||
#############################################################################
|
||||
def is_model(item):
|
||||
"""Is this an instance of a BaseModel-like object?"""
|
||||
return isinstance(item, BaseModel)
|
||||
|
||||
def is_dict(item):
|
||||
"""Is this an instance of a dictionary?"""
|
||||
return type(item) is types.DictType
|
||||
|
||||
def is_list(item):
|
||||
"""Is this a list?"""
|
||||
return type(item) is types.ListType or type(item) is types.TupleType
|
||||
|
||||
def is_atom(item):
|
||||
"""Is this an atom?"""
|
||||
return(type(item) in types.StringTypes
|
||||
or type(item) is types.BooleanType
|
||||
or type(item) is types.IntType
|
||||
or type(item) is types.LongType
|
||||
or type(item) is types.FloatType
|
||||
or type(item) is types.ComplexType)
|
||||
|
||||
def is_function(item):
|
||||
"""Is this a function?"""
|
||||
return type(item) is types.FunctionType
|
||||
@@ -0,0 +1,94 @@
|
||||
import re
|
||||
|
||||
import cola
|
||||
from cola import gitcmds
|
||||
from cola.obsmodel import ObservableModel
|
||||
|
||||
|
||||
class BrowserModel(ObservableModel):
|
||||
def __init__(self, branch=None):
|
||||
ObservableModel.__init__(self)
|
||||
self.copy_params(cola.model())
|
||||
if branch:
|
||||
self.currentbranch = branch
|
||||
|
||||
# These are parallel lists
|
||||
# ref^{tree}
|
||||
self.types = []
|
||||
self.sha1s = []
|
||||
self.names = []
|
||||
|
||||
self.directories = []
|
||||
self.directory_entries = {}
|
||||
|
||||
# parallel lists
|
||||
self.subtree_types = []
|
||||
self.subtree_sha1s = []
|
||||
self.subtree_names = []
|
||||
|
||||
def init_browser_data(self):
|
||||
"""This scans over self.(names, sha1s, types) to generate
|
||||
directories, directory_entries, and subtree_*"""
|
||||
|
||||
# Collect data for the model
|
||||
if not self.currentbranch:
|
||||
return
|
||||
|
||||
self.subtree_types = []
|
||||
self.subtree_sha1s = []
|
||||
self.subtree_names = []
|
||||
self.directories = []
|
||||
self.directory_entries = {}
|
||||
|
||||
# Lookup the tree info
|
||||
tree_info = gitcmds.parse_ls_tree(self.currentbranch)
|
||||
|
||||
self.set_types(map(lambda(x): x[1], tree_info ))
|
||||
self.set_sha1s(map(lambda(x): x[2], tree_info ))
|
||||
self.set_names(map(lambda(x): x[3], tree_info ))
|
||||
|
||||
if self.directory:
|
||||
self.directories.append('..')
|
||||
|
||||
dir_entries = self.directory_entries
|
||||
dir_regex = re.compile('([^/]+)/')
|
||||
dirs_seen = {}
|
||||
subdirs_seen = {}
|
||||
|
||||
for idx, name in enumerate(self.names):
|
||||
if not name.startswith(self.directory):
|
||||
continue
|
||||
name = name[ len(self.directory): ]
|
||||
if name.count('/'):
|
||||
# This is a directory...
|
||||
match = dir_regex.match(name)
|
||||
if not match:
|
||||
continue
|
||||
dirent = match.group(1) + '/'
|
||||
if dirent not in self.directory_entries:
|
||||
self.directory_entries[dirent] = []
|
||||
|
||||
if dirent not in dirs_seen:
|
||||
dirs_seen[dirent] = True
|
||||
self.directories.append(dirent)
|
||||
|
||||
entry = name.replace(dirent, '')
|
||||
entry_match = dir_regex.match(entry)
|
||||
if entry_match:
|
||||
subdir = entry_match.group(1) + '/'
|
||||
if subdir in subdirs_seen:
|
||||
continue
|
||||
subdirs_seen[subdir] = True
|
||||
dir_entries[dirent].append(subdir)
|
||||
else:
|
||||
dir_entries[dirent].append(entry)
|
||||
else:
|
||||
self.subtree_types.append(self.types[idx])
|
||||
self.subtree_sha1s.append(self.sha1s[idx])
|
||||
self.subtree_names.append(name)
|
||||
|
||||
def subtree_node(self, idx):
|
||||
"""Return data for the tree item at idx."""
|
||||
return (self.subtree_types[idx],
|
||||
self.subtree_sha1s[idx],
|
||||
self.subtree_names[idx])
|
||||
+48
-9
@@ -1,12 +1,15 @@
|
||||
import os
|
||||
from cola import core
|
||||
from cola import git
|
||||
from cola import gitcmds
|
||||
from cola.obsmodel import ObservableModel
|
||||
|
||||
import cola
|
||||
from cola.models.main import MainModel
|
||||
|
||||
class CompareModel(MainModel):
|
||||
|
||||
class CompareModel(ObservableModel):
|
||||
"""Provides custom model data for CompareController."""
|
||||
def __init__(self):
|
||||
MainModel.__init__(self)
|
||||
ObservableModel.__init__(self)
|
||||
self.git = git.instance()
|
||||
self.descriptions_start = []
|
||||
self.descriptions_end = []
|
||||
self.revisions_start = []
|
||||
@@ -16,12 +19,49 @@ class CompareModel(MainModel):
|
||||
self.compare_files = []
|
||||
self.num_results = 100
|
||||
self.show_versions=False
|
||||
self.copy_params(cola.model())
|
||||
|
||||
class BranchCompareModel(MainModel):
|
||||
def update_revision_lists(self, filename=None, show_versions=False):
|
||||
num_results = self.num_results
|
||||
if filename:
|
||||
rev_list = self.git.log('--', filename,
|
||||
max_count=num_results,
|
||||
no_color=True,
|
||||
pretty='oneline')
|
||||
else:
|
||||
rev_list = self.git.log(max_count=num_results,
|
||||
no_color=True,
|
||||
pretty='oneline', all=True)
|
||||
|
||||
commit_list = gitcmds.parse_rev_list(rev_list)
|
||||
commit_list.reverse()
|
||||
commits = map(lambda x: x[0], commit_list)
|
||||
descriptions = map(lambda x: core.decode(x[1]), commit_list)
|
||||
if show_versions:
|
||||
fancy_descr_list = map(lambda x: self.describe(*x), commit_list)
|
||||
self.set_descriptions_start(fancy_descr_list)
|
||||
self.set_descriptions_end(fancy_descr_list)
|
||||
else:
|
||||
self.set_descriptions_start(descriptions)
|
||||
self.set_descriptions_end(descriptions)
|
||||
|
||||
self.set_revisions_start(commits)
|
||||
self.set_revisions_end(commits)
|
||||
|
||||
return commits
|
||||
|
||||
def describe(self, revid, descr):
|
||||
version = self.git.describe(revid, tags=True, always=True,
|
||||
abbrev=4)
|
||||
return version + ' - ' + descr
|
||||
|
||||
|
||||
class BranchCompareModel(ObservableModel):
|
||||
"""Provides custom model data for BranchCompareController."""
|
||||
def __init__(self):
|
||||
MainModel.__init__(self)
|
||||
ObservableModel.__init__(self)
|
||||
self.git = git.instance()
|
||||
self.remote_branches = gitcmds.branch_list(remote=True)
|
||||
self.local_branches = gitcmds.branch_list(remote=False)
|
||||
self.left_combo = ['Local', 'Remote']
|
||||
self.right_combo = ['Local', 'Remote']
|
||||
self.left_combo_index = 0
|
||||
@@ -33,4 +73,3 @@ class BranchCompareModel(MainModel):
|
||||
self.left_list_selected = False
|
||||
self.right_list_selected = False
|
||||
self.diff_files = []
|
||||
self.copy_params(cola.model())
|
||||
|
||||
-1233
File diff suppressed because it is too large
Load Diff
@@ -1,67 +0,0 @@
|
||||
import copy
|
||||
|
||||
from cola.observable import Observable
|
||||
from cola.models.base import BaseModel as Model
|
||||
|
||||
_unserializable_attributes = {
|
||||
'observers': set(),
|
||||
'message_observers': {},
|
||||
'notification_enabled': True,
|
||||
}
|
||||
|
||||
class ObservableModel(Model, Observable):
|
||||
"""Combines serialization from Model with the Observer pattern"""
|
||||
def __init__(self):
|
||||
Model.__init__(self)
|
||||
Observable.__init__(self)
|
||||
|
||||
def save(self, path):
|
||||
"""
|
||||
Saves state to a file. Overrides the base method.
|
||||
|
||||
Hides internal observable attributes when serializing.
|
||||
|
||||
"""
|
||||
attributes = self.remove_unserializable_attributes()
|
||||
Model.save(self, path)
|
||||
self.restore_unserializable_attributes(attributes)
|
||||
|
||||
def clone(self):
|
||||
"""Override Model.clone() to handle observers"""
|
||||
# Go in and out of jsonpickle to create a clone
|
||||
attributes = self.remove_unserializable_attributes()
|
||||
clone = Model.clone(self)
|
||||
unserializable_copy = copy.deepcopy(_unserializable_attributes)
|
||||
self.restore_unserializable_attributes(attributes)
|
||||
clone.restore_unserializable_attributes(unserializable_copy)
|
||||
return clone
|
||||
|
||||
def set_param(self, param, value, notify=True):
|
||||
"""Override Model.set_param() to handle notification"""
|
||||
Model.set_param(self, param, value)
|
||||
# Perform notifications
|
||||
if notify:
|
||||
self.notify_observers(param)
|
||||
|
||||
def remove_unserializable_attributes(self):
|
||||
"""Remove unserializable instance attributes."""
|
||||
attributes = {}
|
||||
for attribute in _unserializable_attributes:
|
||||
attributes[attribute] = self.__dict__.pop(attribute)
|
||||
return attributes
|
||||
|
||||
def restore_unserializable_attributes(self, attributes):
|
||||
"""Restores unserializable instance attributes."""
|
||||
# Restore properties
|
||||
self.__dict__.update(attributes)
|
||||
|
||||
@staticmethod
|
||||
def instance(path):
|
||||
"""Override Model.instance() to account for unserializable data."""
|
||||
obj = Model.instance(path)
|
||||
if isinstance(obj, ObservableModel):
|
||||
# This ensures that clones always start out with no observers,
|
||||
# but retain the hidden _unserializable_attributes.
|
||||
unserializable_copy = copy.deepcopy(_unserializable_attributes)
|
||||
obj.restore_unserializable_attributes(unserializable_copy)
|
||||
return obj
|
||||
@@ -1,8 +1,8 @@
|
||||
from cola.models import main
|
||||
from cola.main.model import MainModel
|
||||
|
||||
class SearchModel(main.MainModel):
|
||||
class SearchModel(MainModel):
|
||||
def __init__(self, cwd=None):
|
||||
main.MainModel.__init__(self, cwd=cwd)
|
||||
MainModel.__init__(self, cwd=cwd)
|
||||
self.query = ''
|
||||
self.max_results = 500
|
||||
self.start_date = ''
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
"""Provides a selection model to handle selection."""
|
||||
|
||||
from cola.models.observable import ObservableModel
|
||||
from cola.obsmodel import ObservableModel
|
||||
from cola.decorators import memoize
|
||||
|
||||
_selection_model = None
|
||||
|
||||
@memoize
|
||||
def selection_model():
|
||||
"""Provides access to a static SelectionModel instance."""
|
||||
global _selection_model
|
||||
if _selection_model:
|
||||
return _selection_model
|
||||
_selection_model = SelectionModel()
|
||||
return _selection_model
|
||||
return SelectionModel()
|
||||
|
||||
|
||||
def selection():
|
||||
@@ -36,6 +34,19 @@ def single_selection():
|
||||
return s, m, um, ut
|
||||
|
||||
|
||||
def filename():
|
||||
s, m, um, ut = single_selection()
|
||||
if s:
|
||||
return s
|
||||
if m:
|
||||
return m
|
||||
if um:
|
||||
return um
|
||||
if ut:
|
||||
return ut
|
||||
return None
|
||||
|
||||
|
||||
class SelectionModel(ObservableModel):
|
||||
"""Provides information about selected file paths."""
|
||||
# Notification message sent out when selection changes
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
"""Simple data container for the CreateTag dialog."""
|
||||
|
||||
import cola
|
||||
from cola.models import observable
|
||||
from cola.obsmodel import ObservableModel
|
||||
|
||||
class TagModel(observable.ObservableModel):
|
||||
class TagModel(ObservableModel):
|
||||
def __init__(self):
|
||||
observable.ObservableModel.__init__(self)
|
||||
ObservableModel.__init__(self)
|
||||
self.tag_msg = ''
|
||||
self.tag_name = ''
|
||||
self.revision = ['HEAD'] + cola.model().all_branches()
|
||||
|
||||
+16
-17
@@ -1,37 +1,36 @@
|
||||
import os
|
||||
from PyQt4 import QtCore
|
||||
from cola import signals
|
||||
|
||||
from cola.compat import set
|
||||
from cola.decorators import memoize
|
||||
|
||||
|
||||
debug = os.environ.get('COLA_NOTIFIER_DEBUG', False)
|
||||
_instance = None
|
||||
|
||||
|
||||
@memoize
|
||||
def notifier():
|
||||
global _instance
|
||||
if _instance:
|
||||
return _instance
|
||||
_instance = Notifier()
|
||||
return _instance
|
||||
return Notifier()
|
||||
|
||||
|
||||
class Notifier(object):
|
||||
"""A pure-python re-implementation of QObject."""
|
||||
"""Object for sending and receiving notification messages"""
|
||||
|
||||
def __init__(self):
|
||||
self.channels = {}
|
||||
|
||||
def broadcast(self, signal, *args, **opts):
|
||||
if debug:
|
||||
print ('broadcast: %s(%s, %s)' % (signals.name(signal),
|
||||
print ('broadcast: %s(%s, %s)' % (signal,
|
||||
args or '<empty>',
|
||||
kwargs or '<empty>'))
|
||||
opts or '<empty>'))
|
||||
self.emit(signal, *args, **opts)
|
||||
|
||||
def emit(self, signal, *args, **opts):
|
||||
subscribers = self.channels.get(signal, None)
|
||||
if subscribers:
|
||||
for fxn in subscribers:
|
||||
fxn(*args, **opts)
|
||||
|
||||
def listen(self, signal, callback):
|
||||
self.connect(signal, callback)
|
||||
if not subscribers:
|
||||
return
|
||||
for fxn in subscribers:
|
||||
fxn(*args, **opts)
|
||||
|
||||
def connect(self, signal, callback):
|
||||
subscribers = self.channels.setdefault(signal, set())
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2008 David Aguilar
|
||||
"""This module provides the Observable class"""
|
||||
|
||||
import types
|
||||
from cola.compat import set
|
||||
|
||||
class Observable(object):
|
||||
"""Handles subject/observer notifications."""
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import copy
|
||||
|
||||
from cola import serializer
|
||||
from cola.observable import Observable
|
||||
from cola.basemodel import BaseModel as Model
|
||||
from cola.compat import set
|
||||
|
||||
|
||||
class ObservableModel(Model, Observable):
|
||||
"""Combines serialization from Model with the Observer pattern"""
|
||||
def __init__(self):
|
||||
Model.__init__(self)
|
||||
Observable.__init__(self)
|
||||
|
||||
def set_param(self, param, value, notify=True):
|
||||
"""Override Model.set_param() to handle notification"""
|
||||
Model.set_param(self, param, value)
|
||||
# Perform notifications
|
||||
if notify:
|
||||
self.notify_observers(param)
|
||||
|
||||
|
||||
_unserializable_attributes = {
|
||||
'observers': set(),
|
||||
'message_observers': {},
|
||||
'notification_enabled': True,
|
||||
}
|
||||
|
||||
class OMSerializer(object):
|
||||
"""Hide the internal 'observers' fields from serialization"""
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
self.attributes = {}
|
||||
|
||||
def pre_encode_hook(self):
|
||||
for attribute in _unserializable_attributes:
|
||||
self.attributes[attribute] = self.obj.__dict__.pop(attribute)
|
||||
|
||||
def post_encode_hook(self):
|
||||
self.obj.__dict__.update(self.attributes)
|
||||
self.attributes = {}
|
||||
|
||||
def post_decode_hook(self):
|
||||
self.obj.__dict__.update(copy.deepcopy(_unserializable_attributes))
|
||||
|
||||
|
||||
# Add a hook for this object
|
||||
serializer.handlers[ObservableModel] = OMSerializer
|
||||
@@ -0,0 +1,15 @@
|
||||
from cola.prefs.view import PreferencesView
|
||||
from cola.prefs.view import diff_font
|
||||
from cola.prefs.view import tab_width
|
||||
from cola.prefs.model import PreferencesModel
|
||||
from cola.prefs.controller import PreferencesController
|
||||
|
||||
|
||||
def preferences(model=None):
|
||||
if model is None:
|
||||
model = PreferencesModel()
|
||||
prefs = PreferencesView(model)
|
||||
ctl = PreferencesController(model, prefs)
|
||||
prefs.show()
|
||||
prefs.raise_()
|
||||
return ctl
|
||||
@@ -0,0 +1,8 @@
|
||||
from cola.ctrl import Controller
|
||||
from cola.prefs.model import command_directory
|
||||
|
||||
|
||||
class PreferencesController(Controller):
|
||||
def __init__(self, model, view):
|
||||
Controller.__init__(self, model, view)
|
||||
self.add_commands(command_directory)
|
||||
@@ -0,0 +1,50 @@
|
||||
from cola import gitcfg
|
||||
from cola import observable
|
||||
from cola.cmds import BaseCommand
|
||||
|
||||
|
||||
class PreferencesModel(observable.Observable):
|
||||
message_set_config = 'set_config'
|
||||
message_config_updated = 'config_updated'
|
||||
|
||||
def __init__(self):
|
||||
observable.Observable.__init__(self)
|
||||
self.config = gitcfg.instance()
|
||||
|
||||
def set_config(self, source, config, value):
|
||||
if source == 'repo':
|
||||
self.config.set_repo(config, value)
|
||||
else:
|
||||
self.config.set_user(config, value)
|
||||
message = self.message_config_updated
|
||||
self.notify_message_observers(message, source, config, value)
|
||||
|
||||
def get_config(self, source, config):
|
||||
if source == 'repo':
|
||||
return self.config.get_repo(config)
|
||||
else:
|
||||
return self.config.get(config)
|
||||
|
||||
|
||||
class SetConfigCommand(BaseCommand):
|
||||
def __init__(self, source, config, value):
|
||||
BaseCommand.__init__(self)
|
||||
self.undoable = True
|
||||
self.source = source
|
||||
self.config = config
|
||||
self.value = value
|
||||
self.old_value = None
|
||||
|
||||
def do(self):
|
||||
self.old_value = self.context.get_config(self.source, self.config)
|
||||
self.context.set_config(self.source, self.config, self.value)
|
||||
|
||||
def undo(self):
|
||||
if self.old_value is None:
|
||||
return
|
||||
self.context.set_config(self.source, self.config, self.old_value)
|
||||
|
||||
|
||||
command_directory = {
|
||||
PreferencesModel.message_set_config: SetConfigCommand,
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import os
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola import qtutils
|
||||
from cola import gitcfg
|
||||
from cola.views import standard
|
||||
from cola.qtutils import relay_signal
|
||||
from cola.utils import is_darwin
|
||||
|
||||
|
||||
class FormWidget(QtGui.QWidget):
|
||||
def __init__(self, model, parent, source='user'):
|
||||
QtGui.QWidget.__init__(self, parent)
|
||||
self.model = model
|
||||
self.config_to_widget = {}
|
||||
self.widget_to_config = {}
|
||||
self.source = source
|
||||
self.config = gitcfg.instance()
|
||||
self.defaults = {}
|
||||
self.setLayout(QtGui.QFormLayout())
|
||||
|
||||
def add_row(self, label, widget):
|
||||
self.layout().addRow(label, widget)
|
||||
|
||||
def set_config(self, config_dict):
|
||||
self.config_to_widget.update(config_dict)
|
||||
for config, (widget, default) in config_dict.items():
|
||||
self.widget_to_config[config] = widget
|
||||
self.defaults[config] = default
|
||||
self.connect_widget_to_config(widget, config)
|
||||
|
||||
def connect_widget_to_config(self, widget, config):
|
||||
if isinstance(widget, QtGui.QSpinBox):
|
||||
widget.connect(widget, SIGNAL('valueChanged(int)'),
|
||||
self._int_config_changed(config))
|
||||
|
||||
elif isinstance(widget, QtGui.QCheckBox):
|
||||
widget.connect(widget, SIGNAL('toggled(bool)'),
|
||||
self._bool_config_changed(config))
|
||||
|
||||
elif isinstance(widget, QtGui.QLineEdit):
|
||||
widget.connect(widget, SIGNAL('editingFinished()'),
|
||||
self._text_config_changed(config))
|
||||
widget.connect(widget, SIGNAL('returnPressed()'),
|
||||
self._text_config_changed(config))
|
||||
|
||||
def _int_config_changed(self, config):
|
||||
def emitter(value):
|
||||
self.emit(SIGNAL(self.model.message_set_config),
|
||||
self.source, config, value)
|
||||
return emitter
|
||||
|
||||
def _bool_config_changed(self, config):
|
||||
def emitter(value):
|
||||
self.emit(SIGNAL(self.model.message_set_config),
|
||||
self.source, config, value)
|
||||
return emitter
|
||||
|
||||
def _text_config_changed(self, config):
|
||||
def emitter():
|
||||
value = unicode(self.sender().text())
|
||||
self.emit(SIGNAL(self.model.message_set_config),
|
||||
self.source, config, value)
|
||||
return emitter
|
||||
|
||||
def update_from_config(self):
|
||||
if self.source == 'repo':
|
||||
getter = self.config.get_repo
|
||||
elif self.source == 'user':
|
||||
getter = self.config.get_user
|
||||
else:
|
||||
getter = self.config.get
|
||||
|
||||
for config, widget in self.widget_to_config.items():
|
||||
value = getter(config)
|
||||
if value is None:
|
||||
value = self.defaults[config]
|
||||
self.set_widget_value(widget, value)
|
||||
|
||||
def set_widget_value(self, widget, value):
|
||||
widget.blockSignals(True)
|
||||
if isinstance(widget, QtGui.QSpinBox):
|
||||
widget.setValue(value)
|
||||
elif isinstance(widget, QtGui.QLineEdit):
|
||||
widget.setText(value)
|
||||
elif isinstance(widget, QtGui.QCheckBox):
|
||||
widget.setChecked(value)
|
||||
widget.blockSignals(False)
|
||||
|
||||
|
||||
class RepoFormWidget(FormWidget):
|
||||
def __init__(self, model, parent, source):
|
||||
FormWidget.__init__(self, model, parent, source=source)
|
||||
|
||||
self.name = QtGui.QLineEdit()
|
||||
self.email = QtGui.QLineEdit()
|
||||
self.merge_verbosity = QtGui.QSpinBox()
|
||||
self.merge_verbosity.setMinimum(0)
|
||||
self.merge_verbosity.setMaximum(5)
|
||||
self.merge_verbosity.setProperty('value', QtCore.QVariant(5))
|
||||
|
||||
self.diff_context = QtGui.QSpinBox()
|
||||
self.diff_context.setMinimum(2)
|
||||
self.diff_context.setMaximum(99)
|
||||
self.diff_context.setProperty('value', QtCore.QVariant(5))
|
||||
|
||||
self.merge_summary = QtGui.QCheckBox()
|
||||
self.merge_summary.setChecked(True)
|
||||
|
||||
self.merge_diffstat = QtGui.QCheckBox()
|
||||
self.merge_diffstat.setChecked(True)
|
||||
|
||||
self.add_row('User Name', self.name)
|
||||
self.add_row('Email Address', self.email)
|
||||
self.add_row('Merge Verbosity', self.merge_verbosity)
|
||||
self.add_row('Number of Diff Context Lines', self.diff_context)
|
||||
self.add_row('Summarize Merge Commits', self.merge_summary)
|
||||
self.add_row('Show Diffstat After Merge', self.merge_diffstat)
|
||||
|
||||
self.set_config({
|
||||
'gui.diffcontext': (self.diff_context, 5),
|
||||
'user.name': (self.name, ''),
|
||||
'user.email': (self.email, ''),
|
||||
'merge.diffstat': (self.merge_diffstat, True),
|
||||
'merge.summary': (self.merge_summary, True),
|
||||
'merge.verbosity': (self.merge_verbosity, 5),
|
||||
})
|
||||
|
||||
|
||||
class SettingsFormWidget(FormWidget):
|
||||
def __init__(self, model, parent):
|
||||
FormWidget.__init__(self, model, parent)
|
||||
|
||||
self.fixed_font = QtGui.QFontComboBox()
|
||||
self.fixed_font.setFontFilters(QtGui.QFontComboBox.MonospacedFonts)
|
||||
|
||||
self.font_size = QtGui.QSpinBox()
|
||||
self.font_size.setMinimum(8)
|
||||
self.font_size.setProperty('value', QtCore.QVariant(12))
|
||||
self._font_str = None
|
||||
|
||||
self.tab_width = QtGui.QSpinBox()
|
||||
self.tab_width.setWrapping(True)
|
||||
self.tab_width.setMaximum(42)
|
||||
|
||||
self.editor = QtGui.QLineEdit()
|
||||
self.historybrowser = QtGui.QLineEdit()
|
||||
self.difftool = QtGui.QLineEdit()
|
||||
self.mergetool = QtGui.QLineEdit()
|
||||
self.keep_merge_backups = QtGui.QCheckBox()
|
||||
self.save_gui_settings = QtGui.QCheckBox()
|
||||
|
||||
self.add_row('Fixed-Width Font', self.fixed_font)
|
||||
self.add_row('Font Size', self.font_size)
|
||||
self.add_row('Tab Width', self.tab_width)
|
||||
self.add_row('Editor', self.editor)
|
||||
self.add_row('History Browser', self.historybrowser)
|
||||
self.add_row('Diff Tool', self.difftool)
|
||||
self.add_row('Merge Tool', self.mergetool)
|
||||
self.add_row('Keep *.orig Merge Backups', self.keep_merge_backups)
|
||||
self.add_row('Save GUI Settings', self.save_gui_settings)
|
||||
|
||||
self.set_config({
|
||||
'cola.savewindowsettings': (self.save_gui_settings, True),
|
||||
'cola.tabwidth': (self.tab_width, 8),
|
||||
'diff.tool': (self.difftool, 'xxdiff'),
|
||||
'gui.editor': (self.editor, os.getenv('VISUAL', 'gvim')),
|
||||
'gui.historybrowser': (self.historybrowser, 'gitk'),
|
||||
'merge.keepbackup': (self.keep_merge_backups, True),
|
||||
'merge.tool': (self.mergetool, 'xxdiff'),
|
||||
})
|
||||
|
||||
self.connect(self.fixed_font, SIGNAL('currentFontChanged(const QFont &)'),
|
||||
self.current_font_changed)
|
||||
|
||||
self.connect(self.font_size, SIGNAL('valueChanged(int)'),
|
||||
self.font_size_changed)
|
||||
|
||||
def update_from_config(self):
|
||||
FormWidget.update_from_config(self)
|
||||
self.fixed_font.blockSignals(True)
|
||||
self.font_size.blockSignals(True)
|
||||
|
||||
font = diff_font()
|
||||
font_size = font.pointSize()
|
||||
|
||||
self.fixed_font.setCurrentFont(font)
|
||||
self.font_size.setValue(font_size)
|
||||
|
||||
self.fixed_font.blockSignals(False)
|
||||
self.font_size.blockSignals(False)
|
||||
|
||||
def font_size_changed(self, size):
|
||||
font = self.fixed_font.currentFont()
|
||||
font.setPointSize(size)
|
||||
self.emit(SIGNAL(self.model.message_set_config),
|
||||
'user', 'cola.fontdiff', unicode(font.toString()))
|
||||
|
||||
def current_font_changed(self, font):
|
||||
self.emit(SIGNAL(self.model.message_set_config),
|
||||
'user', 'cola.fontdiff', unicode(font.toString()))
|
||||
|
||||
|
||||
class PreferencesView(standard.Dialog):
|
||||
def __init__(self, model, parent=None):
|
||||
standard.Dialog.__init__(self, parent=parent)
|
||||
self.setWindowTitle(self.tr('Preferences'))
|
||||
|
||||
self.resize(600, 360)
|
||||
|
||||
self._tabbar = QtGui.QTabBar()
|
||||
self._tabbar.setDrawBase(False)
|
||||
self._tabbar.addTab('All Repositories')
|
||||
self._tabbar.addTab('Current Repository')
|
||||
self._tabbar.addTab('Settings')
|
||||
|
||||
self._user_form = RepoFormWidget(model, self, source='user')
|
||||
self._repo_form = RepoFormWidget(model, self, source='all')
|
||||
self._options_form = SettingsFormWidget(model, self)
|
||||
|
||||
relay_signal(self, self._user_form, SIGNAL(model.message_set_config))
|
||||
relay_signal(self, self._repo_form, SIGNAL(model.message_set_config))
|
||||
relay_signal(self, self._options_form, SIGNAL(model.message_set_config))
|
||||
|
||||
self._stackedwidget = QtGui.QStackedWidget()
|
||||
self._stackedwidget.addWidget(self._user_form)
|
||||
self._stackedwidget.addWidget(self._repo_form)
|
||||
self._stackedwidget.addWidget(self._options_form)
|
||||
|
||||
self.close_button = QtGui.QPushButton(self)
|
||||
self.close_button.setText(qtutils.tr('Close'))
|
||||
self.close_button.setIcon(qtutils.close_icon())
|
||||
|
||||
self._button_layt = QtGui.QHBoxLayout()
|
||||
self._button_layt.setMargin(0)
|
||||
self._button_layt.addStretch()
|
||||
self._button_layt.addWidget(self.close_button)
|
||||
|
||||
self._layt = QtGui.QVBoxLayout()
|
||||
self._layt.setMargin(4)
|
||||
self._layt.addWidget(self._tabbar)
|
||||
self._layt.addWidget(self._stackedwidget)
|
||||
self._layt.addLayout(self._button_layt)
|
||||
self.setLayout(self._layt)
|
||||
|
||||
self.connect(self._tabbar, SIGNAL('currentChanged(int)'),
|
||||
self._stackedwidget.setCurrentIndex)
|
||||
|
||||
self.connect(self._stackedwidget, SIGNAL('currentChanged(int)'),
|
||||
self.update_widget)
|
||||
|
||||
self.connect(self.close_button, SIGNAL('clicked()'), self.accept)
|
||||
|
||||
qtutils.add_close_action(self)
|
||||
|
||||
self.update_widget(0)
|
||||
|
||||
def update_widget(self, idx):
|
||||
widget = self._stackedwidget.widget(idx)
|
||||
widget.update_from_config()
|
||||
|
||||
|
||||
def tab_width():
|
||||
return gitcfg.instance().get('cola.tabwidth', 8)
|
||||
|
||||
|
||||
def diff_font_str():
|
||||
font_str = gitcfg.instance().get('cola.fontdiff')
|
||||
if font_str is None:
|
||||
font = QtGui.QFont()
|
||||
family = 'Monospace'
|
||||
if is_darwin():
|
||||
family = 'Monaco'
|
||||
font.setFamily(family)
|
||||
font_str = unicode(font.toString())
|
||||
return font_str
|
||||
|
||||
|
||||
def diff_font():
|
||||
font_str = diff_font_str()
|
||||
font = QtGui.QFont()
|
||||
font.fromString(font_str)
|
||||
return font
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from cola.prefs import preferences
|
||||
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
preferences()
|
||||
sys.exit(app.exec_())
|
||||
+14
-34
@@ -10,12 +10,12 @@ are always available in the model without having to worry about the
|
||||
different ways to query Qt widgets.
|
||||
|
||||
"""
|
||||
import types
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola import observer
|
||||
from cola.compat import set
|
||||
|
||||
class QObserver(observer.Observer, QtCore.QObject):
|
||||
|
||||
@@ -32,28 +32,22 @@ class QObserver(observer.Observer, QtCore.QObject):
|
||||
self._model_to_view = {}
|
||||
self._mapped_params = set()
|
||||
self._connected = set()
|
||||
self._in_textfield = False
|
||||
self._in_callback = False
|
||||
|
||||
def SLOT(self, *args):
|
||||
"""Default slot to handle all Qt callbacks.
|
||||
This method delegates to callbacks from add_signals."""
|
||||
|
||||
self._in_textfield = False
|
||||
|
||||
widget = self.sender()
|
||||
param = self._widget_names[widget]
|
||||
|
||||
if param in self._mapped_params:
|
||||
model = self.model
|
||||
if isinstance(widget, QtGui.QTextEdit):
|
||||
self._in_textfield = True
|
||||
value = unicode(widget.toPlainText())
|
||||
model.set_param(param, value, notify=False)
|
||||
elif isinstance(widget, QtGui.QLineEdit):
|
||||
self._in_textfield = True
|
||||
value = unicode(widget.text())
|
||||
model.set_param(param, value)
|
||||
model.set_param(param, value, notify=False)
|
||||
elif isinstance(widget, QtGui.QCheckBox):
|
||||
model.set_param(param, widget.isChecked())
|
||||
elif isinstance(widget, QtGui.QSpinBox):
|
||||
@@ -107,24 +101,13 @@ class QObserver(observer.Observer, QtCore.QObject):
|
||||
else:
|
||||
print("SLOT(): Unknown widget:", param, widget)
|
||||
|
||||
self._in_callback = True
|
||||
if param in self._callbacks:
|
||||
self._callbacks[param](*args)
|
||||
self._in_callback = False
|
||||
self._in_textfield = False
|
||||
|
||||
def connect(self, obj, signal_str, *args):
|
||||
"""Convenience function so that subclasses do not have
|
||||
to import QtCore.SIGNAL."""
|
||||
signal = signal_str
|
||||
if type(signal) is str:
|
||||
signal = QtCore.SIGNAL(signal)
|
||||
return QtCore.QObject.connect(obj, signal, *args)
|
||||
|
||||
def add_signals(self, signal_str, *objects):
|
||||
"""Connects object's signal to the QObserver."""
|
||||
for obj in objects:
|
||||
self.connect(obj, signal_str, self.SLOT)
|
||||
self.connect(obj, SIGNAL(signal_str), self.SLOT)
|
||||
|
||||
def add_callbacks(self, **callbacks):
|
||||
"""Registers callbacks that are called in response to GUI events."""
|
||||
@@ -160,18 +143,20 @@ class QObserver(observer.Observer, QtCore.QObject):
|
||||
self.add_signals('itemClicked(QListWidgetItem *)', widget)
|
||||
doubleclick = str(widget.objectName())+'_doubleclick'
|
||||
if hasattr(self, doubleclick):
|
||||
self.connect(widget, 'itemDoubleClicked(QListWidgetItem *)',
|
||||
self.connect(widget,
|
||||
SIGNAL('itemDoubleClicked(QListWidgetItem *)'),
|
||||
getattr(self, doubleclick))
|
||||
elif isinstance(widget, QtGui.QTreeWidget):
|
||||
self.add_signals('itemSelectionChanged()', widget)
|
||||
self.add_signals('itemClicked(QTreeWidgetItem *, int)', widget)
|
||||
doubleclick = str(widget.objectName())+'_doubleclick'
|
||||
if hasattr(self, doubleclick):
|
||||
self.connect(widget, 'itemDoubleClicked(QTreeWidgetItem *, int)',
|
||||
self.connect(widget,
|
||||
SIGNAL('itemDoubleClicked(QTreeWidgetItem *, int)'),
|
||||
getattr(self, doubleclick))
|
||||
|
||||
elif isinstance(widget, QtGui.QAbstractButton):
|
||||
self.add_signals('released()', widget)
|
||||
self.add_signals('clicked()', widget)
|
||||
elif isinstance(widget, QtGui.QAction):
|
||||
self.add_signals('triggered()', widget)
|
||||
elif isinstance(widget, QtGui.QCheckBox):
|
||||
@@ -194,9 +179,9 @@ class QObserver(observer.Observer, QtCore.QObject):
|
||||
def add_actions(self, **kwargs):
|
||||
"""
|
||||
Register view actions.
|
||||
|
||||
|
||||
Action are called in response to view changes.(view->model).
|
||||
|
||||
|
||||
"""
|
||||
for param, callback in kwargs.iteritems():
|
||||
if type(callback) is list:
|
||||
@@ -207,12 +192,6 @@ class QObserver(observer.Observer, QtCore.QObject):
|
||||
def subject_changed(self, param, value):
|
||||
"""Sends a model param to the view(model->view)"""
|
||||
|
||||
if self._in_textfield and not self._in_callback:
|
||||
# A slot has changed the model and we're not in
|
||||
# a user callback. In this case the event is causing
|
||||
# a feedback loop so skip redundant work and return.
|
||||
return
|
||||
|
||||
if param in self._model_to_view:
|
||||
# Temporarily disable notification to avoid loop-backs
|
||||
notify = self.model.notification_enabled
|
||||
@@ -224,7 +203,7 @@ class QObserver(observer.Observer, QtCore.QObject):
|
||||
elif isinstance(widget, QtGui.QPixmap):
|
||||
widget.load(value)
|
||||
elif isinstance(widget, QtGui.QTextEdit):
|
||||
widget.setText(value)
|
||||
widget.setPlainText(value)
|
||||
elif isinstance(widget, QtGui.QLineEdit):
|
||||
widget.setText(value)
|
||||
elif isinstance(widget, QtGui.QListWidget):
|
||||
@@ -295,6 +274,7 @@ class QObserver(observer.Observer, QtCore.QObject):
|
||||
else:
|
||||
print('subject_changed(): Unknown widget:',
|
||||
str(widget.objectName()), widget, value)
|
||||
|
||||
self.model.notification_enabled = notify
|
||||
|
||||
if param not in self._actions:
|
||||
|
||||
+688
@@ -0,0 +1,688 @@
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import Qt
|
||||
from PyQt4.QtCore import QVariant
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
from PyQt4.QtGui import QFont
|
||||
from PyQt4.QtGui import QSyntaxHighlighter
|
||||
from PyQt4.QtGui import QTextCharFormat
|
||||
from PyQt4.QtGui import QColor
|
||||
from PyQt4.QtCore import pyqtProperty
|
||||
|
||||
import cola
|
||||
from cola import resources
|
||||
from cola import utils
|
||||
from cola import qtutils
|
||||
from cola.compat import set
|
||||
from cola.qtutils import tr
|
||||
|
||||
|
||||
def create_button(text, layout=None, tooltip=None, icon=None):
|
||||
"""Create a button, set its title, and add it to the parent."""
|
||||
button = QtGui.QPushButton()
|
||||
button.setText(tr(text))
|
||||
if icon:
|
||||
button.setIcon(icon)
|
||||
if layout is not None:
|
||||
layout.addWidget(button)
|
||||
return button
|
||||
|
||||
|
||||
def create_dock(title, parent):
|
||||
"""Create a dock widget and set it up accordingly."""
|
||||
dock = QtGui.QDockWidget(parent)
|
||||
dock.setWindowTitle(tr(title))
|
||||
dock.setObjectName(title)
|
||||
return dock
|
||||
|
||||
|
||||
def create_menu(title, parent):
|
||||
"""Create a menu and set its title."""
|
||||
qmenu = QtGui.QMenu(parent)
|
||||
qmenu.setTitle(tr(title))
|
||||
return qmenu
|
||||
|
||||
|
||||
def create_toolbutton(parent, text=None, layout=None, tooltip=None, icon=None):
|
||||
button = QtGui.QToolButton(parent)
|
||||
button.setAutoRaise(True)
|
||||
button.setAutoFillBackground(True)
|
||||
if icon:
|
||||
button.setIcon(icon)
|
||||
if text:
|
||||
button.setText(tr(text))
|
||||
button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
||||
if tooltip:
|
||||
button.setToolTip(tr(tooltip))
|
||||
if layout is not None:
|
||||
layout.addWidget(button)
|
||||
return button
|
||||
|
||||
|
||||
class QFlowLayoutWidget(QtGui.QWidget):
|
||||
|
||||
_horizontal = QtGui.QBoxLayout.LeftToRight
|
||||
_vertical = QtGui.QBoxLayout.TopToBottom
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtGui.QWidget.__init__(self, parent)
|
||||
self._direction = self._vertical
|
||||
self._layout = layout = QtGui.QBoxLayout(self._direction)
|
||||
layout.setSpacing(2)
|
||||
layout.setMargin(2)
|
||||
self.setLayout(layout)
|
||||
self.setContentsMargins(2, 2, 2, 2)
|
||||
policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum,
|
||||
QtGui.QSizePolicy.Minimum)
|
||||
self.setSizePolicy(policy)
|
||||
self.setMinimumSize(QtCore.QSize(1, 1))
|
||||
|
||||
def resizeEvent(self, event):
|
||||
size = event.size()
|
||||
if size.width() * 0.8 < size.height():
|
||||
dxn = self._vertical
|
||||
else:
|
||||
dxn = self._horizontal
|
||||
|
||||
if dxn != self._direction:
|
||||
self._direction = dxn
|
||||
self.layout().setDirection(dxn)
|
||||
|
||||
|
||||
class QCollapsibleGroupBox(QtGui.QGroupBox):
|
||||
def __init__(self, parent=None):
|
||||
QtGui.QGroupBox.__init__(self, parent)
|
||||
self.setFlat(True)
|
||||
self.collapsed = False
|
||||
self.click_pos = None
|
||||
self.collapse_icon_size = 16
|
||||
|
||||
def set_collapsed(self, collapsed):
|
||||
self.collapsed = collapsed
|
||||
for widget in self.findChildren(QtGui.QWidget):
|
||||
widget.setHidden(collapsed)
|
||||
self.emit(SIGNAL('toggled(bool)'), collapsed)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
option = QtGui.QStyleOptionGroupBox()
|
||||
self.initStyleOption(option)
|
||||
icon_size = self.collapse_icon_size
|
||||
button_area = QtCore.QRect(0, 0, icon_size, icon_size)
|
||||
top_left = option.rect.adjusted(0, 0, -10, 0).topLeft()
|
||||
button_area.moveTopLeft(QtCore.QPoint(top_left))
|
||||
self.click_pos = event.pos()
|
||||
QtGui.QGroupBox.mousePressEvent(self, event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if (event.button() == QtCore.Qt.LeftButton and
|
||||
self.click_pos == event.pos()):
|
||||
self.set_collapsed(not self.collapsed)
|
||||
QtGui.QGroupBox.mouseReleaseEvent(self, event)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QStylePainter(self)
|
||||
option = QtGui.QStyleOptionGroupBox()
|
||||
self.initStyleOption(option)
|
||||
painter.save()
|
||||
painter.translate(self.collapse_icon_size + 4, 0)
|
||||
painter.drawComplexControl(QtGui.QStyle.CC_GroupBox, option)
|
||||
painter.restore()
|
||||
|
||||
style = QtGui.QStyle
|
||||
point = option.rect.adjusted(0, 0, -10, 0).topLeft()
|
||||
icon_size = self.collapse_icon_size
|
||||
option.rect = QtCore.QRect(point.x(), point.y(), icon_size, icon_size)
|
||||
if self.collapsed:
|
||||
painter.drawPrimitive(style.PE_IndicatorArrowRight, option)
|
||||
else:
|
||||
painter.drawPrimitive(style.PE_IndicatorArrowDown, option)
|
||||
|
||||
|
||||
class GitRefCompleter(QtGui.QCompleter):
|
||||
"""Provides completion for branches and tags"""
|
||||
def __init__(self, parent):
|
||||
QtGui.QCompleter.__init__(self, parent)
|
||||
self._model = GitRefModel(parent)
|
||||
self.setModel(self._model)
|
||||
self.setCompletionMode(self.UnfilteredPopupCompletion)
|
||||
self.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
def __del__(self):
|
||||
self.dispose()
|
||||
|
||||
def dispose(self):
|
||||
self._model.dispose()
|
||||
|
||||
|
||||
class GitRefLineEdit(QtGui.QLineEdit):
|
||||
def __init__(self, parent=None):
|
||||
QtGui.QLineEdit.__init__(self, parent)
|
||||
self.refcompleter = GitRefCompleter(self)
|
||||
self.setCompleter(self.refcompleter)
|
||||
|
||||
|
||||
class GitRefModel(QtGui.QStandardItemModel):
|
||||
def __init__(self, parent):
|
||||
QtGui.QStandardItemModel.__init__(self, parent)
|
||||
self.cmodel = cola.model()
|
||||
msg = self.cmodel.message_updated
|
||||
self.cmodel.add_message_observer(msg, self.update_matches)
|
||||
self.update_matches()
|
||||
|
||||
def dispose(self):
|
||||
self.cmodel.remove_observer(self.update_matches)
|
||||
|
||||
def update_matches(self):
|
||||
model = self.cmodel
|
||||
matches = model.local_branches + model.remote_branches + model.tags
|
||||
QStandardItem = QtGui.QStandardItem
|
||||
self.clear()
|
||||
for match in matches:
|
||||
item = QStandardItem()
|
||||
item.setIcon(QtGui.QIcon(resources.icon('git.svg')))
|
||||
item.setText(match)
|
||||
self.appendRow(item)
|
||||
|
||||
|
||||
class GitLogCompletionModel(QtGui.QStandardItemModel):
|
||||
def __init__(self, parent):
|
||||
self.matched_text = None
|
||||
QtGui.QStandardItemModel.__init__(self, parent)
|
||||
self.cmodel = cola.model()
|
||||
|
||||
def lower_cmp(self, a, b):
|
||||
return cmp(a.replace('.','').lower(), b.replace('.','').lower())
|
||||
|
||||
def update_matches(self, case_sensitive):
|
||||
QStandardItem = QtGui.QStandardItem
|
||||
file_list = self.cmodel.everything()
|
||||
files = set(file_list)
|
||||
files_and_dirs = utils.add_parents(set(files))
|
||||
dirs = files_and_dirs.difference(files)
|
||||
|
||||
file_icon = qtutils.file_icon()
|
||||
dir_icon = qtutils.dir_icon()
|
||||
git_icon = QtGui.QIcon(resources.icon('git.svg'))
|
||||
|
||||
model = self.cmodel
|
||||
refs = model.local_branches + model.remote_branches + model.tags
|
||||
matched_text = self.matched_text
|
||||
|
||||
if matched_text:
|
||||
if case_sensitive:
|
||||
matched_refs = [r for r in refs if matched_text in r]
|
||||
else:
|
||||
matched_refs = [r for r in refs
|
||||
if matched_text.lower() in r.lower()]
|
||||
else:
|
||||
matched_refs = refs
|
||||
|
||||
matched_refs.sort(cmp=self.lower_cmp)
|
||||
|
||||
if matched_text:
|
||||
if case_sensitive:
|
||||
matched_paths = [f for f in files_and_dirs
|
||||
if matched_text in f]
|
||||
else:
|
||||
matched_paths = [f for f in files_and_dirs
|
||||
if matched_text.lower() in f.lower()]
|
||||
else:
|
||||
matched_paths = list(files_and_dirs)
|
||||
|
||||
matched_paths.sort(cmp=self.lower_cmp)
|
||||
|
||||
items = []
|
||||
|
||||
for ref in matched_refs:
|
||||
item = QStandardItem()
|
||||
item.setText(ref)
|
||||
item.setIcon(git_icon)
|
||||
items.append(item)
|
||||
|
||||
if matched_paths and (not matched_text or matched_text in '--'):
|
||||
item = QStandardItem()
|
||||
item.setText('--')
|
||||
item.setIcon(file_icon)
|
||||
items.append(item)
|
||||
|
||||
for match in matched_paths:
|
||||
item = QStandardItem()
|
||||
item.setText(match)
|
||||
if match in dirs:
|
||||
item.setIcon(dir_icon)
|
||||
else:
|
||||
item.setIcon(file_icon)
|
||||
items.append(item)
|
||||
|
||||
self.clear()
|
||||
for item in items:
|
||||
self.appendRow(item)
|
||||
|
||||
def set_match_text(self, text, case_sensitive):
|
||||
self.matched_text = text
|
||||
self.update_matches(case_sensitive)
|
||||
|
||||
|
||||
class GitLogLineEdit(QtGui.QLineEdit):
|
||||
def __init__(self, parent=None):
|
||||
QtGui.QLineEdit.__init__(self, parent)
|
||||
# used to hide the completion popup after a drag-select
|
||||
self._drag = 0
|
||||
|
||||
self._model = GitLogCompletionModel(self)
|
||||
self._delegate = HighlightCompletionDelegate(self)
|
||||
|
||||
self._completer = QtGui.QCompleter(self)
|
||||
self._completer.setWidget(self)
|
||||
self._completer.setModel(self._model)
|
||||
self._completer.setCompletionMode(
|
||||
QtGui.QCompleter.UnfilteredPopupCompletion)
|
||||
self._completer.popup().setItemDelegate(self._delegate)
|
||||
|
||||
self.connect(self._completer, SIGNAL('activated(QString)'),
|
||||
self._complete)
|
||||
self.connect(self, SIGNAL('textChanged(QString)'), self._text_changed)
|
||||
self._keys_to_ignore = set([QtCore.Qt.Key_Enter,
|
||||
QtCore.Qt.Key_Return,
|
||||
QtCore.Qt.Key_Escape])
|
||||
|
||||
def is_case_sensitive(self, text):
|
||||
return bool([char for char in text if char.isupper()])
|
||||
|
||||
def _text_changed(self, text):
|
||||
text = self.last_word()
|
||||
case_sensitive = self.is_case_sensitive(text)
|
||||
if case_sensitive:
|
||||
self._completer.setCaseSensitivity(QtCore.Qt.CaseSensitive)
|
||||
else:
|
||||
self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
self._delegate.set_highlight_text(text, case_sensitive)
|
||||
self._model.set_match_text(text, case_sensitive)
|
||||
|
||||
def update_matches(self):
|
||||
text = self.last_word()
|
||||
case_sensitive = self.is_case_sensitive(text)
|
||||
self._model.update_matches(case_sensitive)
|
||||
|
||||
def _complete(self, completion):
|
||||
"""
|
||||
This is the event handler for the QCompleter.activated(QString) signal,
|
||||
it is called when the user selects an item in the completer popup.
|
||||
"""
|
||||
if not completion:
|
||||
return
|
||||
words = self.words()
|
||||
if words:
|
||||
words.pop()
|
||||
words.append(unicode(completion))
|
||||
self.setText(subprocess.list2cmdline(words))
|
||||
self.emit(SIGNAL('ref_changed'))
|
||||
|
||||
def words(self):
|
||||
return utils.shell_usplit(unicode(self.text()))
|
||||
|
||||
def last_word(self):
|
||||
words = self.words()
|
||||
if not words:
|
||||
return unicode(self.text())
|
||||
if not words[-1]:
|
||||
return u''
|
||||
return words[-1]
|
||||
|
||||
def event(self, event):
|
||||
if event.type() == QtCore.QEvent.KeyPress:
|
||||
if (event.key() == QtCore.Qt.Key_Tab and
|
||||
self._completer.popup().isVisible()):
|
||||
event.ignore()
|
||||
return True
|
||||
return QtGui.QLineEdit.event(self, event)
|
||||
|
||||
def do_completion(self):
|
||||
self._completer.popup().setCurrentIndex(
|
||||
self._completer.completionModel().index(0,0))
|
||||
self._completer.complete()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if self._completer.popup().isVisible():
|
||||
if event.key() in self._keys_to_ignore:
|
||||
event.ignore()
|
||||
self._complete(self.last_word())
|
||||
return
|
||||
|
||||
elif (event.key() == QtCore.Qt.Key_Down and
|
||||
self._completer.completionCount() > 0):
|
||||
event.accept()
|
||||
self.do_completion()
|
||||
return
|
||||
|
||||
QtGui.QLineEdit.keyPressEvent(self, event)
|
||||
|
||||
prefix = self.last_word()
|
||||
if prefix != unicode(self._completer.completionPrefix()):
|
||||
self._update_popup_items(prefix)
|
||||
if len(event.text()) > 0 and len(prefix) > 0:
|
||||
self._completer.complete()
|
||||
if len(prefix) == 0:
|
||||
self._completer.popup().hide()
|
||||
|
||||
#: _drag: 0 - unclicked, 1 - clicked, 2 - dragged
|
||||
def mousePressEvent(self, event):
|
||||
self._drag = 1
|
||||
return QtGui.QLineEdit.mousePressEvent(self, event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self._drag == 1:
|
||||
self._drag = 2
|
||||
return QtGui.QLineEdit.mouseMoveEvent(self, event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._drag != 2 and event.buttons() != QtCore.Qt.RightButton:
|
||||
self.do_completion()
|
||||
self._drag = 0
|
||||
return QtGui.QLineEdit.mouseReleaseEvent(self, event)
|
||||
|
||||
def close_popup(self):
|
||||
self._completer.popup().close()
|
||||
|
||||
def _update_popup_items(self, prefix):
|
||||
"""
|
||||
Filters the completer's popup items to only show items
|
||||
with the given prefix.
|
||||
"""
|
||||
self._completer.setCompletionPrefix(prefix)
|
||||
self._completer.popup().setCurrentIndex(
|
||||
self._completer.completionModel().index(0,0))
|
||||
|
||||
|
||||
class HighlightCompletionDelegate(QtGui.QStyledItemDelegate):
|
||||
"""A delegate used for auto-completion to give formatted completion"""
|
||||
def __init__(self, parent=None): # model, parent=None):
|
||||
QtGui.QStyledItemDelegate.__init__(self, parent)
|
||||
self.highlight_text = ''
|
||||
self.case_sensitive = False
|
||||
|
||||
self.doc = QtGui.QTextDocument()
|
||||
self.doc.setDocumentMargin(0)
|
||||
|
||||
def set_highlight_text(self, text, case_sensitive):
|
||||
"""Sets the text that will be made bold in the term name when displayed"""
|
||||
self.highlight_text = text
|
||||
self.case_sensitive = case_sensitive
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
"""Overloaded Qt method for custom painting of a model index"""
|
||||
if not self.highlight_text:
|
||||
return QtGui.QStyledItemDelegate.paint(self, painter, option, index)
|
||||
|
||||
text = unicode(index.data().toPyObject())
|
||||
if self.case_sensitive:
|
||||
html = text.replace(self.highlight_text,
|
||||
'<strong>%s</strong>' % self.highlight_text)
|
||||
else:
|
||||
match = re.match('(.*)(' + self.highlight_text + ')(.*)',
|
||||
text, re.IGNORECASE)
|
||||
if match:
|
||||
start = match.group(1) or ''
|
||||
middle = match.group(2) or ''
|
||||
end = match.group(3) or ''
|
||||
html = (start + ('<strong>%s</strong>' % middle) + end)
|
||||
else:
|
||||
html = text
|
||||
self.doc.setHtml(html)
|
||||
|
||||
# Painting item without text, Text Document will paint the text
|
||||
optionV4 = QtGui.QStyleOptionViewItemV4(option)
|
||||
self.initStyleOption(optionV4, index)
|
||||
optionV4.text = QtCore.QString()
|
||||
|
||||
style = QtGui.QApplication.style()
|
||||
style.drawControl(QtGui.QStyle.CE_ItemViewItem, optionV4, painter)
|
||||
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
|
||||
|
||||
# Highlighting text if item is selected
|
||||
if (optionV4.state & QtGui.QStyle.State_Selected):
|
||||
ctx.palette.setColor(QtGui.QPalette.Text, optionV4.palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
|
||||
|
||||
# translate the painter to where the text is drawn
|
||||
textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, optionV4)
|
||||
painter.save()
|
||||
|
||||
start = textRect.topLeft() + QtCore.QPoint(3, 0)
|
||||
painter.translate(start)
|
||||
painter.setClipRect(textRect.translated(-start))
|
||||
|
||||
# tell the text document to draw the html for us
|
||||
self.doc.documentLayout().draw(painter, ctx)
|
||||
painter.restore()
|
||||
|
||||
# Syntax highlighting
|
||||
|
||||
def TERMINAL(pattern):
|
||||
"""
|
||||
Denotes that a pattern is the final pattern that should
|
||||
be matched. If this pattern matches no other formats
|
||||
will be applied, even if they would have matched.
|
||||
"""
|
||||
return '__TERMINAL__:%s' % pattern
|
||||
|
||||
# Cache the results of re.compile so that we don't keep
|
||||
# rebuilding the same regexes whenever stylesheets change
|
||||
_RGX_CACHE = {}
|
||||
|
||||
default_colors = {}
|
||||
def _install_default_colors():
|
||||
def color(c, a=255):
|
||||
qc = QColor(c)
|
||||
qc.setAlpha(a)
|
||||
return qc
|
||||
default_colors.update({
|
||||
'color_add': color(Qt.green, 128),
|
||||
'color_remove': color(Qt.red, 128),
|
||||
'color_begin': color(Qt.darkCyan),
|
||||
'color_header': color(Qt.darkYellow),
|
||||
'color_stat_add': color(QColor(32, 255, 32)),
|
||||
'color_stat_info': color(QColor(32, 32, 255)),
|
||||
'color_stat_remove': color(QColor(255, 32, 32)),
|
||||
'color_emphasis': color(Qt.black),
|
||||
'color_info': color(Qt.blue),
|
||||
'color_date': color(Qt.darkCyan),
|
||||
})
|
||||
_install_default_colors()
|
||||
|
||||
|
||||
class GenericSyntaxHighligher(QSyntaxHighlighter):
|
||||
def __init__(self, doc, *args, **kwargs):
|
||||
QSyntaxHighlighter.__init__(self, doc)
|
||||
for attr, val in default_colors.items():
|
||||
setattr(self, attr, val)
|
||||
self._rules = []
|
||||
self.generate_rules()
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self._rules = []
|
||||
self.generate_rules()
|
||||
|
||||
def generate_rules(self):
|
||||
pass
|
||||
|
||||
def create_rules(self, *rules):
|
||||
if len(rules) % 2:
|
||||
raise Exception('create_rules requires an even '
|
||||
'number of arguments.')
|
||||
for idx, rule in enumerate(rules):
|
||||
if idx % 2:
|
||||
continue
|
||||
formats = rules[idx+1]
|
||||
terminal = rule.startswith(TERMINAL(''))
|
||||
if terminal:
|
||||
rule = rule[len(TERMINAL('')):]
|
||||
try:
|
||||
regex = _RGX_CACHE[rule]
|
||||
except KeyError:
|
||||
regex = _RGX_CACHE[rule] = re.compile(rule)
|
||||
self._rules.append((regex, formats, terminal,))
|
||||
|
||||
def formats(self, line):
|
||||
matched = []
|
||||
for regex, fmts, terminal in self._rules:
|
||||
match = regex.match(line)
|
||||
if not match:
|
||||
continue
|
||||
matched.append([match, fmts])
|
||||
if terminal:
|
||||
return matched
|
||||
return matched
|
||||
|
||||
def mkformat(self, fg=None, bg=None, bold=False):
|
||||
fmt = QTextCharFormat()
|
||||
if fg:
|
||||
fmt.setForeground(fg)
|
||||
if bg:
|
||||
fmt.setBackground(bg)
|
||||
if bold:
|
||||
fmt.setFontWeight(QFont.Bold)
|
||||
return fmt
|
||||
|
||||
def highlightBlock(self, qstr):
|
||||
ascii = unicode(qstr)
|
||||
if not ascii:
|
||||
return
|
||||
formats = self.formats(ascii)
|
||||
if not formats:
|
||||
return
|
||||
for match, fmts in formats:
|
||||
start = match.start()
|
||||
end = match.end()
|
||||
groups = match.groups()
|
||||
|
||||
# No groups in the regex, assume this is a single rule
|
||||
# that spans the entire line
|
||||
if not groups:
|
||||
self.setFormat(0, len(ascii), fmts)
|
||||
continue
|
||||
|
||||
# Groups exist, rule is a tuple corresponding to group
|
||||
for grpidx, group in enumerate(groups):
|
||||
# allow empty matches
|
||||
if not group:
|
||||
continue
|
||||
# allow None as a no-op format
|
||||
length = len(group)
|
||||
if fmts[grpidx]:
|
||||
self.setFormat(start, start+length,
|
||||
fmts[grpidx])
|
||||
start += length
|
||||
|
||||
def set_colors(self, colordict):
|
||||
for attr, val in colordict.items():
|
||||
setattr(self, attr, val)
|
||||
|
||||
|
||||
class DiffSyntaxHighlighter(GenericSyntaxHighligher):
|
||||
"""Implements the diff syntax highlighting
|
||||
|
||||
This class is used by widgets that display diffs.
|
||||
|
||||
"""
|
||||
def __init__(self, doc, whitespace=True):
|
||||
self.whitespace = whitespace
|
||||
GenericSyntaxHighligher.__init__(self, doc)
|
||||
|
||||
def generate_rules(self):
|
||||
diff_begin = self.mkformat(self.color_begin, bold=True)
|
||||
diff_head = self.mkformat(self.color_header)
|
||||
diff_add = self.mkformat(bg=self.color_add)
|
||||
diff_remove = self.mkformat(bg=self.color_remove)
|
||||
|
||||
diffstat_info = self.mkformat(self.color_stat_info, bold=True)
|
||||
diffstat_add = self.mkformat(self.color_stat_add, bold=True)
|
||||
diffstat_remove = self.mkformat(self.color_stat_remove, bold=True)
|
||||
|
||||
if self.whitespace:
|
||||
bad_ws = self.mkformat(Qt.black, Qt.red)
|
||||
|
||||
# We specify the whitespace rule last so that it is
|
||||
# applied after the diff addition/removal rules.
|
||||
# The rules for the header
|
||||
diff_bgn_rgx = TERMINAL('^@@|^\+\+\+|^---')
|
||||
diff_hd1_rgx = TERMINAL('^diff --git')
|
||||
diff_hd2_rgx = TERMINAL('^index \S+\.\.\S+')
|
||||
diff_hd3_rgx = TERMINAL('^new file mode')
|
||||
diff_add_rgx = TERMINAL('^\+')
|
||||
diff_rmv_rgx = TERMINAL('^-')
|
||||
diff_sts_rgx = ('(.+\|.+?)(\d+)(.+?)([\+]*?)([-]*?)$')
|
||||
diff_sum_rgx = ('(\s+\d+ files changed[^\d]*)'
|
||||
'(:?\d+ insertions[^\d]*)'
|
||||
'(:?\d+ deletions.*)$')
|
||||
|
||||
self.create_rules(diff_bgn_rgx, diff_begin,
|
||||
diff_hd1_rgx, diff_head,
|
||||
diff_hd2_rgx, diff_head,
|
||||
diff_hd3_rgx, diff_head,
|
||||
diff_add_rgx, diff_add,
|
||||
diff_rmv_rgx, diff_remove,
|
||||
diff_sts_rgx, (None, diffstat_info,
|
||||
None, diffstat_add,
|
||||
diffstat_remove),
|
||||
diff_sum_rgx, (diffstat_info,
|
||||
diffstat_add,
|
||||
diffstat_remove))
|
||||
if self.whitespace:
|
||||
self.create_rules('(..*?)(\s+)$', (None, bad_ws))
|
||||
|
||||
|
||||
# This is used as a mixin to generate property callbacks
|
||||
def accessors(attr):
|
||||
private_attr = '_'+attr
|
||||
|
||||
def getter(self):
|
||||
return self.__dict__.get(private_attr, None)
|
||||
|
||||
def setter(self, value):
|
||||
self.__dict__[private_attr] = value
|
||||
self.reset_syntax()
|
||||
return (getter, setter)
|
||||
|
||||
def install_style_properties(cls):
|
||||
# Diff GUI colors -- this is controllable via the style sheet
|
||||
if pyqtProperty is None:
|
||||
return
|
||||
for name in default_colors:
|
||||
setattr(cls, name, pyqtProperty('QColor', *accessors(name)))
|
||||
|
||||
def set_theme_properties(widget):
|
||||
for name, color in default_colors.items():
|
||||
widget.setProperty(name, QVariant(color))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
class SyntaxTestDialog(QtGui.QDialog):
|
||||
def __init__(self, parent):
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.resize(720, 512)
|
||||
self.vboxlayout = QtGui.QVBoxLayout(self)
|
||||
self.vboxlayout.setObjectName('vboxlayout')
|
||||
self.output_text = QtGui.QTextEdit(self)
|
||||
font = QtGui.QFont()
|
||||
if utils.is_darwin():
|
||||
family = 'Monaco'
|
||||
else:
|
||||
family = 'Monospace'
|
||||
font.setFamily(family)
|
||||
font.setPointSize(13)
|
||||
self.output_text.setFont(font)
|
||||
self.output_text.setAcceptDrops(False)
|
||||
self.vboxlayout.addWidget(self.output_text)
|
||||
self.syntax = DiffSyntaxHighlighter(self.output_text.document())
|
||||
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
dialog = SyntaxTestDialog(app.activeWindow())
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
app.exec_()
|
||||
@@ -0,0 +1,25 @@
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
|
||||
def install():
|
||||
if not hasattr(QtGui.QHBoxLayout, 'setContentsMargins'):
|
||||
QtGui.QHBoxLayout.setContentsMargins = lambda *args: True
|
||||
|
||||
if not hasattr(QtGui.QVBoxLayout, 'setContentsMargins'):
|
||||
QtGui.QVBoxLayout.setContentsMargins = lambda *args: True
|
||||
|
||||
if not hasattr(QtGui.QKeySequence, 'Preferences'):
|
||||
QtGui.QKeySequence.Preferences = 'Ctrl+O'
|
||||
|
||||
|
||||
def add_search_path(prefix, path):
|
||||
if hasattr(QtCore.QDir, 'addSearchPath'):
|
||||
QtCore.QDir.addSearchPath(prefix, path)
|
||||
|
||||
def set_common_dock_options(window):
|
||||
if not hasattr(window, 'setDockOptions'):
|
||||
return
|
||||
nested = QtGui.QMainWindow.AllowNestedDocks
|
||||
tabbed = QtGui.QMainWindow.AllowTabbedDocks
|
||||
animated = QtGui.QMainWindow.AnimatedDocks
|
||||
window.setDockOptions(nested | tabbed | animated)
|
||||
+240
-55
@@ -3,23 +3,28 @@
|
||||
"""
|
||||
import os
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import Qt
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
import cola
|
||||
from cola import core
|
||||
from cola import gitcfg
|
||||
from cola import utils
|
||||
from cola import settings
|
||||
from cola import signals
|
||||
from cola import resources
|
||||
import cola.views.log
|
||||
from cola.compat import set
|
||||
from cola.decorators import memoize
|
||||
from cola.widgets.log import LogView
|
||||
|
||||
_logger = None
|
||||
|
||||
@memoize
|
||||
def logger():
|
||||
global _logger
|
||||
if not _logger:
|
||||
_logger = cola.views.log.LogView()
|
||||
cola.notifier().listen(signals.log_cmd, _logger.log)
|
||||
return _logger
|
||||
logview = LogView()
|
||||
cola.notifier().connect(signals.log_cmd, logview.log)
|
||||
return logview
|
||||
|
||||
|
||||
def log(status, output):
|
||||
@@ -29,10 +34,21 @@ def log(status, output):
|
||||
return
|
||||
cola.notifier().broadcast(signals.log_cmd, status, output)
|
||||
|
||||
|
||||
def emit(widget, signal, *args, **opts):
|
||||
"""Return a function that emits a signal"""
|
||||
def emitter(*local_args, **local_opts):
|
||||
if args or opts:
|
||||
widget.emit(SIGNAL(signal), *args, **opts)
|
||||
else:
|
||||
widget.emit(SIGNAL(signal), *local_args, **local_opts)
|
||||
return emitter
|
||||
|
||||
|
||||
def SLOT(signal, *args, **opts):
|
||||
"""
|
||||
Returns a callback that broadcasts a message over the notifier.
|
||||
|
||||
|
||||
If the caller of SLOT() provides args or opts then those are
|
||||
used instead of the ones provided by the invoker of the callback.
|
||||
|
||||
@@ -45,6 +61,22 @@ def SLOT(signal, *args, **opts):
|
||||
return broadcast
|
||||
|
||||
|
||||
def connect_button(button, callback):
|
||||
button.connect(button, SIGNAL('clicked()'), callback)
|
||||
|
||||
|
||||
def relay_button(button, signal):
|
||||
connect_button(button, SLOT(signal))
|
||||
|
||||
|
||||
def relay_signal(parent, child, signal):
|
||||
"""Relay a signal from the child widget through the parent"""
|
||||
def relay_slot(*args, **opts):
|
||||
parent.emit(signal, *args, **opts)
|
||||
parent.connect(child, signal, relay_slot)
|
||||
return relay_slot
|
||||
|
||||
|
||||
def prompt(msg, title=None):
|
||||
"""Presents the user with an input widget and returns the input."""
|
||||
if title is None:
|
||||
@@ -55,6 +87,7 @@ def prompt(msg, title=None):
|
||||
result = QtGui.QInputDialog.getText(parent, msg, title)
|
||||
return (unicode(result[0]), result[1])
|
||||
|
||||
|
||||
def create_listwidget_item(text, filename):
|
||||
"""Creates a QListWidgetItem with text and the icon at filename."""
|
||||
item = QtGui.QListWidgetItem()
|
||||
@@ -62,28 +95,69 @@ def create_listwidget_item(text, filename):
|
||||
item.setText(text)
|
||||
return item
|
||||
|
||||
_tree_icon_cache = {}
|
||||
|
||||
def create_treewidget_item(text, filename):
|
||||
"""Creates a QTreeWidgetItem with text and the icon at filename."""
|
||||
if filename not in _tree_icon_cache:
|
||||
_tree_icon_cache[filename] = QtGui.QIcon(filename)
|
||||
icon = _tree_icon_cache[filename]
|
||||
icon = cached_icon_from_path(filename)
|
||||
item = QtGui.QTreeWidgetItem()
|
||||
item.setIcon(0, icon)
|
||||
item.setText(0, text)
|
||||
return item
|
||||
|
||||
def information(title, message=None):
|
||||
"""Launches a QMessageBox information with the
|
||||
provided title and message."""
|
||||
|
||||
@memoize
|
||||
def cached_icon_from_path(filename):
|
||||
return QtGui.QIcon(filename)
|
||||
|
||||
|
||||
def information(title, message=None, parent=None, details=None, informative_text=None):
|
||||
"""Show information with the provided title and message."""
|
||||
if message is None:
|
||||
message = title
|
||||
title = tr(title)
|
||||
message = tr(message)
|
||||
if parent is None:
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
mbox = QtGui.QMessageBox(parent)
|
||||
mbox.setStandardButtons(QtGui.QMessageBox.Close)
|
||||
mbox.setDefaultButton(QtGui.QMessageBox.Close)
|
||||
mbox.setWindowTitle(title)
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModal)
|
||||
mbox.setTextFormat(QtCore.Qt.PlainText)
|
||||
mbox.setText(message)
|
||||
if informative_text:
|
||||
mbox.setInformativeText(tr(informative_text))
|
||||
if details:
|
||||
mbox.setDetailedText(details)
|
||||
# Render git.svg into a 1-inch wide pixmap
|
||||
pixmap = QtGui.QPixmap(resources.icon('git.svg'))
|
||||
xres = pixmap.physicalDpiX()
|
||||
pixmap = pixmap.scaledToHeight(xres, QtCore.Qt.SmoothTransformation)
|
||||
mbox.setIconPixmap(pixmap)
|
||||
mbox.exec_()
|
||||
|
||||
|
||||
def critical(title, message=None, details=None):
|
||||
"""Show a warning with the provided title and message."""
|
||||
if message is None:
|
||||
message = title
|
||||
title = tr(title)
|
||||
message = tr(message)
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
QtGui.QMessageBox.information(parent, title, message)
|
||||
mbox = QtGui.QMessageBox(parent)
|
||||
mbox.setWindowTitle(title)
|
||||
mbox.setTextFormat(QtCore.Qt.PlainText)
|
||||
mbox.setText(message)
|
||||
mbox.setIcon(QtGui.QMessageBox.Critical)
|
||||
mbox.setStandardButtons(QtGui.QMessageBox.Close)
|
||||
mbox.setDefaultButton(QtGui.QMessageBox.Close)
|
||||
if details:
|
||||
mbox.setDetailedText(details)
|
||||
mbox.exec_()
|
||||
|
||||
# Register globally with the notifier
|
||||
cola.notifier().listen(signals.information, information)
|
||||
cola.notifier().connect(signals.information, information)
|
||||
|
||||
|
||||
def selected_treeitem(tree_widget):
|
||||
"""Returns a(id_number, is_selected) for a QTreeWidget."""
|
||||
@@ -95,12 +169,15 @@ def selected_treeitem(tree_widget):
|
||||
selected = True
|
||||
return(id_number, selected)
|
||||
|
||||
|
||||
def selected_row(list_widget):
|
||||
"""Returns a(row_number, is_selected) tuple for a QListWidget."""
|
||||
row = list_widget.currentRow()
|
||||
item = list_widget.item(row)
|
||||
selected = item is not None and item.isSelected()
|
||||
return(row, selected)
|
||||
items = list_widget.selectedItems()
|
||||
if not items:
|
||||
return (-1, False)
|
||||
item = items[0]
|
||||
return (list_widget.row(item), True)
|
||||
|
||||
|
||||
def selection_list(listwidget, items):
|
||||
"""Returns an array of model items that correspond to
|
||||
@@ -114,6 +191,7 @@ def selection_list(listwidget, items):
|
||||
selected.append(item)
|
||||
return selected
|
||||
|
||||
|
||||
def tree_selection(treeitem, items):
|
||||
"""Returns model items that correspond to selected widget indices"""
|
||||
itemcount = treeitem.childCount()
|
||||
@@ -125,20 +203,27 @@ def tree_selection(treeitem, items):
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
def selected_item(list_widget, items):
|
||||
"""Returns the selected item in a QListWidget."""
|
||||
row, selected = selected_row(list_widget)
|
||||
if selected and row < len(items):
|
||||
widget_items = list_widget.selectedItems()
|
||||
if not widget_items:
|
||||
return None
|
||||
widget_item = widget_items[0]
|
||||
row = list_widget.row(widget_item)
|
||||
if row < len(items):
|
||||
return items[row]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def open_dialog(parent, title, filename=None):
|
||||
"""Creates an Open File dialog and returns a filename."""
|
||||
title_tr = tr(title)
|
||||
return unicode(QtGui.QFileDialog
|
||||
.getOpenFileName(parent, title_tr, filename))
|
||||
|
||||
|
||||
def opendir_dialog(parent, title, path):
|
||||
"""Prompts for a directory path"""
|
||||
|
||||
@@ -156,10 +241,12 @@ def save_dialog(parent, title, filename=''):
|
||||
return unicode(QtGui.QFileDialog
|
||||
.getSaveFileName(parent, title_tr, filename))
|
||||
|
||||
|
||||
def icon(basename):
|
||||
"""Given a basename returns a QIcon from the corresponding cola icon."""
|
||||
return QtGui.QIcon(resources.icon(basename))
|
||||
|
||||
|
||||
def question(parent, title, message, default=True):
|
||||
"""Launches a QMessageBox question with the provided title and message.
|
||||
Passing "default=False" will make "No" the default choice."""
|
||||
@@ -176,6 +263,22 @@ def question(parent, title, message, default=True):
|
||||
buttons, default)
|
||||
return result == QtGui.QMessageBox.Yes
|
||||
|
||||
|
||||
def confirm(parent, title, text, informative_text, ok_text, icon=None):
|
||||
"""Confirm that an action should take place"""
|
||||
if icon is None:
|
||||
icon = ok_icon()
|
||||
msgbox = QtGui.QMessageBox(parent)
|
||||
msgbox.setWindowTitle(tr(title))
|
||||
msgbox.setText(tr(text))
|
||||
msgbox.setInformativeText(tr(informative_text))
|
||||
ok = msgbox.addButton(tr(ok_text), QtGui.QMessageBox.ActionRole)
|
||||
ok.setIcon(icon)
|
||||
msgbox.addButton(QtGui.QMessageBox.Cancel)
|
||||
msgbox.exec_()
|
||||
return msgbox.clickedButton() == ok
|
||||
|
||||
|
||||
def set_clipboard(text):
|
||||
"""Sets the copy/paste buffer to text."""
|
||||
if not text:
|
||||
@@ -184,6 +287,18 @@ def set_clipboard(text):
|
||||
clipboard.setText(text, QtGui.QClipboard.Clipboard)
|
||||
clipboard.setText(text, QtGui.QClipboard.Selection)
|
||||
|
||||
|
||||
def add_action(widget, text, fn, *shortcuts):
|
||||
action = QtGui.QAction(text, widget)
|
||||
action.connect(action, SIGNAL('triggered()'), fn)
|
||||
if shortcuts:
|
||||
shortcuts = list(set(shortcuts))
|
||||
action.setShortcuts(shortcuts)
|
||||
action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
|
||||
widget.addAction(action)
|
||||
return action
|
||||
|
||||
|
||||
def set_selected_item(widget, idx):
|
||||
"""Sets a the currently selected item to the item at index idx."""
|
||||
if type(widget) is QtGui.QTreeWidget:
|
||||
@@ -192,20 +307,28 @@ def set_selected_item(widget, idx):
|
||||
widget.setItemSelected(item, True)
|
||||
widget.setCurrentItem(item)
|
||||
|
||||
|
||||
def add_items(widget, items):
|
||||
"""Adds items to a widget."""
|
||||
for item in items:
|
||||
widget.addItem(item)
|
||||
|
||||
|
||||
def set_items(widget, items):
|
||||
"""Clear the existing widget contents and set the new items."""
|
||||
widget.clear()
|
||||
add_items(widget, items)
|
||||
|
||||
|
||||
@memoize
|
||||
def tr(txt):
|
||||
"""Translate a string into a local language."""
|
||||
if type(txt) is QtCore.QString:
|
||||
# This has already been translated; leave as-is
|
||||
return unicode(txt)
|
||||
return unicode(QtGui.QApplication.instance().translate('', txt))
|
||||
|
||||
|
||||
def icon_file(filename, staged=False, untracked=False):
|
||||
"""Returns a file path representing a corresponding file path."""
|
||||
if staged:
|
||||
@@ -216,14 +339,16 @@ def icon_file(filename, staged=False, untracked=False):
|
||||
elif untracked:
|
||||
ifile = resources.icon('untracked.png')
|
||||
else:
|
||||
ifile = utils.file_icon(filename)
|
||||
ifile = utils.file_icon(core.encode(filename))
|
||||
return ifile
|
||||
|
||||
|
||||
def icon_for_file(filename, staged=False, untracked=False):
|
||||
"""Returns a QIcon for a particular file path."""
|
||||
ifile = icon_file(filename, staged=staged, untracked=untracked)
|
||||
return icon(ifile)
|
||||
|
||||
|
||||
def create_treeitem(filename, staged=False, untracked=False, check=True):
|
||||
"""Given a filename, return a QListWidgetItem suitable
|
||||
for adding to a QListWidget. "staged" and "untracked"
|
||||
@@ -236,7 +361,7 @@ def create_treeitem(filename, staged=False, untracked=False, check=True):
|
||||
|
||||
|
||||
def update_file_icons(widget, items, staged=True,
|
||||
untracked=False, offset=0):
|
||||
untracked=False, offset=0):
|
||||
"""Populate a QListWidget with custom icon items."""
|
||||
for idx, model_item in enumerate(items):
|
||||
item = widget.item(idx+offset)
|
||||
@@ -248,44 +373,104 @@ def set_listwidget_strings(widget, items):
|
||||
widget.clear()
|
||||
add_items(widget, [ QtGui.QListWidgetItem(i) for i in items ])
|
||||
|
||||
_icon_cache = {}
|
||||
@memoize
|
||||
def cached_icon(key):
|
||||
"""Maintain a cache of standard icons and return cache entries."""
|
||||
if key not in _icon_cache:
|
||||
style = QtGui.QApplication.instance().style()
|
||||
_icon_cache[key] = style.standardIcon(key)
|
||||
return _icon_cache[key]
|
||||
style = QtGui.QApplication.instance().style()
|
||||
return style.standardIcon(key)
|
||||
|
||||
|
||||
def dir_icon():
|
||||
"""Return a standard icon for a directory."""
|
||||
return cached_icon(QtGui.QStyle.SP_DirIcon)
|
||||
|
||||
|
||||
def file_icon():
|
||||
"""Return a standard icon for a file."""
|
||||
return cached_icon(QtGui.QStyle.SP_FileIcon)
|
||||
|
||||
def diff_font():
|
||||
"""Return the diff font string."""
|
||||
qfont = QtGui.QFont()
|
||||
font = cola.model().cola_config('fontdiff')
|
||||
if font and qfont.fromString(font):
|
||||
return qfont
|
||||
family = 'Monospace'
|
||||
if cola.utils.is_darwin():
|
||||
family = 'Monaco'
|
||||
qfont.setFamily(family)
|
||||
font = unicode(qfont.toString())
|
||||
# TODO this might not be the best place for the default
|
||||
cola.model().set_diff_font(font)
|
||||
return qfont
|
||||
|
||||
def set_diff_font(widget):
|
||||
"""Updates the diff font based on the configured value."""
|
||||
qfont = diff_font()
|
||||
block = widget.signalsBlocked()
|
||||
widget.blockSignals(True)
|
||||
if isinstance(widget, QtGui.QFontComboBox):
|
||||
widget.setCurrentFont(qfont)
|
||||
else:
|
||||
widget.setFont(qfont)
|
||||
widget.blockSignals(block)
|
||||
def apply_icon():
|
||||
"""Return a standard Apply icon"""
|
||||
return cached_icon(QtGui.QStyle.SP_DialogApplyButton)
|
||||
|
||||
|
||||
def save_icon():
|
||||
"""Return a standard Save icon"""
|
||||
return cached_icon(QtGui.QStyle.SP_DialogSaveButton)
|
||||
|
||||
|
||||
def ok_icon():
|
||||
"""Return a standard Ok icon"""
|
||||
return cached_icon(QtGui.QStyle.SP_DialogOkButton)
|
||||
|
||||
|
||||
def discard_icon():
|
||||
"""Return a standard Discard icon"""
|
||||
return cached_icon(QtGui.QStyle.SP_DialogDiscardButton)
|
||||
|
||||
|
||||
def close_icon():
|
||||
"""Return a standard Close icon"""
|
||||
return cached_icon(QtGui.QStyle.SP_DialogCloseButton)
|
||||
|
||||
|
||||
def add_close_action(widget):
|
||||
"""Adds close action and shortcuts to a widget."""
|
||||
return add_action(widget, 'Close...',
|
||||
widget.close, QtGui.QKeySequence.Close, 'Ctrl+Q')
|
||||
|
||||
|
||||
def center_on_screen(widget):
|
||||
"""Move widget to the center of the default screen"""
|
||||
desktop = QtGui.QApplication.instance().desktop()
|
||||
rect = desktop.screenGeometry(QtGui.QCursor().pos())
|
||||
cy = rect.height()/2
|
||||
cx = rect.width()/2
|
||||
widget.move(cx - widget.width()/2, cy - widget.height()/2)
|
||||
|
||||
|
||||
def save_state(widget):
|
||||
if gitcfg.instance().get('cola.savewindowsettings', True):
|
||||
settings.SettingsManager.save_gui_state(widget)
|
||||
|
||||
|
||||
def export_window_state(widget, state, version):
|
||||
# Save the window state
|
||||
windowstate = widget.saveState(version)
|
||||
state['windowstate'] = unicode(windowstate.toBase64().data())
|
||||
return state
|
||||
|
||||
|
||||
def apply_window_state(widget, state, version):
|
||||
# Restore the dockwidget, etc. window state
|
||||
try:
|
||||
windowstate = state['windowstate']
|
||||
widget.restoreState(QtCore.QByteArray.fromBase64(str(windowstate)),
|
||||
version)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def apply_state(widget):
|
||||
state = settings.SettingsManager.gui_state(widget)
|
||||
widget.apply_state(state)
|
||||
return bool(state)
|
||||
|
||||
|
||||
@memoize
|
||||
def theme_icon(name):
|
||||
"""Grab an icon from the current theme with a fallback
|
||||
|
||||
Support older versions of Qt by catching AttributeError and
|
||||
falling back to our default icons.
|
||||
|
||||
"""
|
||||
try:
|
||||
base, ext = os.path.splitext(name)
|
||||
qicon = QtGui.QIcon.fromTheme(base)
|
||||
if not qicon.isNull():
|
||||
return qicon
|
||||
except AttributeError:
|
||||
pass
|
||||
return icon(name)
|
||||
|
||||
+20
-25
@@ -13,42 +13,37 @@ else:
|
||||
# __file__ = '$prefix/cola/__file__.py'
|
||||
_prefix = os.path.dirname(os.path.dirname(_modpath))
|
||||
|
||||
|
||||
def prefix(*args):
|
||||
"""Returns a path relative to cola's installation prefix"""
|
||||
"""Return a path relative to cola's installation prefix"""
|
||||
return os.path.join(_prefix, *args)
|
||||
|
||||
|
||||
def doc(*args):
|
||||
"""Returns a path relative to cola's /usr/share/doc/ directory"""
|
||||
"""Return a path relative to cola's /usr/share/doc/ directory"""
|
||||
return os.path.join(_prefix, 'share', 'doc', 'git-cola', *args)
|
||||
|
||||
|
||||
def html_docs():
|
||||
"""Returns the path to the cola html documentation."""
|
||||
return doc('git-cola.html')
|
||||
"""Return the path to the cola html documentation."""
|
||||
# index.html only exists after the install-docs target is run,
|
||||
# so fallback to git-cola.txt.
|
||||
htmldocs = doc('html', 'index.html')
|
||||
if os.path.exists(htmldocs):
|
||||
return htmldocs
|
||||
return doc('git-cola.txt')
|
||||
|
||||
|
||||
def share(*args):
|
||||
"""Returns a path relative to cola's /usr/share/ directory"""
|
||||
"""Return a path relative to cola's /usr/share/ directory"""
|
||||
return prefix('share', 'git-cola', *args)
|
||||
|
||||
|
||||
def icon(basename):
|
||||
"""Returns the full path to an icon file given a basename."""
|
||||
return share('icons', basename)
|
||||
"""Return the full path to an icon file given a basename."""
|
||||
return 'icons:'+basename
|
||||
|
||||
def qm(name):
|
||||
"""Returns the path to a qm file given its name"""
|
||||
return share('qm', name + '.qm')
|
||||
|
||||
def stylesheet(name):
|
||||
"""Returns a path relative to cola's /usr/share/../styles directory"""
|
||||
stylesheet = share('styles', name + '.qss')
|
||||
if os.path.exists(stylesheet):
|
||||
return stylesheet
|
||||
else:
|
||||
return None
|
||||
|
||||
def style_dir():
|
||||
"""Returns the path to the style dir within the cola install tree."""
|
||||
return share('styles')
|
||||
|
||||
def resource_dirs(path):
|
||||
"""Returns directories beneath a specific path"""
|
||||
return [p for p in glob.glob(os.path.join(path, '*')) if os.path.isdir(p)]
|
||||
def icon_dir():
|
||||
"""Return the path to the style dir within the cola install tree."""
|
||||
return share('icons')
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Provides a serializer for arbitrary Python objects"""
|
||||
import jsonpickle
|
||||
jsonpickle.set_encoder_options('simplejson', indent=4)
|
||||
|
||||
from cola import utils
|
||||
|
||||
handlers = {}
|
||||
|
||||
|
||||
def save(obj, path):
|
||||
utils.write(path, encode(obj))
|
||||
|
||||
|
||||
def load(path):
|
||||
return decode(utils.slurp(path))
|
||||
|
||||
|
||||
def clone(obj):
|
||||
# Go in and out of encode/decode to return a clone
|
||||
return decode(encode(obj))
|
||||
|
||||
|
||||
def encode(obj):
|
||||
handler = _gethandler(obj)
|
||||
if handler:
|
||||
handler.pre_encode_hook()
|
||||
jsonstr = jsonpickle.encode(obj)
|
||||
if handler:
|
||||
handler.post_encode_hook()
|
||||
return jsonstr
|
||||
|
||||
|
||||
def decode(jsonstr):
|
||||
obj = jsonpickle.decode(jsonstr)
|
||||
handler = _gethandler(obj)
|
||||
if handler:
|
||||
handler.post_decode_hook()
|
||||
return obj
|
||||
|
||||
|
||||
def _gethandler(obj):
|
||||
cls = type(obj)
|
||||
# Allow base classes to provide a serialization handlers
|
||||
# for their subclasses
|
||||
if hasattr(cls, 'mro'):
|
||||
for supercls in cls.mro():
|
||||
if supercls in handlers:
|
||||
return handlers[supercls](obj)
|
||||
if cls in handlers:
|
||||
return handlers[cls](obj)
|
||||
return None
|
||||
+23
-20
@@ -3,9 +3,11 @@
|
||||
"""
|
||||
|
||||
import os
|
||||
import user
|
||||
|
||||
from cola.models.observable import ObservableModel
|
||||
from cola import core
|
||||
from cola import serializer
|
||||
from cola.obsmodel import ObservableModel
|
||||
|
||||
|
||||
class SettingsModel(ObservableModel):
|
||||
def __init__(self):
|
||||
@@ -13,22 +15,6 @@ class SettingsModel(ObservableModel):
|
||||
ObservableModel.__init__(self)
|
||||
self.bookmarks = []
|
||||
self.gui_state = {}
|
||||
self.load()
|
||||
|
||||
def path(self):
|
||||
"""Path to the model's on-disk representation"""
|
||||
return os.path.join(user.home, '.cola')
|
||||
|
||||
def load(self):
|
||||
"""Loads settings if they exist"""
|
||||
settings = self.path()
|
||||
if os.path.exists(settings):
|
||||
ObservableModel.load(self, settings)
|
||||
|
||||
def save(self):
|
||||
"""Saves settings to the .cola file"""
|
||||
# Call the base method
|
||||
ObservableModel.save(self, self.path())
|
||||
|
||||
def add_bookmark(self, bookmark):
|
||||
"""Adds a bookmark to the saved settings"""
|
||||
@@ -40,14 +26,26 @@ class SettingsModel(ObservableModel):
|
||||
if bookmark in self.bookmarks:
|
||||
self.bookmarks.remove(bookmark)
|
||||
|
||||
|
||||
class SettingsManager(object):
|
||||
"""Manages a SettingsModel singleton
|
||||
"""
|
||||
_settings = SettingsModel()
|
||||
_settings = None
|
||||
|
||||
# Here we store settings
|
||||
_rcfile = os.path.join(core.decode(os.path.expanduser('~')), '.cola')
|
||||
|
||||
@staticmethod
|
||||
def settings():
|
||||
"""Returns the SettingsModel singleton"""
|
||||
if not SettingsManager._settings:
|
||||
if os.path.exists(SettingsManager._rcfile):
|
||||
try:
|
||||
SettingsManager._settings = serializer.load(SettingsManager._rcfile)
|
||||
except: # bad json
|
||||
SettingsManager._settings = SettingsModel()
|
||||
else:
|
||||
SettingsManager._settings = SettingsModel()
|
||||
return SettingsManager._settings
|
||||
|
||||
@staticmethod
|
||||
@@ -57,9 +55,14 @@ class SettingsManager(object):
|
||||
state = gui.export_state()
|
||||
model = SettingsManager.settings()
|
||||
model.gui_state[name] = state
|
||||
model.save()
|
||||
SettingsManager.save()
|
||||
|
||||
@staticmethod
|
||||
def gui_state(gui):
|
||||
"""Returns the state for a gui"""
|
||||
return SettingsManager.settings().gui_state.get(gui.name(), {})
|
||||
|
||||
@staticmethod
|
||||
def save():
|
||||
model = SettingsManager.settings()
|
||||
serializer.save(model, SettingsManager._rcfile)
|
||||
|
||||
+62
-66
@@ -1,66 +1,62 @@
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
_signals = dict(add_signoff = SIGNAL('add_signoff'),
|
||||
amend = SIGNAL('amend'),
|
||||
amend_mode = SIGNAL('amend_mode'),
|
||||
apply_diff_selection = SIGNAL('apply_diff_selection'),
|
||||
branch_mode = SIGNAL('branch_mode'),
|
||||
commit = SIGNAL('commit'),
|
||||
edit = SIGNAL('edit'),
|
||||
checkout = SIGNAL('checkout'),
|
||||
checkout_branch = SIGNAL('checkout_branch'),
|
||||
cherry_pick = SIGNAL('cherry_pick'),
|
||||
clone = SIGNAL('clone'),
|
||||
delete = SIGNAL('delete'),
|
||||
delete_branch = SIGNAL('delete_branch'),
|
||||
diff = SIGNAL('diff'),
|
||||
diff_expr_mode = SIGNAL('diff_expr_mode'),
|
||||
diff_mode = SIGNAL('diff_mode'),
|
||||
diff_staged = SIGNAL('diff_staged'),
|
||||
diff_text = SIGNAL('diff_text'),
|
||||
diffstat = SIGNAL('diffstat'),
|
||||
difftool = SIGNAL('difftool'),
|
||||
editor_text = SIGNAL('editor_text'),
|
||||
format_patch = SIGNAL('format_patch'),
|
||||
grep = SIGNAL('grep'),
|
||||
information = SIGNAL('information'),
|
||||
inotify = SIGNAL('inotify'),
|
||||
log_cmd = SIGNAL('log_cmd'),
|
||||
load_commit_message = SIGNAL('load_commit_message'),
|
||||
mergetool = SIGNAL('mergetool'),
|
||||
mode = SIGNAL('mode'),
|
||||
modified_summary = SIGNAL('modified_summary'),
|
||||
open_repo = SIGNAL('open_repo'),
|
||||
redo = SIGNAL('redo'),
|
||||
rescan = SIGNAL('rescan'),
|
||||
reset_mode = SIGNAL('reset_mode'),
|
||||
review_branch_mode = SIGNAL('review_branch_mode'),
|
||||
show_untracked = SIGNAL('show_untracked'),
|
||||
stage = SIGNAL('stage'),
|
||||
stage_diffs = SIGNAL('stage_diffs'),
|
||||
stage_modified = SIGNAL('stage_modified'),
|
||||
stage_untracked = SIGNAL('stage_untracked'),
|
||||
staged_summary = SIGNAL('staged_summary'),
|
||||
tag = SIGNAL('tag'),
|
||||
unmerged_summary = SIGNAL('unmerged_summary'),
|
||||
undo = SIGNAL('undo'),
|
||||
undo_diffs = SIGNAL('undo_diffs'),
|
||||
unstage = SIGNAL('unstage'),
|
||||
unstage_diffs = SIGNAL('unstage_diffs'),
|
||||
unstage_all = SIGNAL('unstage_all'),
|
||||
unstage_selected = SIGNAL('unstage_selected'),
|
||||
untracked_summary = SIGNAL('untracked_summary'),
|
||||
visualize_all = SIGNAL('visualize_all'),
|
||||
visualize_current = SIGNAL('visualize_current'),
|
||||
visualize_paths = SIGNAL('visualize_paths'))
|
||||
|
||||
_signals_names = {}
|
||||
for name, signal in _signals.iteritems():
|
||||
_signals_names[signal] = name
|
||||
|
||||
# Bring signals into the module namespace
|
||||
globals().update(_signals)
|
||||
|
||||
def name(signal):
|
||||
"""Return the name for a signal."""
|
||||
return _signals_names.get(signal, 'no-name')
|
||||
add_signoff = 'add_signoff'
|
||||
amend = 'amend'
|
||||
amend_mode = 'amend_mode'
|
||||
apply_diff_selection = 'apply_diff_selection'
|
||||
apply_patches = 'apply_patches'
|
||||
branch_mode = 'branch_mode'
|
||||
commit = 'commit'
|
||||
commits_selected = 'commits_selected'
|
||||
edit = 'edit'
|
||||
checkout = 'checkout'
|
||||
checkout_branch = 'checkout_branch'
|
||||
cherry_pick = 'cherry_pick'
|
||||
clone = 'clone'
|
||||
delete = 'delete'
|
||||
ignore = 'ignore'
|
||||
delete_branch = 'delete_branch'
|
||||
diff = 'diff'
|
||||
diff_expr_mode = 'diff_expr_mode'
|
||||
diff_mode = 'diff_mode'
|
||||
diff_staged = 'diff_staged'
|
||||
diffstat = 'diffstat'
|
||||
difftool = 'difftool'
|
||||
format_patch = 'format_patch'
|
||||
grep = 'grep'
|
||||
information = 'information'
|
||||
log_cmd = 'log_cmd'
|
||||
load_commit_message = 'load_commit_message'
|
||||
load_commit_template = 'load_commit_template'
|
||||
load_previous_message = 'load_previous_message'
|
||||
mergetool = 'mergetool'
|
||||
mode = 'mode'
|
||||
modified_summary = 'modified_summary'
|
||||
open_repo = 'open_repo'
|
||||
question = 'question'
|
||||
redo = 'redo'
|
||||
rescan = 'rescan'
|
||||
reset_mode = 'reset_mode'
|
||||
review_branch_mode = 'review_branch_mode'
|
||||
run_config_action = 'run_config_action'
|
||||
run_command = 'run_command'
|
||||
set_diff_text = 'set_diff_text'
|
||||
show_untracked = 'show_untracked'
|
||||
signoff = 'signoff'
|
||||
stage = 'stage'
|
||||
stage_diffs = 'stage_diffs'
|
||||
stage_modified = 'stage_modified'
|
||||
stage_untracked = 'stage_untracked'
|
||||
staged_summary = 'staged_summary'
|
||||
tag = 'tag'
|
||||
unmerged_summary = 'unmerged_summary'
|
||||
undo = 'undo'
|
||||
undo_diffs = 'undo_diffs'
|
||||
unstage = 'unstage'
|
||||
unstage_diffs = 'unstage_diffs'
|
||||
unstage_all = 'unstage_all'
|
||||
unstage_selected = 'unstage_selected'
|
||||
untracked_summary = 'untracked_summary'
|
||||
update_status = 'update_status'
|
||||
visualize_all = 'visualize_all'
|
||||
visualize_current = 'visualize_current'
|
||||
visualize_paths = 'visualize_paths'
|
||||
visualize_revision = 'visualize_revision'
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from PyQt4 import QtGui
|
||||
|
||||
from cola.stash.controller import StashController
|
||||
from cola.stash.model import StashModel
|
||||
from cola.stash.view import StashView
|
||||
|
||||
|
||||
def stash(parent=None):
|
||||
"""Launches a stash dialog using the provided model + view
|
||||
"""
|
||||
if parent is None:
|
||||
parent = QtGui.QApplication.instance().activeWindow()
|
||||
model = StashModel()
|
||||
view = StashView(model, parent)
|
||||
ctl = StashController(model, view)
|
||||
view.show()
|
||||
view.raise_()
|
||||
return ctl
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from cola.app import ColaApplication
|
||||
import cola
|
||||
|
||||
cola.model().update_status()
|
||||
app = ColaApplication([])
|
||||
ctl = stash()
|
||||
app.exec_()
|
||||
@@ -0,0 +1,9 @@
|
||||
"""This controller handles the stash dialog."""
|
||||
|
||||
from cola.ctrl import Controller
|
||||
from cola.stash.model import command_directory
|
||||
|
||||
class StashController(Controller):
|
||||
def __init__(self, model, view):
|
||||
Controller.__init__(self, model, view)
|
||||
self.add_commands(command_directory)
|
||||
@@ -0,0 +1,83 @@
|
||||
import cola
|
||||
from cola import observable
|
||||
from cola import signals
|
||||
from cola.git import git
|
||||
from cola.cmds import BaseCommand, Rescan, rescan
|
||||
|
||||
apply_stash = 'apply_stash'
|
||||
drop_stash = 'drop_stash'
|
||||
save_stash = 'save_stash'
|
||||
|
||||
|
||||
class StashModel(observable.Observable):
|
||||
def __init__(self):
|
||||
observable.Observable.__init__(self)
|
||||
|
||||
def stash_list(self):
|
||||
return git.stash('list').splitlines()
|
||||
|
||||
def has_stashable_changes(self):
|
||||
model = cola.model()
|
||||
return bool(model.modified + model.staged)
|
||||
|
||||
def stash_info(self, revids=False, names=False):
|
||||
"""Parses "git stash list" and returns a list of stashes."""
|
||||
stashes = self.stash_list()
|
||||
revids = [s[:s.index(':')] for s in stashes]
|
||||
names = [s.split(': ', 2)[-1] for s in stashes]
|
||||
|
||||
return stashes, revids, names
|
||||
|
||||
def stash_diff(self, rev):
|
||||
diffstat = git.stash('show', rev)
|
||||
diff = git.stash('show', '-p', rev)
|
||||
return diffstat + '\n\n' + diff
|
||||
|
||||
|
||||
class ApplyStash(BaseCommand):
|
||||
def __init__(self, selection, index):
|
||||
BaseCommand.__init__(self)
|
||||
self.selection = selection
|
||||
self.index = index
|
||||
|
||||
def do(self):
|
||||
if self.index:
|
||||
args = ['apply', '--index', self.selection]
|
||||
else:
|
||||
args = ['apply', self.selection]
|
||||
status, output = git.stash(with_stderr=True, with_status=True, *args)
|
||||
cola.notifier().broadcast(signals.log_cmd, status, output)
|
||||
|
||||
|
||||
class DropStash(BaseCommand):
|
||||
def __init__(self, stash_sha1):
|
||||
BaseCommand.__init__(self)
|
||||
self.stash_sha1 = stash_sha1
|
||||
|
||||
def do(self):
|
||||
status, output = git.stash('drop', self.stash_sha1,
|
||||
with_stderr=True, with_status=True)
|
||||
cola.notifier().broadcast(signals.log_cmd, status, output)
|
||||
|
||||
|
||||
class SaveStash(BaseCommand):
|
||||
def __init__(self, stash_name, keep_index):
|
||||
BaseCommand.__init__(self)
|
||||
self.stash_name = stash_name
|
||||
self.keep_index = keep_index
|
||||
|
||||
def do(self):
|
||||
if self.keep_index:
|
||||
args = ['save', '--keep-index', self.stash_name]
|
||||
else:
|
||||
args = ['save', self.stash_name]
|
||||
status, output = git.stash(with_stderr=True, with_status=True, *args)
|
||||
cola.notifier().broadcast(signals.log_cmd, status, output)
|
||||
|
||||
|
||||
command_directory = {
|
||||
apply_stash: ApplyStash,
|
||||
drop_stash: DropStash,
|
||||
rescan: Rescan,
|
||||
save_stash: SaveStash,
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
"""Provides the StashView dialog."""
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola import qt
|
||||
from cola import qtutils
|
||||
from cola import utils
|
||||
from cola.stash.model import save_stash, apply_stash, drop_stash, rescan
|
||||
from cola.views import standard
|
||||
from cola.widgets.diff import DiffView
|
||||
|
||||
|
||||
class StashView(standard.Dialog):
|
||||
def __init__(self, model, parent=None):
|
||||
standard.Dialog.__init__(self, parent=parent)
|
||||
self.model = model
|
||||
self.stashes = []
|
||||
self.revids = []
|
||||
self.names = []
|
||||
|
||||
self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.setWindowTitle(self.tr('Stash'))
|
||||
if parent:
|
||||
self.resize(parent.width(), 420)
|
||||
else:
|
||||
self.resize(700, 420)
|
||||
|
||||
self.stash_list = QtGui.QListWidget(self)
|
||||
self.stash_view = DiffView(self)
|
||||
|
||||
self.button_apply =\
|
||||
self.toolbutton(self.tr('Apply'),
|
||||
self.tr('Apply the selected stash'),
|
||||
qtutils.apply_icon())
|
||||
self.button_save =\
|
||||
self.toolbutton(self.tr('Save'),
|
||||
self.tr('Save modified state to new stash'),
|
||||
qtutils.save_icon())
|
||||
self.button_remove = \
|
||||
self.toolbutton(self.tr('Remove'),
|
||||
self.tr('Remove the selected stash'),
|
||||
qtutils.discard_icon())
|
||||
self.button_close = \
|
||||
self.pushbutton(self.tr('Close'),
|
||||
self.tr('Close'), qtutils.close_icon())
|
||||
|
||||
self.keep_index = QtGui.QCheckBox(self)
|
||||
self.keep_index.setText(self.tr('Keep Index'))
|
||||
self.keep_index.setChecked(True)
|
||||
|
||||
self.setTabOrder(self.button_save, self.button_apply)
|
||||
self.setTabOrder(self.button_apply, self.button_remove)
|
||||
self.setTabOrder(self.button_remove, self.keep_index)
|
||||
self.setTabOrder(self.keep_index, self.button_close)
|
||||
|
||||
# Arrange layouts
|
||||
self.main_layt = QtGui.QVBoxLayout()
|
||||
self.main_layt.setMargin(6)
|
||||
self.main_layt.setSpacing(6)
|
||||
|
||||
self.btn_layt = QtGui.QHBoxLayout()
|
||||
self.btn_layt.setMargin(0)
|
||||
self.btn_layt.setSpacing(4)
|
||||
|
||||
self.splitter = QtGui.QSplitter()
|
||||
self.splitter.setOrientation(QtCore.Qt.Horizontal)
|
||||
self.splitter.setChildrenCollapsible(True)
|
||||
self.splitter.setStretchFactor(0, 1)
|
||||
self.splitter.setStretchFactor(1, 1)
|
||||
self.splitter.insertWidget(0, self.stash_list)
|
||||
self.splitter.insertWidget(1, self.stash_view)
|
||||
|
||||
self.btn_layt.addWidget(self.button_save)
|
||||
self.btn_layt.addWidget(self.button_apply)
|
||||
self.btn_layt.addWidget(self.button_remove)
|
||||
self.btn_layt.addWidget(self.keep_index)
|
||||
self.btn_layt.addStretch()
|
||||
self.btn_layt.addWidget(self.button_close)
|
||||
|
||||
self.main_layt.addWidget(self.splitter)
|
||||
self.main_layt.addLayout(self.btn_layt)
|
||||
self.setLayout(self.main_layt)
|
||||
|
||||
self.splitter.setSizes([self.width()/3, self.width()*2/3])
|
||||
|
||||
self.connect(self.button_apply, SIGNAL('clicked()'),
|
||||
self.stash_apply)
|
||||
self.connect(self.button_save, SIGNAL('clicked()'),
|
||||
self.stash_save)
|
||||
self.connect(self.button_remove, SIGNAL('clicked()'),
|
||||
self.stash_remove)
|
||||
self.connect(self.button_close, SIGNAL('clicked()'), self.close)
|
||||
|
||||
self.update_from_model()
|
||||
self.update_actions()
|
||||
|
||||
self.connect(self.stash_list, SIGNAL('itemSelectionChanged()'),
|
||||
self.item_selected)
|
||||
|
||||
def close(self):
|
||||
self.accept()
|
||||
self.emit(SIGNAL(rescan))
|
||||
|
||||
def toolbutton(self, text, tooltip, icon):
|
||||
return qt.create_toolbutton(self,
|
||||
text=text, tooltip=tooltip, icon=icon)
|
||||
|
||||
def pushbutton(self, text, tooltip, icon):
|
||||
btn = QtGui.QPushButton(self)
|
||||
btn.setText(self.tr(text))
|
||||
btn.setToolTip(self.tr(tooltip))
|
||||
btn.setIcon(icon)
|
||||
return btn
|
||||
|
||||
def selected_stash(self):
|
||||
"""Returns the stash name of the currently selected stash
|
||||
"""
|
||||
list_widget = self.stash_list
|
||||
stash_list = self.revids
|
||||
return qtutils.selected_item(list_widget, stash_list)
|
||||
|
||||
def selected_name(self):
|
||||
list_widget = self.stash_list
|
||||
stash_list = self.names
|
||||
return qtutils.selected_item(list_widget, stash_list)
|
||||
|
||||
def item_selected(self):
|
||||
"""Shows the current stash in the main view."""
|
||||
self.update_actions()
|
||||
selection = self.selected_stash()
|
||||
if not selection:
|
||||
return
|
||||
diff_text = self.model.stash_diff(selection)
|
||||
self.stash_view.setPlainText(diff_text)
|
||||
|
||||
def update_actions(self):
|
||||
has_changes = self.model.has_stashable_changes()
|
||||
has_stash = bool(self.selected_stash())
|
||||
self.button_save.setEnabled(has_changes)
|
||||
self.button_apply.setEnabled(has_stash)
|
||||
self.button_remove.setEnabled(has_stash)
|
||||
|
||||
def update_from_model(self):
|
||||
"""Initiates git queries on the model and updates the view
|
||||
"""
|
||||
stashes, revids, names = self.model.stash_info()
|
||||
self.stashes = stashes
|
||||
self.revids = revids
|
||||
self.names = names
|
||||
|
||||
self.stash_list.clear()
|
||||
self.stash_list.addItems(self.stashes)
|
||||
|
||||
def stash_apply(self):
|
||||
"""Applies the currently selected stash
|
||||
"""
|
||||
selection = self.selected_stash()
|
||||
if not selection:
|
||||
return
|
||||
index = self.keep_index.isChecked()
|
||||
self.emit(SIGNAL(apply_stash), selection, index)
|
||||
self.accept()
|
||||
self.emit(SIGNAL(rescan))
|
||||
|
||||
def stash_save(self):
|
||||
"""Saves the worktree in a stash
|
||||
|
||||
This prompts the user for a stash name and creates
|
||||
a git stash named accordingly.
|
||||
|
||||
"""
|
||||
stash_name, ok = qtutils.prompt('Save Stash',
|
||||
'Enter a name for the stash')
|
||||
if not ok or not stash_name:
|
||||
return
|
||||
# Sanitize the stash name
|
||||
stash_name = utils.sanitize(stash_name)
|
||||
if stash_name in self.names:
|
||||
qtutils.critical('Oops!',
|
||||
'A stash named "%s" already exists' % stash_name)
|
||||
return
|
||||
|
||||
keep_index = self.keep_index.isChecked()
|
||||
self.emit(SIGNAL(save_stash), stash_name, keep_index)
|
||||
self.accept()
|
||||
self.emit(SIGNAL(rescan))
|
||||
|
||||
def stash_remove(self):
|
||||
"""Drops the currently selected stash
|
||||
"""
|
||||
selection = self.selected_stash()
|
||||
name = self.selected_name()
|
||||
if not selection:
|
||||
return
|
||||
if not qtutils.confirm(self,
|
||||
'Remove Stash?',
|
||||
'Remove "%s"?' % name,
|
||||
'Recovering these changes may not be possible.',
|
||||
'Remove',
|
||||
icon=qtutils.discard_icon()):
|
||||
return
|
||||
self.emit(SIGNAL(drop_stash), selection)
|
||||
self.update_from_model()
|
||||
self.stash_view.setPlainText('')
|
||||
+154
-87
@@ -6,17 +6,16 @@ import re
|
||||
import sys
|
||||
import errno
|
||||
import platform
|
||||
import shlex
|
||||
import subprocess
|
||||
import hashlib
|
||||
import mimetypes
|
||||
|
||||
from glob import glob
|
||||
from cStringIO import StringIO
|
||||
|
||||
from cola import git
|
||||
from cola import core
|
||||
from cola import resources
|
||||
from cola.git import shell_quote
|
||||
from cola.compat import hashlib
|
||||
from cola.decorators import memoize
|
||||
|
||||
|
||||
KNOWN_FILE_MIME_TYPES = {
|
||||
'text': 'script.png',
|
||||
@@ -64,15 +63,6 @@ def run_cmd(command):
|
||||
return git.Git.execute(command)
|
||||
|
||||
|
||||
def qm_for_locale(locale):
|
||||
"""Returns the .qm file for a particular $LANG values."""
|
||||
regex = re.compile(r'([^\.])+\..*$')
|
||||
match = regex.match(locale)
|
||||
if match:
|
||||
locale = match.group(1)
|
||||
return resources.qm(locale.split('_')[0])
|
||||
|
||||
|
||||
def ident_file_type(filename):
|
||||
"""Returns an icon based on the contents of filename."""
|
||||
if os.path.exists(filename):
|
||||
@@ -100,62 +90,10 @@ def file_icon(filename):
|
||||
return resources.icon(ident_file_type(filename))
|
||||
|
||||
|
||||
def win32_abspath(exe):
|
||||
"""Return the absolute path to an .exe if it exists"""
|
||||
if os.path.exists(exe):
|
||||
return exe
|
||||
if not exe.endswith('.exe'):
|
||||
exe += '.exe'
|
||||
if os.path.exists(exe):
|
||||
return exe
|
||||
for path in os.environ['PATH'].split(os.pathsep):
|
||||
abspath = os.path.join(path, exe)
|
||||
if os.path.exists(abspath):
|
||||
return abspath
|
||||
return None
|
||||
|
||||
|
||||
def win32_expand_paths(args):
|
||||
"""Expand filenames after the double-dash"""
|
||||
if '--' not in args:
|
||||
return args
|
||||
dashes_idx = args.index('--')
|
||||
cmd = args[:dashes_idx+1]
|
||||
for path in args[dashes_idx+1:]:
|
||||
cmd.append(shell_quote(os.path.join(os.getcwd(), path)))
|
||||
return cmd
|
||||
|
||||
|
||||
def fork(args):
|
||||
"""Launch a command in the background."""
|
||||
if os.name in ('nt', 'dos'):
|
||||
# Windows is absolutely insane.
|
||||
#
|
||||
# If we want to launch 'gitk' we have to use the 'sh -c' trick.
|
||||
#
|
||||
# If we want to launch 'git.exe' we have to expand all filenames
|
||||
# after the double-dash.
|
||||
#
|
||||
# os.spawnv wants an absolute path in the command name but not in
|
||||
# the command vector. Wow.
|
||||
enc_args = win32_expand_paths([core.encode(a) for a in args])
|
||||
abspath = win32_abspath(enc_args[0])
|
||||
if abspath:
|
||||
# e.g. fork(['git', 'difftool', '--no-prompt', '--', 'path'])
|
||||
return os.spawnv(os.P_NOWAIT, abspath, enc_args)
|
||||
|
||||
# e.g. fork(['gitk', '--all'])
|
||||
sh_exe = win32_abspath('sh')
|
||||
enc_argv = map(shell_quote, enc_args)
|
||||
cmdstr = ' '.join(enc_argv)
|
||||
cmd = ['sh.exe', '-c', cmdstr]
|
||||
return os.spawnv(os.P_NOWAIT, sh_exe, cmd)
|
||||
else:
|
||||
# Unix is absolutely simple
|
||||
enc_args = [core.encode(a) for a in args]
|
||||
enc_argv = map(shell_quote, enc_args)
|
||||
cmdstr = ' '.join(enc_argv)
|
||||
return os.system(cmdstr + '&')
|
||||
encoded_args = [core.encode(arg) for arg in args]
|
||||
return subprocess.Popen(encoded_args).pid
|
||||
|
||||
|
||||
def sublist(a,b):
|
||||
@@ -235,18 +173,19 @@ def dirname(path):
|
||||
|
||||
def slurp(path):
|
||||
"""Slurps a filepath into a string."""
|
||||
fh = open(path)
|
||||
fh = open(core.encode(path))
|
||||
slushy = core.read_nointr(fh)
|
||||
fh.close()
|
||||
return core.decode(slushy)
|
||||
|
||||
|
||||
def write(path, contents):
|
||||
"""Writes a string to a file."""
|
||||
fh = open(path, 'w')
|
||||
"""Writes a raw string to a file."""
|
||||
fh = open(core.encode(path), 'wb')
|
||||
core.write_nointr(fh, core.encode(contents))
|
||||
fh.close()
|
||||
|
||||
|
||||
def strip_prefix(prefix, string):
|
||||
"""Return string, without the prefix. Blow up if string doesn't
|
||||
start with prefix."""
|
||||
@@ -259,6 +198,20 @@ def sanitize(s):
|
||||
s = s.replace(c, '_')
|
||||
return s
|
||||
|
||||
|
||||
def shell_split(s):
|
||||
"""Split string apart into utf-8 encoded words using shell syntax"""
|
||||
try:
|
||||
return shlex.split(core.encode(s))
|
||||
except ValueError:
|
||||
return [s]
|
||||
|
||||
|
||||
def shell_usplit(s):
|
||||
"""Returns a unicode list instead of encoded strings"""
|
||||
return [core.decode(arg) for arg in shell_split(s)]
|
||||
|
||||
|
||||
def is_linux():
|
||||
"""Is this a linux machine?"""
|
||||
while True:
|
||||
@@ -288,17 +241,32 @@ def is_darwin():
|
||||
return 'macintosh' in p or 'darwin' in p
|
||||
|
||||
|
||||
def is_broken():
|
||||
"""Is it windows or mac? (e.g. is running git-mergetool non-trivial?)"""
|
||||
if is_darwin():
|
||||
return True
|
||||
while True:
|
||||
try:
|
||||
return platform.system() == 'Windows'
|
||||
except IOError, e:
|
||||
if e.errno == errno.EINTR:
|
||||
continue
|
||||
raise e
|
||||
@memoize
|
||||
def is_win32():
|
||||
"""Return True on win32"""
|
||||
return os.name in ('nt', 'dos')
|
||||
|
||||
|
||||
def win32_set_binary(fd):
|
||||
try:
|
||||
import msvcrt
|
||||
except ImportError:
|
||||
return
|
||||
# When run without console, pipes may expose invalid
|
||||
# fileno(), usually set to -1.
|
||||
if hasattr(fd, 'fileno') and fd.fileno() >= 0:
|
||||
msvcrt.setmode(fd.fileno(), os.O_BINARY)
|
||||
|
||||
|
||||
def posix_set_binary(fd):
|
||||
"""POSIX file descriptors are always binary"""
|
||||
pass
|
||||
|
||||
|
||||
if is_win32():
|
||||
set_binary = win32_set_binary
|
||||
else:
|
||||
set_binary = posix_set_binary
|
||||
|
||||
|
||||
def checksum(path):
|
||||
@@ -308,8 +276,107 @@ def checksum(path):
|
||||
return md5.hexdigest()
|
||||
|
||||
|
||||
def quote_repopath(repopath):
|
||||
"""Quote a path for nt/dos only."""
|
||||
if os.name in ('nt', 'dos'):
|
||||
repopath = '"%s"' % repopath
|
||||
return repopath
|
||||
def error(msg, *args):
|
||||
"""Print an error message to stderr."""
|
||||
print >> sys.stderr, "ERROR:", msg % args
|
||||
|
||||
|
||||
def warn(msg, *args):
|
||||
"""Print a warning message to stderr."""
|
||||
print >> sys.stderr, "warning:", msg % args
|
||||
|
||||
|
||||
def die(msg, *args):
|
||||
"""Print as error message to stderr and exit the program."""
|
||||
error(msg, *args)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class ProgressIndicator(object):
|
||||
|
||||
"""Simple progress indicator.
|
||||
|
||||
Displayed as a spinning character by default, but can be customized
|
||||
by passing custom messages that overrides the spinning character.
|
||||
|
||||
"""
|
||||
|
||||
States = ("|", "/", "-", "\\")
|
||||
|
||||
def __init__(self, prefix="", f=sys.stdout):
|
||||
"""Create a new ProgressIndicator, bound to the given file object."""
|
||||
self.n = 0 # Simple progress counter
|
||||
self.f = f # Progress is written to this file object
|
||||
self.prev_len = 0 # Length of previous msg (to be overwritten)
|
||||
self.prefix = prefix # Prefix prepended to each progress message
|
||||
self.prefix_lens = [] # Stack of prefix string lengths
|
||||
|
||||
def pushprefix(self, prefix):
|
||||
"""Append the given prefix onto the prefix stack."""
|
||||
self.prefix_lens.append(len(self.prefix))
|
||||
self.prefix += prefix
|
||||
|
||||
def popprefix(self):
|
||||
"""Remove the last prefix from the prefix stack."""
|
||||
prev_len = self.prefix_lens.pop()
|
||||
self.prefix = self.prefix[:prev_len]
|
||||
|
||||
def __call__(self, msg = None, lf=False):
|
||||
"""Indicate progress, possibly with a custom message."""
|
||||
if msg is None:
|
||||
msg = self.States[self.n % len(self.States)]
|
||||
msg = self.prefix + msg
|
||||
print >> self.f, "\r%-*s" % (self.prev_len, msg),
|
||||
self.prev_len = len(msg.expandtabs())
|
||||
if lf:
|
||||
print >> self.f
|
||||
self.prev_len = 0
|
||||
self.n += 1
|
||||
|
||||
def finish (self, msg="done", noprefix=False):
|
||||
"""Finalize progress indication with the given message."""
|
||||
if noprefix:
|
||||
self.prefix = ""
|
||||
self(msg, True)
|
||||
|
||||
|
||||
def start_command(args, cwd=None, shell=False, add_env=None,
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE):
|
||||
"""Start the given command, and return a subprocess object.
|
||||
|
||||
This provides a simpler interface to the subprocess module.
|
||||
|
||||
"""
|
||||
env = None
|
||||
if add_env is not None:
|
||||
env = os.environ.copy()
|
||||
env.update(add_env)
|
||||
return subprocess.Popen(args, bufsize=1, stdin=stdin, stdout=stdout,
|
||||
stderr=stderr, cwd=cwd, shell=shell,
|
||||
env=env, universal_newlines=True)
|
||||
|
||||
|
||||
def run_command(args, cwd=None, shell=False, add_env=None,
|
||||
flag_error=True):
|
||||
"""Run the given command to completion, and return its results.
|
||||
|
||||
This provides a simpler interface to the subprocess module.
|
||||
|
||||
The results are formatted as a 3-tuple: (exit_code, output, errors)
|
||||
|
||||
If flag_error is enabled, Error messages will be produced if the
|
||||
subprocess terminated with a non-zero exit code and/or stderr
|
||||
output.
|
||||
|
||||
The other arguments are passed on to start_command().
|
||||
|
||||
"""
|
||||
process = start_command(args, cwd, shell, add_env)
|
||||
(output, errors) = process.communicate()
|
||||
exit_code = process.returncode
|
||||
if flag_error and errors:
|
||||
error("'%s' returned errors:\n---\n%s---", " ".join(args), errors)
|
||||
if flag_error and exit_code:
|
||||
error("'%s' returned exit code %i", " ".join(args), exit_code)
|
||||
return (exit_code, output, errors)
|
||||
|
||||
+59
-32
@@ -1,32 +1,40 @@
|
||||
# Copyright (c) 2008 David Aguilar
|
||||
"""Provides the current cola version number"""
|
||||
# Copyright (c) 2012 David Aguilar
|
||||
"""Provide cola's version number"""
|
||||
|
||||
# The current git-cola version
|
||||
_default_version = '1.7.0'
|
||||
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
srcdir = os.path.dirname(os.path.dirname(__file__))
|
||||
sys.path.insert(1, srcdir)
|
||||
sys.path.insert(1, os.path.join(srcdir, 'thirdparty'))
|
||||
|
||||
from cola import git
|
||||
from cola import errors
|
||||
from cola import utils
|
||||
from cola import resources
|
||||
from cola.decorators import memoize
|
||||
|
||||
# minimum version requirements
|
||||
_versions = {
|
||||
'git': '1.5.2',
|
||||
'python': '2.4',
|
||||
'pyqt': '4.3',
|
||||
# git-difftool moved out of contrib in git 1.6.3
|
||||
'difftool-builtin': '1.6.3',
|
||||
# git-diff learned --patience in 1.6.2
|
||||
# git-mergetool learned --no-prompt in 1.6.2
|
||||
'mergetool-no-prompt': '1.6.2',
|
||||
# git-difftool moved out of contrib in git 1.6.3
|
||||
'git': '1.6.3',
|
||||
'python': '2.4',
|
||||
'pyqt': '4.4',
|
||||
'pyqt_qrunnable': '4.4',
|
||||
'diff-submodule': '1.6.6',
|
||||
}
|
||||
|
||||
|
||||
def get(key):
|
||||
"""Returns an entry from the known versions table"""
|
||||
return _versions.get(key)
|
||||
|
||||
# cached to avoid recalculation
|
||||
_version = None
|
||||
|
||||
class VersionUnavailable(Exception):
|
||||
pass
|
||||
@@ -43,7 +51,7 @@ def git_describe_version():
|
||||
with_stderr=True)
|
||||
except errors.GitCommandError, e:
|
||||
raise VersionUnavailable(str(e))
|
||||
if not re.match(r'^v[0-9]', v):
|
||||
if v[0:1] != 'v' or not v[1:2].isdigit():
|
||||
raise VersionUnavailable('%s: bad version' % v)
|
||||
try:
|
||||
dirty = git.Git.execute(['git', 'diff-index', '--name-only', 'HEAD'])
|
||||
@@ -51,7 +59,7 @@ def git_describe_version():
|
||||
raise VersionUnavailable(str(e))
|
||||
if dirty:
|
||||
v += '-dirty'
|
||||
return re.sub('-', '.', utils.strip_prefix('v', v))
|
||||
return utils.strip_prefix('v', v.replace('-', '.'))
|
||||
|
||||
|
||||
def builtin_version():
|
||||
@@ -64,46 +72,59 @@ def builtin_version():
|
||||
return bv.version
|
||||
|
||||
|
||||
def _builtin_version_file(ext = 'py'):
|
||||
@memoize
|
||||
def _builtin_version_file(ext='py'):
|
||||
"""Returns the path to cola's builtin_version.py."""
|
||||
dirname = os.path.dirname(os.path.abspath(__file__))
|
||||
dirname = os.path.dirname(__file__)
|
||||
return os.path.join(dirname, 'builtin_version.%s' % ext)
|
||||
|
||||
|
||||
def release_version():
|
||||
"""Return a version number for a release
|
||||
|
||||
First see if there is a version file (included in release tarballs),
|
||||
then try git-describe, then default.
|
||||
|
||||
"""
|
||||
if os.path.isdir('.git'):
|
||||
try:
|
||||
return git_describe_version()
|
||||
except VersionUnavailable:
|
||||
pass
|
||||
return version()
|
||||
|
||||
|
||||
def write_builtin_version():
|
||||
"""Writes cola/builtin_version.py."""
|
||||
try:
|
||||
v = git_describe_version()
|
||||
except VersionUnavailable:
|
||||
return
|
||||
"""Writes cola/builtin_version.py
|
||||
|
||||
"""
|
||||
v = release_version()
|
||||
f = file(_builtin_version_file(), 'w')
|
||||
f.write('# This file was generated automatically. Do not edit by hand.\n'
|
||||
'version = %r\n' % v)
|
||||
f.close()
|
||||
|
||||
|
||||
def delete_builtin_version():
|
||||
"""Deletes cola/builtin_version.py."""
|
||||
for ext in ('py', 'pyc', 'pyo'):
|
||||
fn = _builtin_version_file(ext)
|
||||
fn = _builtin_version_file(ext=ext)
|
||||
if os.path.exists(fn):
|
||||
os.remove(fn)
|
||||
|
||||
|
||||
def version():
|
||||
@memoize
|
||||
def version(vstr=_default_version):
|
||||
"""Returns the builtin version or calculates the current version."""
|
||||
global _version
|
||||
if _version:
|
||||
return _version
|
||||
for v in [builtin_version, git_describe_version]:
|
||||
try:
|
||||
_version = v()
|
||||
break
|
||||
return v()
|
||||
except VersionUnavailable:
|
||||
pass
|
||||
if not _version:
|
||||
_version = 'unknown-version'
|
||||
return _version
|
||||
return vstr
|
||||
|
||||
|
||||
@memoize
|
||||
def check_version(min_ver, ver):
|
||||
"""Check whether ver is greater or equal to min_ver
|
||||
"""
|
||||
@@ -112,6 +133,7 @@ def check_version(min_ver, ver):
|
||||
return min_ver_list <= ver_list
|
||||
|
||||
|
||||
@memoize
|
||||
def check(key, ver):
|
||||
"""Checks if a version is greater than the known version for <what>"""
|
||||
return check_version(get(key), ver)
|
||||
@@ -130,6 +152,11 @@ def version_to_list(version):
|
||||
return ver_list
|
||||
|
||||
|
||||
@memoize
|
||||
def git_version():
|
||||
"""Returns the current GIT version"""
|
||||
return utils.run_cmd(['git', '--version']).split()[2]
|
||||
return git.instance().version().split()[-1]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(release_version())
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
"""Provides the BookmarksView dialog."""
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola.views import standard
|
||||
|
||||
|
||||
class BookmarkView(standard.StandardDialog):
|
||||
class BookmarkView(standard.Dialog):
|
||||
def __init__(self, parent=None):
|
||||
standard.StandardDialog.__init__(self, parent=parent)
|
||||
standard.Dialog.__init__(self, parent=parent)
|
||||
|
||||
self.setWindowTitle(self.tr('Bookmarks'))
|
||||
self.resize(494, 238)
|
||||
|
||||
@@ -7,9 +7,9 @@ from PyQt4.QtCore import SIGNAL
|
||||
from cola.views import standard
|
||||
|
||||
|
||||
class CompareView(standard.StandardDialog):
|
||||
class CompareView(standard.Dialog):
|
||||
def __init__(self, parent=None):
|
||||
standard.StandardDialog.__init__(self, parent=parent)
|
||||
standard.Dialog.__init__(self, parent=parent)
|
||||
|
||||
self.setWindowTitle(self.tr('Compare Commits'))
|
||||
self.resize(649, 372)
|
||||
@@ -83,9 +83,11 @@ class CompareView(standard.StandardDialog):
|
||||
self._bottom_layt.addItem(self._bottom_spacer)
|
||||
|
||||
self.button_compare = QtGui.QPushButton(self)
|
||||
self.button_compare.setText(self.tr('Compare'))
|
||||
self._bottom_layt.addWidget(self.button_compare)
|
||||
|
||||
self.button_close = QtGui.QPushButton(self)
|
||||
self.button_close.setText(self.tr('Close'))
|
||||
self._bottom_layt.addWidget(self.button_close)
|
||||
|
||||
self._main_layt.addLayout(self._bottom_layt)
|
||||
@@ -93,9 +95,9 @@ class CompareView(standard.StandardDialog):
|
||||
self.connect(self.button_close, SIGNAL('clicked()'), self.accept)
|
||||
|
||||
|
||||
class BranchCompareView(standard.StandardDialog):
|
||||
class BranchCompareView(standard.Dialog):
|
||||
def __init__(self, parent=None):
|
||||
standard.StandardDialog.__init__(self, parent=parent)
|
||||
standard.Dialog.__init__(self, parent=parent)
|
||||
|
||||
self.setWindowTitle(self.tr('Branch Diff Viewer'))
|
||||
self.resize(658, 350)
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola.views import standard
|
||||
|
||||
|
||||
class CreateBranchView(standard.StandardDialog):
|
||||
class CreateBranchView(standard.Dialog):
|
||||
"""A dialog for creating branches."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
standard.StandardDialog.__init__(self, parent=parent)
|
||||
standard.Dialog.__init__(self, parent=parent)
|
||||
|
||||
self.setWindowTitle(self.tr('Create Branch'))
|
||||
|
||||
|
||||
+5
-12
@@ -1,18 +1,15 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
import cola
|
||||
from cola import qtutils
|
||||
from cola.prefs import diff_font
|
||||
from cola.views import standard
|
||||
|
||||
|
||||
class CreateTag(standard.StandardDialog):
|
||||
class CreateTag(standard.Dialog):
|
||||
def __init__(self, parent=None):
|
||||
standard.StandardDialog.__init__(self, parent=parent)
|
||||
standard.Dialog.__init__(self, parent=parent)
|
||||
self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.setWindowTitle(self.tr('Create Tag'))
|
||||
self.resize(506, 295)
|
||||
self._main_layt = QtGui.QVBoxLayout(self)
|
||||
@@ -54,6 +51,7 @@ class CreateTag(standard.StandardDialog):
|
||||
self.tag_msg = QtGui.QTextEdit(self)
|
||||
self.tag_msg.setAcceptRichText(False)
|
||||
self.tag_msg.setToolTip(self.tr('Specifies the tag message'))
|
||||
self.tag_msg.setFont(diff_font())
|
||||
self._input_form_layt.setWidget(2, QtGui.QFormLayout.FieldRole,
|
||||
self.tag_msg)
|
||||
# Revision
|
||||
@@ -84,9 +82,4 @@ class CreateTag(standard.StandardDialog):
|
||||
self.close_button.setText(self.tr('Close'))
|
||||
self._button_hbox_layt.addWidget(self.close_button)
|
||||
|
||||
self.connect(self.sign_tag, SIGNAL('stateChanged(int)'),
|
||||
self.tag_msg.setEnabled)
|
||||
self.connect(self.close_button, SIGNAL('clicked()'), self.accept)
|
||||
|
||||
qtutils.set_diff_font(self.tag_msg)
|
||||
self.tag_msg.setEnabled(False)
|
||||
|
||||
@@ -1,696 +0,0 @@
|
||||
"""This view provides the main git-cola user interface.
|
||||
"""
|
||||
import os
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import Qt
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
import cola
|
||||
from cola import core
|
||||
from cola import utils
|
||||
from cola import qtutils
|
||||
from cola import settings
|
||||
from cola import signals
|
||||
from cola import resources
|
||||
from cola.qtutils import SLOT
|
||||
from cola.views import about
|
||||
from cola.views.syntax import DiffSyntaxHighlighter
|
||||
from cola.views.mainwindow import MainWindow
|
||||
from cola.controllers import compare
|
||||
from cola.controllers import search as smod
|
||||
from cola.controllers import createtag
|
||||
from cola.controllers.bookmark import manage_bookmarks
|
||||
from cola.controllers.bookmark import save_bookmark
|
||||
from cola.controllers.createbranch import create_new_branch
|
||||
from cola.controllers.merge import local_merge
|
||||
from cola.controllers.merge import abort_merge
|
||||
from cola.controllers.options import update_options
|
||||
from cola.controllers.util import choose_from_combo
|
||||
from cola.controllers.util import choose_from_list
|
||||
from cola.controllers.remote import remote_action
|
||||
from cola.controllers.repobrowser import browse_git_branch
|
||||
from cola.controllers.stash import stash
|
||||
from cola.controllers.selectcommits import select_commits
|
||||
|
||||
class MainView(MainWindow):
|
||||
"""The main cola interface."""
|
||||
idx_header = -1
|
||||
idx_staged = 0
|
||||
idx_modified = 1
|
||||
idx_unmerged = 2
|
||||
idx_untracked = 3
|
||||
idx_end = 4
|
||||
|
||||
# Read-only mode property
|
||||
mode = property(lambda self: self.model.mode)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
MainWindow.__init__(self, parent)
|
||||
self.amend_is_checked = self.amend_checkbox.isChecked
|
||||
|
||||
# Qt does not support noun/verbs
|
||||
self.commit_button.setText(qtutils.tr('Commit@@verb'))
|
||||
self.commit_menu.setTitle(qtutils.tr('Commit@@verb'))
|
||||
|
||||
# Diff/patch syntax highlighter
|
||||
self.syntax = DiffSyntaxHighlighter(self.display_text.document())
|
||||
|
||||
# Display the current column
|
||||
self.connect(self.commitmsg,
|
||||
SIGNAL('cursorPositionChanged()'),
|
||||
self.show_cursor_position)
|
||||
|
||||
# Keeps track of merge messages we've seen
|
||||
self.merge_message_hash = ''
|
||||
|
||||
# Initialize the seen tree widget indexes
|
||||
self._seen_indexes = set()
|
||||
|
||||
# Initialize the GUI to show 'Column: 00'
|
||||
self.show_cursor_position()
|
||||
|
||||
# Internal field used by import/export_state().
|
||||
# Change this whenever dockwidgets are removed.
|
||||
self._widget_version = 1
|
||||
|
||||
self.model = cola.model()
|
||||
self.model.add_message_observer(self.model.message_updated,
|
||||
self._update_view)
|
||||
|
||||
# Listen for text and amend messages
|
||||
cola.notifier().listen(signals.diff_text, self.set_display)
|
||||
cola.notifier().listen(signals.mode, self._mode_changed)
|
||||
cola.notifier().listen(signals.inotify, self._inotify_enabled)
|
||||
cola.notifier().listen(signals.amend, self.amend_checkbox.setChecked)
|
||||
|
||||
# Broadcast the amend mode
|
||||
self.connect(self.amend_checkbox, SIGNAL('toggled(bool)'),
|
||||
SLOT(signals.amend_mode))
|
||||
|
||||
# Add button callbacks
|
||||
self._relay_button(self.alt_button, signals.reset_mode)
|
||||
self._relay_button(self.rescan_button, signals.rescan)
|
||||
self._relay_button(self.signoff_button, signals.add_signoff)
|
||||
|
||||
self._connect_button(self.stage_button, self.stage)
|
||||
self._connect_button(self.unstage_button, self.unstage)
|
||||
self._connect_button(self.commit_button, self.commit)
|
||||
self._connect_button(self.fetch_button, self.fetch)
|
||||
self._connect_button(self.push_button, self.push)
|
||||
self._connect_button(self.pull_button, self.pull)
|
||||
self._connect_button(self.stash_button, stash)
|
||||
|
||||
# Menu actions
|
||||
actions = (
|
||||
(self.menu_quit, self.close),
|
||||
(self.menu_branch_compare, compare.branch_compare),
|
||||
(self.menu_branch_diff, self.branch_diff),
|
||||
(self.menu_branch_review, self.review_branch),
|
||||
(self.menu_browse_branch, self.browse_current),
|
||||
(self.menu_browse_other_branch, self.browse_other),
|
||||
(self.menu_browse_commits, self.browse_commits),
|
||||
(self.menu_create_tag, createtag.create_tag),
|
||||
(self.menu_create_branch, create_new_branch),
|
||||
(self.menu_checkout_branch, self.checkout_branch),
|
||||
(self.menu_delete_branch, self.branch_delete),
|
||||
(self.menu_rebase_branch, self.rebase),
|
||||
(self.menu_clone_repo, self.clone_repo),
|
||||
(self.menu_commit_compare, compare.compare),
|
||||
(self.menu_commit_compare_file, compare.compare_file),
|
||||
(self.menu_cherry_pick, self.cherry_pick),
|
||||
(self.menu_diff_expression, self.diff_expression),
|
||||
(self.menu_diff_branch, self.diff_branch),
|
||||
(self.menu_export_patches, self.export_patches),
|
||||
(self.menu_help_about, about.launch_about_dialog),
|
||||
(self.menu_help_docs,
|
||||
lambda: self.model.git.web__browse(resources.html_docs())),
|
||||
(self.menu_load_commitmsg, self.load_commitmsg),
|
||||
(self.menu_load_commitmsg_template, self.load_template),
|
||||
(self.menu_manage_bookmarks, manage_bookmarks),
|
||||
(self.menu_save_bookmark, save_bookmark),
|
||||
(self.menu_merge_local, local_merge),
|
||||
(self.menu_merge_abort, abort_merge),
|
||||
(self.menu_open_repo, self.open_repo),
|
||||
(self.menu_options, update_options),
|
||||
(self.menu_rescan, SLOT(signals.rescan)),
|
||||
(self.menu_search_grep, self.grep),
|
||||
(self.menu_search_revision, smod.search(smod.REVISION_ID)),
|
||||
(self.menu_search_revision_range, smod.search(smod.REVISION_RANGE)),
|
||||
(self.menu_search_message, smod.search(smod.MESSAGE)),
|
||||
(self.menu_search_path, smod.search(smod.PATH, True)),
|
||||
(self.menu_search_date_range, smod.search(smod.DATE_RANGE)),
|
||||
(self.menu_search_diff, smod.search(smod.DIFF)),
|
||||
(self.menu_search_author, smod.search(smod.AUTHOR)),
|
||||
(self.menu_search_committer, smod.search(smod.COMMITTER)),
|
||||
(self.menu_show_diffstat, SLOT(signals.diffstat)),
|
||||
(self.menu_stash, stash),
|
||||
(self.menu_stage_modified, SLOT(signals.stage_modified)),
|
||||
(self.menu_stage_untracked, SLOT(signals.stage_untracked)),
|
||||
(self.menu_unstage_selected, SLOT(signals.unstage_selected)),
|
||||
(self.menu_unstage_all, SLOT(signals.unstage_all)),
|
||||
(self.menu_visualize_all, SLOT(signals.visualize_all)),
|
||||
(self.menu_visualize_current, SLOT(signals.visualize_current)),
|
||||
# TODO This edit menu stuff should/could be command objects
|
||||
(self.menu_cut, self.action_cut),
|
||||
(self.menu_copy, self.action_copy),
|
||||
(self.menu_paste, self.commitmsg.paste),
|
||||
(self.menu_delete, self.action_delete),
|
||||
(self.menu_select_all, self.commitmsg.selectAll),
|
||||
(self.menu_undo, self.commitmsg.undo),
|
||||
(self.menu_redo, self.commitmsg.redo),
|
||||
)
|
||||
for menu, callback in actions:
|
||||
self.connect(menu, SIGNAL('triggered()'), callback)
|
||||
|
||||
# Install diff shortcut keys for stage/unstage
|
||||
self.display_text.keyPressEvent = self.diff_key_press_event
|
||||
self.display_text.contextMenuEvent = self.diff_context_menu_event
|
||||
|
||||
# Restore saved settings
|
||||
self._load_gui_state()
|
||||
|
||||
def _relay_button(self, button, signal):
|
||||
callback = SLOT(signal)
|
||||
self._connect_button(button, callback)
|
||||
|
||||
def _connect_button(self, button, callback):
|
||||
self.connect(button, SIGNAL('clicked()'), callback)
|
||||
|
||||
def _inotify_enabled(self, enabled):
|
||||
"""Hide the rescan button when inotify is enabled."""
|
||||
if enabled:
|
||||
self.rescan_button.hide()
|
||||
else:
|
||||
self.rescan_button.show()
|
||||
|
||||
def _update_view(self):
|
||||
"""Update the title with the current branch and directory name."""
|
||||
title = '%s [%s]' % (self.model.project,
|
||||
self.model.currentbranch)
|
||||
if self.mode in (self.model.mode_diff, self.model.mode_diff_expr):
|
||||
title += ' *** diff mode***'
|
||||
elif self.mode == self.model.mode_review:
|
||||
title += ' *** review mode***'
|
||||
elif self.mode == self.model.mode_amend:
|
||||
title += ' *** amending ***'
|
||||
self.setWindowTitle(title)
|
||||
|
||||
if self.mode != self.model.mode_amend:
|
||||
self.amend_checkbox.blockSignals(True)
|
||||
self.amend_checkbox.setChecked(False)
|
||||
self.amend_checkbox.blockSignals(False)
|
||||
|
||||
if not self.model.read_only() and self.mode != self.model.mode_amend:
|
||||
# Check if there's a message file in .git/
|
||||
merge_msg_path = self.model.merge_message_path()
|
||||
if merge_msg_path is None:
|
||||
return
|
||||
merge_msg_hash = utils.checksum(merge_msg_path)
|
||||
if merge_msg_hash == self.merge_message_hash:
|
||||
return
|
||||
self.merge_message_hash = merge_msg_hash
|
||||
cola.notifier().broadcast(signals.load_commit_message,
|
||||
merge_msg_path)
|
||||
|
||||
def _mode_changed(self, mode):
|
||||
"""React to mode changes; hide/show the "Exit Diff Mode" button."""
|
||||
if mode in (self.model.mode_review,
|
||||
self.model.mode_diff,
|
||||
self.model.mode_diff_expr):
|
||||
self.alt_button.setMinimumHeight(40)
|
||||
self.alt_button.show()
|
||||
else:
|
||||
self.alt_button.setMinimumHeight(1)
|
||||
self.alt_button.hide()
|
||||
|
||||
def set_display(self, text):
|
||||
"""Set the diff text display."""
|
||||
scrollbar = self.display_text.verticalScrollBar()
|
||||
scrollvalue = scrollbar.value()
|
||||
if text is not None:
|
||||
self.display_text.setText(text)
|
||||
scrollbar.setValue(scrollvalue)
|
||||
|
||||
def action_cut(self):
|
||||
self.action_copy()
|
||||
self.action_delete()
|
||||
|
||||
def action_copy(self):
|
||||
cursor = self.commitmsg.textCursor()
|
||||
selection = cursor.selection().toPlainText()
|
||||
qtutils.set_clipboard(selection)
|
||||
|
||||
def action_delete(self):
|
||||
self.commitmsg.textCursor().removeSelectedText()
|
||||
|
||||
def copy_display(self):
|
||||
cursor = self.display_text.textCursor()
|
||||
selection = cursor.selection().toPlainText()
|
||||
qtutils.set_clipboard(selection)
|
||||
|
||||
def diff_selection(self):
|
||||
cursor = self.display_text.textCursor()
|
||||
offset = cursor.position()
|
||||
selection = unicode(cursor.selection().toPlainText())
|
||||
return offset, selection
|
||||
|
||||
def selected_line(self):
|
||||
cursor = self.display_text.textCursor()
|
||||
offset = cursor.position()
|
||||
contents = unicode(self.display_text.toPlainText())
|
||||
while (offset >= 1
|
||||
and contents[offset-1]
|
||||
and contents[offset-1] != '\n'):
|
||||
offset -= 1
|
||||
data = contents[offset:]
|
||||
if '\n' in data:
|
||||
line, rest = data.split('\n', 1)
|
||||
else:
|
||||
line = data
|
||||
return line
|
||||
|
||||
def show_cursor_position(self):
|
||||
"""Update the UI with the current row and column."""
|
||||
cursor = self.commitmsg.textCursor()
|
||||
position = cursor.position()
|
||||
txt = unicode(self.commitmsg.toPlainText())
|
||||
rows = txt[:position].count('\n') + 1
|
||||
cols = cursor.columnNumber()
|
||||
display = ' %d,%d ' % (rows, cols)
|
||||
if cols > 78:
|
||||
display = ('<span style="color: white; '
|
||||
' background-color: red;"'
|
||||
'>%s</span>' % display.replace(' ', ' '))
|
||||
elif cols > 72:
|
||||
display = ('<span style="color: black; '
|
||||
' background-color: orange;"'
|
||||
'>%s</span>' % display.replace(' ', ' '))
|
||||
elif cols > 64:
|
||||
display = ('<span style="color: black; '
|
||||
' background-color: yellow;"'
|
||||
'>%s</span>' % display.replace(' ', ' '))
|
||||
self.position_label.setText(display)
|
||||
|
||||
def import_state(self, state):
|
||||
"""Imports data for save/restore"""
|
||||
MainWindow.import_state(self, state)
|
||||
# Restore the dockwidget, etc. window state
|
||||
if 'windowstate' in state:
|
||||
windowstate = state['windowstate']
|
||||
self.restoreState(QtCore.QByteArray.fromBase64(str(windowstate)),
|
||||
self._widget_version)
|
||||
|
||||
def export_state(self):
|
||||
"""Exports data for save/restore"""
|
||||
state = MainWindow.export_state(self)
|
||||
# Save the window state
|
||||
windowstate = self.saveState(self._widget_version)
|
||||
state['windowstate'] = unicode(windowstate.toBase64().data())
|
||||
return state
|
||||
|
||||
def review_branch(self):
|
||||
"""Diff against an arbitrary revision, branch, tag, etc."""
|
||||
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
|
||||
self.model.all_branches() +
|
||||
self.model.tags)
|
||||
if not branch:
|
||||
return
|
||||
cola.notifier().broadcast(signals.review_branch_mode, branch)
|
||||
|
||||
def branch_diff(self):
|
||||
"""Diff against an arbitrary revision, branch, tag, etc."""
|
||||
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
|
||||
['HEAD^'] +
|
||||
self.model.all_branches() +
|
||||
self.model.tags)
|
||||
if not branch:
|
||||
return
|
||||
cola.notifier().broadcast(signals.diff_mode, branch)
|
||||
|
||||
def diff_expression(self):
|
||||
"""Diff using an arbitrary expression."""
|
||||
expr = choose_from_combo('Enter Diff Expression',
|
||||
self.model.all_branches() +
|
||||
self.model.tags)
|
||||
if not expr:
|
||||
return
|
||||
cola.notifier().broadcast(signals.diff_expr_mode, expr)
|
||||
|
||||
|
||||
def diff_branch(self):
|
||||
"""Launches a diff against a branch."""
|
||||
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
|
||||
['HEAD^'] +
|
||||
self.model.all_branches() +
|
||||
self.model.tags)
|
||||
if not branch:
|
||||
return
|
||||
zfiles_str = self.model.git.diff(branch, name_only=True,
|
||||
no_color=True,
|
||||
z=True).rstrip('\0')
|
||||
files = zfiles_str.split('\0')
|
||||
filename = choose_from_list('Select File', files)
|
||||
if not filename:
|
||||
return
|
||||
cola.notifier().broadcast(signals.branch_mode, branch, filename)
|
||||
|
||||
def _load_gui_state(self):
|
||||
"""Restores the gui from the preferences file."""
|
||||
state = settings.SettingsManager.gui_state(self)
|
||||
self.import_state(state)
|
||||
|
||||
def load_commitmsg(self):
|
||||
"""Load a commit message from a file."""
|
||||
filename = qtutils.open_dialog(self,
|
||||
'Load Commit Message...',
|
||||
self.model.getcwd())
|
||||
if filename:
|
||||
cola.notifier().broadcast(signals.load_commit_message, filename)
|
||||
|
||||
|
||||
def load_template(self):
|
||||
"""Load the configured commit message template."""
|
||||
template = self.model.global_config('commit.template')
|
||||
if template:
|
||||
cola.notifier().broadcast(signals.load_commit_message, template)
|
||||
|
||||
|
||||
def diff_key_press_event(self, event):
|
||||
"""Handle shortcut keys in the diff view."""
|
||||
if event.key() != QtCore.Qt.Key_H and event.key() != QtCore.Qt.Key_S:
|
||||
event.ignore()
|
||||
return
|
||||
staged, modified, unmerged, untracked = cola.single_selection()
|
||||
if event.key() == QtCore.Qt.Key_H:
|
||||
if self.mode == self.model.mode_worktree and modified:
|
||||
self.stage_hunk()
|
||||
elif self.mode == self.model.mode_index:
|
||||
self.unstage_hunk()
|
||||
elif event.key() == QtCore.Qt.Key_S:
|
||||
if self.mode == self.model.mode_worktree and modified:
|
||||
self.stage_hunk_selection()
|
||||
elif self.mode == self.model.mode_index:
|
||||
self.unstage_hunk_selection()
|
||||
|
||||
def process_diff_selection(self, selected=False,
|
||||
staged=True, apply_to_worktree=False,
|
||||
reverse=False):
|
||||
"""Implement un/staging of selected lines or hunks."""
|
||||
offset, selection = self.diff_selection()
|
||||
cola.notifier().broadcast(signals.apply_diff_selection,
|
||||
staged,
|
||||
selected,
|
||||
offset,
|
||||
selection,
|
||||
apply_to_worktree)
|
||||
|
||||
def undo_hunk(self):
|
||||
"""Destructively remove a hunk from a worktree file."""
|
||||
if not qtutils.question(self,
|
||||
'Destroy Local Changes?',
|
||||
'This operation will drop '
|
||||
'uncommitted changes.\n'
|
||||
'Continue?',
|
||||
default=False):
|
||||
return
|
||||
self.process_diff_selection(staged=False, apply_to_worktree=True,
|
||||
reverse=True)
|
||||
|
||||
def undo_selection(self):
|
||||
"""Destructively check out content for the selected file from $head."""
|
||||
if not qtutils.question(self,
|
||||
'Destroy Local Changes?',
|
||||
'This operation will drop '
|
||||
'uncommitted changes.\n'
|
||||
'Continue?',
|
||||
default=False):
|
||||
return
|
||||
self.process_diff_selection(staged=False, apply_to_worktree=True,
|
||||
reverse=True, selected=True)
|
||||
|
||||
def stage(self):
|
||||
"""Stage selected files, or all files if no selection exists."""
|
||||
paths = cola.selection_model().unstaged
|
||||
if not paths:
|
||||
cola.notifier().broadcast(signals.stage_modified)
|
||||
else:
|
||||
cola.notifier().broadcast(signals.stage, paths)
|
||||
|
||||
def unstage(self):
|
||||
"""Unstage selected files, or all files if no selection exists."""
|
||||
paths = cola.selection_model().staged
|
||||
if not paths:
|
||||
cola.notifier().broadcast(signals.unstage_all)
|
||||
else:
|
||||
cola.notifier().broadcast(signals.unstage, paths)
|
||||
|
||||
def stage_hunk(self):
|
||||
"""Stage a specific hunk."""
|
||||
self.process_diff_selection(staged=False)
|
||||
|
||||
def stage_hunk_selection(self):
|
||||
"""Stage selected lines."""
|
||||
self.process_diff_selection(staged=False, selected=True)
|
||||
|
||||
def unstage_hunk(self, cached=True):
|
||||
"""Unstage a hunk."""
|
||||
self.process_diff_selection(staged=True)
|
||||
|
||||
def unstage_hunk_selection(self):
|
||||
"""Unstage selected lines."""
|
||||
self.process_diff_selection(staged=True, selected=True)
|
||||
|
||||
def diff_context_menu_event(self, event):
|
||||
"""Create the context menu for the diff display."""
|
||||
menu = self.diff_context_menu_setup()
|
||||
textedit = self.display_text
|
||||
menu.exec_(textedit.mapToGlobal(event.pos()))
|
||||
|
||||
def diff_context_menu_setup(self):
|
||||
"""Set up the context menu for the diff display."""
|
||||
menu = QtGui.QMenu(self)
|
||||
staged, modified, unmerged, untracked = cola.selection()
|
||||
|
||||
if self.mode == self.model.mode_worktree:
|
||||
if modified:
|
||||
menu.addAction(self.tr('Stage &Hunk For Commit'),
|
||||
self.stage_hunk)
|
||||
menu.addAction(self.tr('Stage &Selected Lines'),
|
||||
self.stage_hunk_selection)
|
||||
menu.addSeparator()
|
||||
menu.addAction(self.tr('Undo Hunk'), self.undo_hunk)
|
||||
menu.addAction(self.tr('Undo Selection'), self.undo_selection)
|
||||
|
||||
elif self.mode == self.model.mode_index:
|
||||
menu.addAction(self.tr('Unstage &Hunk From Commit'), self.unstage_hunk)
|
||||
menu.addAction(self.tr('Unstage &Selected Lines'), self.unstage_hunk_selection)
|
||||
|
||||
elif self.mode == self.model.mode_branch:
|
||||
menu.addAction(self.tr('Apply Diff to Work Tree'), self.stage_hunk)
|
||||
menu.addAction(self.tr('Apply Diff Selection to Work Tree'), self.stage_hunk_selection)
|
||||
|
||||
elif self.mode == self.model.mode_grep:
|
||||
menu.addAction(self.tr('Go Here'), self.goto_grep)
|
||||
|
||||
menu.addSeparator()
|
||||
menu.addAction(self.tr('Copy'), self.copy_display)
|
||||
return menu
|
||||
|
||||
def fetch(self):
|
||||
"""Launch the 'fetch' remote dialog."""
|
||||
remote_action(self, 'fetch')
|
||||
|
||||
def push(self):
|
||||
"""Launch the 'push' remote dialog."""
|
||||
remote_action(self, 'push')
|
||||
|
||||
def pull(self):
|
||||
"""Launch the 'pull' remote dialog."""
|
||||
remote_action(self, 'pull')
|
||||
|
||||
def commit(self):
|
||||
"""Attempt to create a commit from the index and commit message."""
|
||||
#self.reset_mode()
|
||||
msg = self.model.commitmsg
|
||||
if not msg:
|
||||
# Describe a good commit message
|
||||
error_msg = self.tr(''
|
||||
'Please supply a commit message.\n\n'
|
||||
'A good commit message has the following format:\n\n'
|
||||
'- First line: Describe in one sentence what you did.\n'
|
||||
'- Second line: Blank\n'
|
||||
'- Remaining lines: Describe why this change is good.\n')
|
||||
qtutils.log(1, error_msg)
|
||||
cola.notifier().broadcast(signals.information,
|
||||
'Missing Commit Message',
|
||||
error_msg)
|
||||
return
|
||||
if not self.model.staged:
|
||||
error_msg = self.tr(''
|
||||
'No changes to commit.\n\n'
|
||||
'You must stage at least 1 file before you can commit.\n')
|
||||
qtutils.log(1, error_msg)
|
||||
cola.notifier().broadcast(signals.information,
|
||||
'No Staged Changes',
|
||||
error_msg)
|
||||
return
|
||||
# Warn that amending published commits is generally bad
|
||||
amend = self.amend_is_checked()
|
||||
if (amend and self.model.is_commit_published() and
|
||||
not qtutils.question(self,
|
||||
'Rewrite Published Commit?',
|
||||
'This commit has already been published.\n'
|
||||
'You are rewriting published history.\n'
|
||||
'You probably don\'t want to do this.\n\n'
|
||||
'Continue?',
|
||||
default=False)):
|
||||
return
|
||||
# Perform the commit
|
||||
cola.notifier().broadcast(signals.commit, amend, msg)
|
||||
|
||||
def grep(self):
|
||||
"""Prompt and use 'git grep' to find the content."""
|
||||
# This should be a command in cola.commands.
|
||||
txt, ok = qtutils.prompt('grep')
|
||||
if not ok:
|
||||
return
|
||||
cola.notifier().broadcast(signals.grep, txt)
|
||||
|
||||
def goto_grep(self):
|
||||
"""Called when Search -> Grep's right-click 'goto' action."""
|
||||
line = self.selected_line()
|
||||
filename, line_number, contents = line.split(':', 2)
|
||||
filename = core.encode(filename)
|
||||
cola.notifier().broadcast(signals.edit, [filename], line_number=line_number)
|
||||
|
||||
def open_repo(self):
|
||||
"""Spawn a new cola session."""
|
||||
dirname = qtutils.opendir_dialog(self,
|
||||
'Open Git Repository...',
|
||||
self.model.getcwd())
|
||||
if not dirname:
|
||||
return
|
||||
cola.notifier().broadcast(signals.open_repo, dirname)
|
||||
|
||||
def clone_repo(self):
|
||||
"""Clone a git repository."""
|
||||
url, ok = qtutils.prompt('Path or URL to clone (Env. $VARS okay)')
|
||||
url = os.path.expandvars(url)
|
||||
if not ok or not url:
|
||||
return
|
||||
try:
|
||||
# Pick a suitable basename by parsing the URL
|
||||
newurl = url.replace('\\', '/')
|
||||
default = newurl.rsplit('/', 1)[-1]
|
||||
if default == '.git':
|
||||
# The end of the URL is /.git, so assume it's a file path
|
||||
default = os.path.basename(os.path.dirname(newurl))
|
||||
if default.endswith('.git'):
|
||||
# The URL points to a bare repo
|
||||
default = default[:-4]
|
||||
if url == '.':
|
||||
# The URL is the current repo
|
||||
default = os.path.basename(os.getcwd())
|
||||
if not default:
|
||||
raise
|
||||
except:
|
||||
cola.notifier().broadcast(signals.information,
|
||||
'Error Cloning',
|
||||
'Could not parse: "%s"' % url)
|
||||
qtutils.log(1, 'Oops, could not parse git url: "%s"' % url)
|
||||
return
|
||||
|
||||
# Prompt the user for a directory to use as the parent directory
|
||||
msg = 'Select a parent directory for the new clone'
|
||||
dirname = qtutils.opendir_dialog(self, msg, self.model.getcwd())
|
||||
if not dirname:
|
||||
return
|
||||
count = 1
|
||||
destdir = os.path.join(dirname, default)
|
||||
olddestdir = destdir
|
||||
if os.path.exists(destdir):
|
||||
# An existing path can be specified
|
||||
msg = ('"%s" already exists, cola will create a new directory' %
|
||||
destdir)
|
||||
cola.notifier().broadcast(signals.information,
|
||||
'Directory Exists', msg)
|
||||
|
||||
# Make sure the new destdir doesn't exist
|
||||
while os.path.exists(destdir):
|
||||
destdir = olddestdir + str(count)
|
||||
count += 1
|
||||
cola.notifier().broadcast(signals.clone, url, destdir)
|
||||
|
||||
def cherry_pick(self):
|
||||
"""Launch the 'Cherry-Pick' dialog."""
|
||||
revs, summaries = self.model.log_helper(all=True)
|
||||
commits = select_commits('Cherry-Pick Commit',
|
||||
revs, summaries, multiselect=False)
|
||||
if not commits:
|
||||
return
|
||||
cola.notifier().broadcast(signals.cherry_pick, commits)
|
||||
|
||||
def browse_commits(self):
|
||||
"""Launch the 'Browse Commits' dialog."""
|
||||
revs, summaries = self.model.log_helper(all=True)
|
||||
select_commits('Browse Commits', revs, summaries)
|
||||
|
||||
def export_patches(self):
|
||||
"""Run 'git format-patch' on a list of commits."""
|
||||
revs, summaries = self.model.log_helper()
|
||||
to_export = select_commits('Export Patches', revs, summaries)
|
||||
if not to_export:
|
||||
return
|
||||
to_export.reverse()
|
||||
revs.reverse()
|
||||
cola.notifier().broadcast(signals.format_patch, to_export, revs)
|
||||
|
||||
def browse_current(self):
|
||||
"""Launch the 'Browse Current Branch' dialog."""
|
||||
branch = self.model.currentbranch
|
||||
browse_git_branch(branch)
|
||||
|
||||
def browse_other(self):
|
||||
"""Prompt for a branch and inspect content at that point in time."""
|
||||
# Prompt for a branch to browse
|
||||
branch = choose_from_combo('Browse Branch Files',
|
||||
self.view,
|
||||
self.model.all_branches())
|
||||
if not branch:
|
||||
return
|
||||
# Launch the repobrowser
|
||||
browse_git_branch(branch)
|
||||
|
||||
def branch_create(self):
|
||||
"""Launch the 'Create Branch' dialog."""
|
||||
create_new_branch()
|
||||
|
||||
def branch_delete(self):
|
||||
"""Launch the 'Delete Branch' dialog."""
|
||||
branch = choose_from_combo('Delete Branch',
|
||||
self.model.local_branches)
|
||||
if not branch:
|
||||
return
|
||||
cola.notifier().broadcast(signals.delete_branch, branch)
|
||||
|
||||
def checkout_branch(self):
|
||||
"""Launch the 'Checkout Branch' dialog."""
|
||||
branch = choose_from_combo('Checkout Branch',
|
||||
self.model.local_branches)
|
||||
if not branch:
|
||||
return
|
||||
cola.notifier().broadcast(signals.checkout_branch, branch)
|
||||
|
||||
def rebase(self):
|
||||
"""Rebase onto a branch."""
|
||||
branch = choose_from_combo('Rebase Branch',
|
||||
self.model.all_branches())
|
||||
if not branch:
|
||||
return
|
||||
#TODO cmd
|
||||
status, output = self.model.git.rebase(branch,
|
||||
with_stderr=True,
|
||||
with_status=True)
|
||||
qtutils.log(status, output)
|
||||
@@ -1,395 +0,0 @@
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
|
||||
import cola
|
||||
from cola import settings
|
||||
from cola import qtutils
|
||||
from cola.views import log
|
||||
from cola.qtutils import tr
|
||||
from cola.views import status
|
||||
from cola.views.standard import create_standard_widget
|
||||
from cola.controllers import classic
|
||||
|
||||
|
||||
MainWindowBase = create_standard_widget(QtGui.QMainWindow)
|
||||
class MainWindow(MainWindowBase):
|
||||
def __init__(self, parent=None):
|
||||
MainWindowBase.__init__(self, parent)
|
||||
# Default size; this is thrown out when save/restore is used
|
||||
self.resize(888, 420)
|
||||
self.setDockOptions(QtGui.QMainWindow.AllowNestedDocks |
|
||||
QtGui.QMainWindow.AllowTabbedDocks |
|
||||
QtGui.QMainWindow.AnimatedDocks)
|
||||
# "Actions" widget
|
||||
self.actiondockwidget = self.create_dock('Actions')
|
||||
self.actiondockwidgetcontents = QtGui.QWidget()
|
||||
self.actiondockwidgetcontents.setContentsMargins(2, 2, 2, 2)
|
||||
self.actiondockwidgetlayout =\
|
||||
QtGui.QVBoxLayout(self.actiondockwidgetcontents)
|
||||
self.actiondockwidgetlayout.setSpacing(3)
|
||||
self.actiondockwidgetlayout.setMargin(3)
|
||||
|
||||
layout = self.actiondockwidgetlayout
|
||||
self.rescan_button = self.create_button('Rescan', layout)
|
||||
self.stage_button = self.create_button('Stage', layout)
|
||||
self.unstage_button = self.create_button('Unstage', layout)
|
||||
self.fetch_button = self.create_button('Fetch', layout)
|
||||
self.push_button = self.create_button('Push', layout)
|
||||
self.pull_button = self.create_button('Pull', layout)
|
||||
self.stash_button = self.create_button('Stash', layout)
|
||||
self.alt_button = self.create_button('Exit Diff Mode', layout)
|
||||
self.alt_button.hide()
|
||||
|
||||
self.action_spacer = QtGui.QSpacerItem(1, 1,
|
||||
QtGui.QSizePolicy.Minimum,
|
||||
QtGui.QSizePolicy.MinimumExpanding)
|
||||
self.actiondockwidgetlayout.addItem(self.action_spacer)
|
||||
self.actiondockwidget.setWidget(self.actiondockwidgetcontents)
|
||||
|
||||
# "Repository Status" widget
|
||||
self.statusdockwidget = self.create_dock('Repository Status')
|
||||
self.statusdockwidget.setWidget(status.widget())
|
||||
|
||||
# "Commit Message Editor" widget
|
||||
self.commitdockwidget = self.create_dock('Commit Message Editor')
|
||||
self.commitdockwidgetcontents = QtGui.QWidget()
|
||||
|
||||
self.commitdockwidgetlayout = QtGui.QVBoxLayout(self.commitdockwidgetcontents)
|
||||
self.commitdockwidgetlayout.setMargin(3)
|
||||
|
||||
self.vboxlayout = QtGui.QVBoxLayout()
|
||||
self.vboxlayout.setSpacing(0)
|
||||
|
||||
self.commitmsg = QtGui.QTextEdit(self.commitdockwidgetcontents)
|
||||
self.commitmsg.setMinimumSize(QtCore.QSize(1, 1))
|
||||
policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum,
|
||||
QtGui.QSizePolicy.Minimum)
|
||||
self.commitmsg.setSizePolicy(policy)
|
||||
self.commitmsg.setLineWrapMode(QtGui.QTextEdit.NoWrap)
|
||||
self.commitmsg.setAcceptRichText(False)
|
||||
self.vboxlayout.addWidget(self.commitmsg)
|
||||
|
||||
self.hboxlayout = QtGui.QHBoxLayout()
|
||||
self.hboxlayout.setSpacing(3)
|
||||
|
||||
# Sign off and commit buttons
|
||||
self.signoff_button = self.create_button('Sign Off')
|
||||
self.commit_button = self.create_button('Commit@@verb')
|
||||
|
||||
# Position display
|
||||
self.position_label = QtGui.QLabel(self.actiondockwidgetcontents)
|
||||
self.position_label.setAlignment(QtCore.Qt.AlignLeft)
|
||||
|
||||
# Spacer between position and amend
|
||||
self.spacer = QtGui.QSpacerItem(1, 1,
|
||||
QtGui.QSizePolicy.MinimumExpanding,
|
||||
QtGui.QSizePolicy.Minimum)
|
||||
# Amend checkbox
|
||||
self.amend_checkbox = QtGui.QCheckBox(self.commitdockwidgetcontents)
|
||||
self.amend_checkbox.setText(tr('Amend Last Commit'))
|
||||
|
||||
self.hboxlayout.addWidget(self.signoff_button)
|
||||
self.hboxlayout.addWidget(self.commit_button)
|
||||
self.hboxlayout.addWidget(self.position_label)
|
||||
self.hboxlayout.addItem(self.spacer)
|
||||
self.hboxlayout.addWidget(self.amend_checkbox)
|
||||
|
||||
self.vboxlayout.addLayout(self.hboxlayout)
|
||||
self.commitdockwidgetlayout.addLayout(self.vboxlayout)
|
||||
self.commitdockwidget.setWidget(self.commitdockwidgetcontents)
|
||||
|
||||
# "Command Output" widget
|
||||
logwidget = qtutils.logger()
|
||||
self.logdockwidget = self.create_dock('Command Output')
|
||||
self.logdockwidget.setWidget(logwidget)
|
||||
|
||||
# "Cola Classic" widget
|
||||
self.classicdockwidget = self.create_dock('Cola Classic')
|
||||
self.classicdockwidget.setWidget(classic.widget())
|
||||
|
||||
# "Diff Viewer" widget
|
||||
self.diffdockwidget = self.create_dock('Diff Viewer')
|
||||
self.diffdockwidgetcontents = QtGui.QWidget()
|
||||
self.diffdockwidgetlayout = QtGui.QVBoxLayout(self.diffdockwidgetcontents)
|
||||
self.diffdockwidgetlayout.setMargin(3)
|
||||
|
||||
self.display_text = QtGui.QTextEdit(self.diffdockwidgetcontents)
|
||||
self.display_text.setMinimumSize(QtCore.QSize(1, 1))
|
||||
self.display_text.setLineWrapMode(QtGui.QTextEdit.NoWrap)
|
||||
self.display_text.setReadOnly(True)
|
||||
self.display_text.setAcceptRichText(False)
|
||||
self.display_text.setCursorWidth(2)
|
||||
self.display_text.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByKeyboard|QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextBrowserInteraction|QtCore.Qt.TextSelectableByKeyboard|QtCore.Qt.TextSelectableByMouse)
|
||||
|
||||
self.diffdockwidgetlayout.addWidget(self.display_text)
|
||||
self.diffdockwidget.setWidget(self.diffdockwidgetcontents)
|
||||
|
||||
# All Actions
|
||||
self.menu_unstage_selected = self.create_action('Unstage From Commit')
|
||||
self.menu_show_diffstat = self.create_action('Diffstat')
|
||||
self.menu_stage_modified =\
|
||||
self.create_action('Stage Changed Files To Commit')
|
||||
self.menu_stage_untracked = self.create_action('Stage All Untracked')
|
||||
self.menu_export_patches = self.create_action('Export Patches...')
|
||||
self.menu_cut = self.create_action('Cut')
|
||||
self.menu_copy = self.create_action('Copy')
|
||||
self.menu_paste = self.create_action('Paste')
|
||||
self.menu_select_all = self.create_action('Select All')
|
||||
self.menu_options = self.create_action('Options')
|
||||
self.menu_delete = self.create_action('Delete')
|
||||
self.menu_undo = self.create_action('Undo')
|
||||
self.menu_redo = self.create_action('Redo')
|
||||
self.menu_rescan = self.create_action('Rescan')
|
||||
self.menu_cherry_pick = self.create_action('Cherry-Pick...')
|
||||
self.menu_unstage_all = self.create_action('Unstage All')
|
||||
self.menu_load_commitmsg = self.create_action('Load Commit Message...')
|
||||
self.menu_quit = self.create_action('Quit')
|
||||
self.menu_search_revision = self.create_action('Revision ID...')
|
||||
self.menu_search_path =\
|
||||
self.create_action('Commits Touching Path(s)...')
|
||||
self.menu_search_revision_range =\
|
||||
self.create_action('Revision Range...')
|
||||
self.menu_search_date_range = self.create_action('Latest Commits...')
|
||||
self.menu_search_message = self.create_action('Commit Messages...')
|
||||
self.menu_search_diff =\
|
||||
self.create_action('Content Introduced in Commit...')
|
||||
self.menu_search_author = self.create_action('Commits By Author...')
|
||||
self.menu_search_committer =\
|
||||
self.create_action('Commits By Committer...')
|
||||
self.menu_manage_bookmarks = self.create_action('Bookmarks...')
|
||||
self.menu_save_bookmark = self.create_action('Bookmark Current...')
|
||||
self.menu_search_grep = self.create_action('Grep')
|
||||
self.menu_merge_local = self.create_action('Merge...')
|
||||
self.menu_merge_abort = self.create_action('Abort Merge...')
|
||||
self.menu_open_repo = self.create_action('Open...')
|
||||
self.menu_stash = self.create_action('Stash...')
|
||||
self.menu_diff_branch =\
|
||||
self.create_action('Apply Changes From Branch...')
|
||||
self.menu_branch_compare = self.create_action('Branches...')
|
||||
self.menu_clone_repo = self.create_action('Clone...')
|
||||
self.menu_help_docs = self.create_action('Documentation')
|
||||
self.menu_commit_compare = self.create_action('Commits...')
|
||||
self.menu_visualize_current =\
|
||||
self.create_action('Visualize Current Branch...')
|
||||
self.menu_visualize_all =\
|
||||
self.create_action('Visualize All Branches...')
|
||||
self.menu_browse_commits = self.create_action('Browse Commits...')
|
||||
self.menu_browse_branch =\
|
||||
self.create_action('Browse Current Branch...')
|
||||
self.menu_browse_other_branch =\
|
||||
self.create_action('Browse Other Branch...')
|
||||
self.menu_load_commitmsg_template =\
|
||||
self.create_action('Get Commit Message Template')
|
||||
self.menu_commit_compare_file =\
|
||||
self.create_action('Commits Touching File...')
|
||||
self.menu_help_about = self.create_action('About')
|
||||
self.menu_branch_diff = self.create_action('SHA-1...')
|
||||
self.menu_diff_expression = self.create_action('Expression...')
|
||||
self.menu_create_tag = self.create_action('Create Tag...')
|
||||
self.menu_create_branch = self.create_action('Create...')
|
||||
self.menu_delete_branch = self.create_action('Delete...')
|
||||
self.menu_checkout_branch = self.create_action('Checkout...')
|
||||
self.menu_rebase_branch = self.create_action('Rebase...')
|
||||
self.menu_branch_review = self.create_action('Review...')
|
||||
|
||||
# Create the application menu
|
||||
self.menubar = QtGui.QMenuBar(self)
|
||||
|
||||
# File Menu
|
||||
self.file_menu = self.create_menu('File', self.menubar)
|
||||
self.file_menu.addAction(self.menu_open_repo)
|
||||
self.file_menu.addAction(self.menu_clone_repo)
|
||||
self.file_menu.addSeparator()
|
||||
self.file_menu.addAction(self.menu_rescan)
|
||||
self.file_menu.addSeparator()
|
||||
self.file_menu.addAction(self.menu_manage_bookmarks)
|
||||
self.file_menu.addAction(self.menu_save_bookmark)
|
||||
self.file_menu.addSeparator()
|
||||
self.file_menu.addAction(self.menu_load_commitmsg)
|
||||
self.file_menu.addAction(self.menu_load_commitmsg_template)
|
||||
self.file_menu.addSeparator()
|
||||
self.file_menu.addAction(self.menu_quit)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.file_menu.menuAction())
|
||||
|
||||
# Edit Menu
|
||||
self.edit_menu = self.create_menu('Edit', self.menubar)
|
||||
self.edit_menu.addAction(self.menu_undo)
|
||||
self.edit_menu.addAction(self.menu_redo)
|
||||
self.edit_menu.addSeparator()
|
||||
self.edit_menu.addAction(self.menu_cut)
|
||||
self.edit_menu.addAction(self.menu_copy)
|
||||
self.edit_menu.addAction(self.menu_paste)
|
||||
self.edit_menu.addAction(self.menu_delete)
|
||||
self.edit_menu.addSeparator()
|
||||
self.edit_menu.addAction(self.menu_select_all)
|
||||
self.edit_menu.addSeparator()
|
||||
self.edit_menu.addAction(self.menu_options)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.edit_menu.menuAction())
|
||||
|
||||
# Commit Menu
|
||||
self.commit_menu = self.create_menu('Commit', self.menubar)
|
||||
self.commit_menu.addAction(self.menu_stage_modified)
|
||||
self.commit_menu.addAction(self.menu_stage_untracked)
|
||||
self.commit_menu.addSeparator()
|
||||
self.commit_menu.addAction(self.menu_unstage_all)
|
||||
self.commit_menu.addAction(self.menu_unstage_selected)
|
||||
self.commit_menu.addSeparator()
|
||||
self.commit_menu.addAction(self.menu_browse_commits)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.commit_menu.menuAction())
|
||||
|
||||
# Branch Menu
|
||||
self.branch_menu = self.create_menu('Branch', self.menubar)
|
||||
self.branch_menu.addAction(self.menu_branch_review)
|
||||
self.branch_menu.addSeparator()
|
||||
self.branch_menu.addAction(self.menu_create_branch)
|
||||
self.branch_menu.addAction(self.menu_checkout_branch)
|
||||
self.branch_menu.addAction(self.menu_rebase_branch)
|
||||
self.branch_menu.addAction(self.menu_delete_branch)
|
||||
self.branch_menu.addSeparator()
|
||||
self.branch_menu.addAction(self.menu_browse_branch)
|
||||
self.branch_menu.addAction(self.menu_browse_other_branch)
|
||||
self.branch_menu.addSeparator()
|
||||
self.branch_menu.addAction(self.menu_visualize_current)
|
||||
self.branch_menu.addAction(self.menu_visualize_all)
|
||||
self.branch_menu.addSeparator()
|
||||
self.branch_menu.addAction(self.menu_diff_branch)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.branch_menu.menuAction())
|
||||
|
||||
# Search Menu
|
||||
self.search_menu = self.create_menu('Search', self.menubar)
|
||||
self.search_menu.addAction(self.menu_search_date_range)
|
||||
self.search_menu.addAction(self.menu_search_grep)
|
||||
self.search_menu.addSeparator()
|
||||
# Search / More Menu
|
||||
self.menu_search_more = self.create_menu('More...', self.search_menu)
|
||||
self.menu_search_more.addAction(self.menu_search_author)
|
||||
self.menu_search_more.addAction(self.menu_search_path)
|
||||
self.menu_search_more.addAction(self.menu_search_message)
|
||||
self.menu_search_more.addSeparator()
|
||||
self.menu_search_more.addAction(self.menu_search_revision_range)
|
||||
self.menu_search_more.addAction(self.menu_search_revision)
|
||||
self.menu_search_more.addSeparator()
|
||||
self.menu_search_more.addAction(self.menu_search_diff)
|
||||
self.search_menu.addAction(self.menu_search_more.menuAction())
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.search_menu.menuAction())
|
||||
|
||||
# Actions menu
|
||||
self.actions_menu = self.create_menu('Actions', self.menubar)
|
||||
self.actions_menu.addAction(self.menu_merge_local)
|
||||
self.actions_menu.addAction(self.menu_stash)
|
||||
self.actions_menu.addSeparator()
|
||||
self.actions_menu.addAction(self.menu_create_tag)
|
||||
self.actions_menu.addSeparator()
|
||||
self.actions_menu.addAction(self.menu_export_patches)
|
||||
self.actions_menu.addAction(self.menu_cherry_pick)
|
||||
self.actions_menu.addSeparator()
|
||||
self.actions_menu.addAction(self.menu_merge_abort)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.actions_menu.menuAction())
|
||||
|
||||
# Diff Menu
|
||||
self.diff_menu = self.create_menu('Diff', self.menubar)
|
||||
self.diff_menu.addAction(self.menu_branch_diff)
|
||||
self.diff_menu.addAction(self.menu_diff_expression)
|
||||
self.diff_menu.addSeparator()
|
||||
self.diff_menu.addAction(self.menu_branch_compare)
|
||||
self.diff_menu.addAction(self.menu_commit_compare)
|
||||
self.diff_menu.addAction(self.menu_commit_compare_file)
|
||||
self.diff_menu.addSeparator()
|
||||
self.diff_menu.addAction(self.menu_show_diffstat)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.diff_menu.menuAction())
|
||||
|
||||
# Tools Menu
|
||||
self.tools_menu = self.create_menu('Tools', self.menubar)
|
||||
self.tools_menu.addAction(self.classicdockwidget.toggleViewAction())
|
||||
self.tools_menu.addAction(self.diffdockwidget.toggleViewAction())
|
||||
self.tools_menu.addAction(self.actiondockwidget.toggleViewAction())
|
||||
self.tools_menu.addAction(self.commitdockwidget.toggleViewAction())
|
||||
self.tools_menu.addAction(self.statusdockwidget.toggleViewAction())
|
||||
self.tools_menu.addAction(self.logdockwidget.toggleViewAction())
|
||||
self.menubar.addAction(self.tools_menu.menuAction())
|
||||
|
||||
# Help Menu
|
||||
self.help_menu = self.create_menu('Help', self.menubar)
|
||||
self.help_menu.addAction(self.menu_help_docs)
|
||||
self.help_menu.addAction(self.menu_help_about)
|
||||
# Add to menubar
|
||||
self.menubar.addAction(self.help_menu.menuAction())
|
||||
|
||||
# Set main menu
|
||||
self.setMenuBar(self.menubar)
|
||||
|
||||
# Shortcuts
|
||||
self.menu_show_diffstat.setShortcut(tr('Ctrl+D'))
|
||||
self.menu_stage_modified.setShortcut(tr('Alt+A'))
|
||||
self.menu_stage_untracked.setShortcut(tr('Alt+U'))
|
||||
self.menu_export_patches.setShortcut(tr('Ctrl+E'))
|
||||
self.menu_cut.setShortcut(tr('Ctrl+X'))
|
||||
self.menu_copy.setShortcut(tr('Ctrl+C'))
|
||||
self.menu_paste.setShortcut(tr('Ctrl+V'))
|
||||
self.menu_select_all.setShortcut(tr('Ctrl+A'))
|
||||
self.menu_options.setShortcut(tr('Ctrl+O'))
|
||||
self.menu_delete.setShortcut(tr('Del'))
|
||||
self.menu_undo.setShortcut(tr('Ctrl+Z'))
|
||||
self.menu_redo.setShortcut(tr('Ctrl+Shift+Z'))
|
||||
self.menu_rescan.setShortcut(tr('Ctrl+R'))
|
||||
self.menu_cherry_pick.setShortcut(tr('Ctrl+P'))
|
||||
self.menu_quit.setShortcut(tr('Ctrl+Q'))
|
||||
self.menu_create_branch.setShortcut(tr('Ctrl+B'))
|
||||
self.menu_checkout_branch.setShortcut(tr('Alt+B'))
|
||||
self.menu_stash.setShortcut(tr('Alt+Shift+S'))
|
||||
self.menu_help_docs.setShortcut(tr('F1'))
|
||||
|
||||
# Arrange dock widgets
|
||||
top = QtCore.Qt.DockWidgetArea(4)
|
||||
bottom = QtCore.Qt.DockWidgetArea(8)
|
||||
|
||||
self.addDockWidget(top, self.commitdockwidget)
|
||||
self.addDockWidget(top, self.classicdockwidget)
|
||||
self.tabifyDockWidget(self.classicdockwidget, self.statusdockwidget)
|
||||
|
||||
self.addDockWidget(bottom, self.actiondockwidget)
|
||||
self.addDockWidget(bottom, self.logdockwidget)
|
||||
self.tabifyDockWidget(self.logdockwidget, self.diffdockwidget)
|
||||
|
||||
# Hide the classic dock by default
|
||||
self.classicdockwidget.toggleViewAction().trigger()
|
||||
|
||||
def create_dock(self, title):
|
||||
"""Create a dock widget and set it up accordingly."""
|
||||
dock = QtGui.QDockWidget(self)
|
||||
dock.setWindowTitle(tr(title))
|
||||
dock.setObjectName(title)
|
||||
return dock
|
||||
|
||||
def create_menu(self, title, parent):
|
||||
"""Create a menu and set its title."""
|
||||
qmenu = QtGui.QMenu(parent)
|
||||
qmenu.setTitle(tr(title))
|
||||
return qmenu
|
||||
|
||||
def create_action(self, title):
|
||||
"""Create an action and set its title."""
|
||||
action = QtGui.QAction(self)
|
||||
action.setText(tr(title))
|
||||
return action
|
||||
|
||||
def create_button(self, text, layout=None):
|
||||
"""Create a button, set its title, and add it to the parent."""
|
||||
button = QtGui.QPushButton()
|
||||
button.setText(tr(text))
|
||||
if layout:
|
||||
layout.addWidget(button)
|
||||
return button
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Save state in the settings manager."""
|
||||
if cola.model().remember_gui_settings():
|
||||
settings.SettingsManager.save_gui_state(self)
|
||||
self.close()
|
||||
@@ -1,114 +0,0 @@
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.QtCore import Qt
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola.qtutils import tr
|
||||
|
||||
class MergeView(QtGui.QDialog):
|
||||
"""Provides a dialog for merging branches."""
|
||||
def __init__(self, parent=None):
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.setWindowTitle(tr('Merge'))
|
||||
self.resize(673, 339)
|
||||
self.vboxlayout = QtGui.QVBoxLayout(self)
|
||||
self.vboxlayout.setMargin(3)
|
||||
|
||||
self.merge_target_label = QtGui.QLabel(self)
|
||||
self.merge_target_label.setAlignment(Qt.AlignCenter)
|
||||
self.vboxlayout.addWidget(self.merge_target_label)
|
||||
|
||||
self.groupbox = QtGui.QGroupBox(self)
|
||||
self.groupbox.setTitle(tr('Revision To Merge'))
|
||||
|
||||
self.gridlayout = QtGui.QGridLayout(self.groupbox)
|
||||
self.gridlayout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
self.revision_label = QtGui.QLabel(self.groupbox)
|
||||
self.revision_label.setText(tr('Revision Expression:'))
|
||||
self.gridlayout.addWidget(self.revision_label, 0, 0, 1, 1)
|
||||
|
||||
self.revision = QtGui.QLineEdit(self.groupbox)
|
||||
self.gridlayout.addWidget(self.revision, 0, 1, 1, 2)
|
||||
|
||||
self.radio_local = QtGui.QRadioButton(self.groupbox)
|
||||
self.radio_local.setText(tr('Local Branch'))
|
||||
self.radio_local.setChecked(True)
|
||||
self.gridlayout.addWidget(self.radio_local, 1, 0, 1, 1)
|
||||
|
||||
self.radio_remote = QtGui.QRadioButton(self.groupbox)
|
||||
self.radio_remote.setText(tr('Tracking Branch'))
|
||||
self.gridlayout.addWidget(self.radio_remote, 1, 1, 1, 1)
|
||||
|
||||
self.radio_tag = QtGui.QRadioButton(self.groupbox)
|
||||
self.radio_tag.setText(tr('Tag'))
|
||||
self.gridlayout.addWidget(self.radio_tag, 1, 2, 1, 1)
|
||||
|
||||
self.revisions = QtGui.QListWidget(self.groupbox)
|
||||
self.revisions.setAlternatingRowColors(True)
|
||||
self.gridlayout.addWidget(self.revisions, 2, 0, 1, 3)
|
||||
|
||||
self.vboxlayout.addWidget(self.groupbox)
|
||||
|
||||
self.hboxlayout = QtGui.QHBoxLayout()
|
||||
|
||||
self.button_viz = QtGui.QPushButton(self)
|
||||
self.button_viz.setText(tr('Visualize'))
|
||||
self.hboxlayout.addWidget(self.button_viz)
|
||||
|
||||
self.spacer = QtGui.QSpacerItem(231, 24, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.hboxlayout.addItem(self.spacer)
|
||||
|
||||
self.checkbox_squash = QtGui.QCheckBox(self)
|
||||
self.checkbox_squash.setText(tr('Squash'))
|
||||
self.hboxlayout.addWidget(self.checkbox_squash)
|
||||
|
||||
self.checkbox_commit = QtGui.QCheckBox(self)
|
||||
self.checkbox_commit.setText(tr('Commit'))
|
||||
self.checkbox_commit.setChecked(True)
|
||||
self.checkbox_commit_state = True
|
||||
self.hboxlayout.addWidget(self.checkbox_commit)
|
||||
|
||||
self.button_cancel = QtGui.QPushButton(self)
|
||||
self.button_cancel.setText(tr('Cancel'))
|
||||
self.hboxlayout.addWidget(self.button_cancel)
|
||||
|
||||
self.button_merge = QtGui.QPushButton(self)
|
||||
self.button_merge.setText(tr('Merge'))
|
||||
self.hboxlayout.addWidget(self.button_merge)
|
||||
|
||||
self.vboxlayout.addLayout(self.hboxlayout)
|
||||
|
||||
self.connect(self.button_cancel, SIGNAL('clicked()'),
|
||||
self.reject)
|
||||
|
||||
self.connect(self.checkbox_squash, SIGNAL('clicked()'),
|
||||
self.toggle_squash)
|
||||
|
||||
self.set_branch('master')
|
||||
self.revision.setFocus()
|
||||
|
||||
def set_branch(self, branch):
|
||||
"""Set the branch name for the window title and label."""
|
||||
txt = unicode(tr('Merge into %s')) % branch
|
||||
self.merge_target_label.setText(txt)
|
||||
|
||||
def toggle_squash(self):
|
||||
"""Toggles the commit checkbox based on the squash checkbox."""
|
||||
if self.checkbox_squash.isChecked():
|
||||
self.checkbox_commit_state =\
|
||||
self.checkbox_commit.checkState()
|
||||
self.checkbox_commit.setCheckState(Qt.Unchecked)
|
||||
self.checkbox_commit.setDisabled(True)
|
||||
else:
|
||||
self.checkbox_commit.setDisabled(False)
|
||||
oldstate = self.checkbox_commit_state
|
||||
self.checkbox_commit.setCheckState(oldstate)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
merge = MergeView()
|
||||
merge.raise_()
|
||||
merge.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -1,246 +0,0 @@
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola.views import standard
|
||||
|
||||
|
||||
class OptionsView(standard.StandardDialog):
|
||||
def __init__(self, parent=None):
|
||||
standard.StandardDialog.__init__(self, parent=parent)
|
||||
#self.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
self.setWindowTitle(self.tr('Options'))
|
||||
self.resize(608, 425)
|
||||
|
||||
self._main_layt = QtGui.QVBoxLayout(self)
|
||||
self._main_grid = QtGui.QGridLayout()
|
||||
self._diff_font_label = QtGui.QLabel(self)
|
||||
self._diff_font_label.setText(self.tr('Diff/Console Font'))
|
||||
self._main_grid.addWidget(self._diff_font_label, 0, 0, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_cola_fontdiff_size = QtGui.QSpinBox(self)
|
||||
self.global_cola_fontdiff_size.setMinimum(8)
|
||||
self.global_cola_fontdiff_size.setProperty('value', QtCore.QVariant(12))
|
||||
self._main_grid.addWidget(self.global_cola_fontdiff_size, 0, 1, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_cola_fontdiff = QtGui.QFontComboBox(self)
|
||||
self.global_cola_fontdiff.setFontFilters(QtGui.QFontComboBox.MonospacedFonts)
|
||||
self._main_grid.addWidget(self.global_cola_fontdiff, 0, 2, 1, 2)
|
||||
|
||||
self._tab_width_label = QtGui.QLabel(self)
|
||||
self._tab_width_label.setText(self.tr('Tab Width'))
|
||||
self._main_grid.addWidget(self._tab_width_label, 1, 0, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_cola_tabwidth = QtGui.QSpinBox(self)
|
||||
self.global_cola_tabwidth.setWrapping(True)
|
||||
self.global_cola_tabwidth.setMaximum(42)
|
||||
self.global_cola_tabwidth.setProperty('value', QtCore.QVariant(8))
|
||||
self._main_grid.addWidget(self.global_cola_tabwidth, 1, 1, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_cola_savewindowsettings = QtGui.QCheckBox(self)
|
||||
self.global_cola_savewindowsettings.setText(self.tr('Save GUI Settings'))
|
||||
self._main_grid.addWidget(self.global_cola_savewindowsettings, 1, 2, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_merge_keepbackup = QtGui.QCheckBox(self)
|
||||
self.global_merge_keepbackup.setText(self.tr('Save *.orig Merge Backups'))
|
||||
self._main_grid.addWidget(self.global_merge_keepbackup, 1, 3, 1, 1)
|
||||
|
||||
self._editor_label = QtGui.QLabel(self)
|
||||
self._editor_label.setText(self.tr('Editor'))
|
||||
self._main_grid.addWidget(self._editor_label, 2, 0, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_gui_editor = QtGui.QLineEdit(self)
|
||||
self._main_grid.addWidget(self.global_gui_editor, 2, 1, 1, 1)
|
||||
|
||||
self._difftool_label = QtGui.QLabel(self)
|
||||
self._difftool_label.setText(self.tr('Diff Tool'))
|
||||
self._main_grid.addWidget(self._difftool_label, 2, 2, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_diff_tool = QtGui.QLineEdit(self)
|
||||
self._main_grid.addWidget(self.global_diff_tool, 2, 3, 1, 1)
|
||||
|
||||
self._historybrowser_label = QtGui.QLabel(self)
|
||||
self._historybrowser_label.setText(self.tr('History Browser'))
|
||||
self._main_grid.addWidget(self._historybrowser_label, 3, 0, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_gui_historybrowser = QtGui.QLineEdit(self)
|
||||
self._main_grid.addWidget(self.global_gui_historybrowser,
|
||||
3, 1, 1, 1)
|
||||
|
||||
self._mergetool_label = QtGui.QLabel(self)
|
||||
self._mergetool_label.setText(self.tr('Merge Tool'))
|
||||
self._main_grid.addWidget(self._mergetool_label, 3, 2, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_merge_tool = QtGui.QLineEdit(self)
|
||||
self._main_grid.addWidget(self.global_merge_tool, 3, 3, 1, 1)
|
||||
|
||||
self._main_layt.addLayout(self._main_grid)
|
||||
|
||||
self._repo_horiz_layt = QtGui.QHBoxLayout()
|
||||
self._repo_horiz_layt.setSpacing(3)
|
||||
|
||||
# Exposed
|
||||
self.local_groupbox = QtGui.QGroupBox(self)
|
||||
self.local_groupbox.setTitle(self.tr('Local Repository'))
|
||||
|
||||
self._local_repo_layt = QtGui.QGridLayout(self.local_groupbox)
|
||||
|
||||
self._local_user_name_label = QtGui.QLabel(self.local_groupbox)
|
||||
self._local_user_name_label.setText(self.tr('User Name'))
|
||||
self._local_repo_layt.addWidget(self._local_user_name_label,
|
||||
0, 0, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.local_user_name = QtGui.QLineEdit(self.local_groupbox)
|
||||
|
||||
self._local_repo_layt.addWidget(self.local_user_name,
|
||||
0, 1, 1, 2)
|
||||
|
||||
self._local_user_email_label = QtGui.QLabel(self.local_groupbox)
|
||||
self._local_user_email_label.setText(self.tr('Email Address'))
|
||||
self._local_repo_layt.addWidget(self._local_user_email_label,
|
||||
1, 0, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.local_user_email = QtGui.QLineEdit(self.local_groupbox)
|
||||
self._local_repo_layt.addWidget(self.local_user_email,
|
||||
1, 1, 1, 2)
|
||||
|
||||
self._local_merge_verb_label = QtGui.QLabel(self.local_groupbox)
|
||||
self._local_merge_verb_label.setText(self.tr('Merge Verbosity'))
|
||||
self._local_repo_layt.addWidget(self._local_merge_verb_label,
|
||||
2, 0, 1, 2)
|
||||
|
||||
# Exposed
|
||||
self.local_merge_verbosity = QtGui.QSpinBox(self.local_groupbox)
|
||||
self.local_merge_verbosity.setMinimum(0)
|
||||
self.local_merge_verbosity.setMaximum(5)
|
||||
self.local_merge_verbosity.setProperty('value', QtCore.QVariant(5))
|
||||
self._local_repo_layt.addWidget(self.local_merge_verbosity,
|
||||
2, 2, 1, 1)
|
||||
|
||||
self._local_diffctxt_label = QtGui.QLabel(self.local_groupbox)
|
||||
self._local_diffctxt_label.setText(self.tr('Number of Diff Context Lines'))
|
||||
self._local_repo_layt.addWidget(self._local_diffctxt_label,
|
||||
3, 0, 1, 2)
|
||||
|
||||
# Exposed
|
||||
self.local_gui_diffcontext = QtGui.QSpinBox(self.local_groupbox)
|
||||
self.local_gui_diffcontext.setMinimum(2)
|
||||
self.local_gui_diffcontext.setProperty('value', QtCore.QVariant(5))
|
||||
self._local_repo_layt.addWidget(self.local_gui_diffcontext,
|
||||
3, 2, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.local_merge_summary = QtGui.QCheckBox(self.local_groupbox)
|
||||
self.local_merge_summary.setText(self.tr('Summarize Merge Commits'))
|
||||
self._local_repo_layt.addWidget(self.local_merge_summary,
|
||||
4, 0, 1, 2)
|
||||
|
||||
# Exposed
|
||||
self.local_merge_diffstat = QtGui.QCheckBox(self.local_groupbox)
|
||||
self.local_merge_diffstat.setText(self.tr('Show Diffstat After Merge'))
|
||||
self._local_repo_layt.addWidget(self.local_merge_diffstat,
|
||||
5, 0, 1, 2)
|
||||
|
||||
self._repo_horiz_layt.addWidget(self.local_groupbox)
|
||||
|
||||
self._global_repo_grp = QtGui.QGroupBox(self)
|
||||
self._global_repo_grp.setTitle(self.tr('Global (All Repositories)'))
|
||||
|
||||
self._global_repo_lyt = QtGui.QGridLayout(self._global_repo_grp)
|
||||
self._global_user_name_label = QtGui.QLabel(self._global_repo_grp)
|
||||
self._global_user_name_label.setText(self.tr('User Name'))
|
||||
self._global_repo_lyt.addWidget(self._global_user_name_label,
|
||||
0, 0, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_user_name = QtGui.QLineEdit(self._global_repo_grp)
|
||||
self._global_repo_lyt.addWidget(self.global_user_name,
|
||||
0, 1, 1, 2)
|
||||
|
||||
self._global_user_email_label = QtGui.QLabel(self._global_repo_grp)
|
||||
self._global_user_email_label.setText(self.tr('Email Address'))
|
||||
self._global_repo_lyt.addWidget(self._global_user_email_label,
|
||||
1, 0, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_user_email = QtGui.QLineEdit(self._global_repo_grp)
|
||||
self._global_repo_lyt.addWidget(self.global_user_email,
|
||||
1, 1, 1, 2)
|
||||
|
||||
self._global_merge_verb_label = QtGui.QLabel(self._global_repo_grp)
|
||||
self._global_merge_verb_label.setText(self.tr('Merge Verbosity'))
|
||||
self._global_repo_lyt.addWidget(self._global_merge_verb_label,
|
||||
2, 0, 1, 2)
|
||||
|
||||
# Exposed
|
||||
self.global_merge_verbosity = QtGui.QSpinBox(self._global_repo_grp)
|
||||
self.global_merge_verbosity.setMinimum(0)
|
||||
self.global_merge_verbosity.setMaximum(5)
|
||||
self.global_merge_verbosity.setProperty('value', QtCore.QVariant(5))
|
||||
self._global_repo_lyt.addWidget(self.global_merge_verbosity,
|
||||
2, 2, 1, 1)
|
||||
|
||||
self._global_diffctxt_label = QtGui.QLabel(self._global_repo_grp)
|
||||
self._global_diffctxt_label.setText(
|
||||
self.tr('Number of Diff Context Lines'))
|
||||
self._global_repo_lyt.addWidget(self._global_diffctxt_label,
|
||||
3, 0, 1, 2)
|
||||
|
||||
# Exposed
|
||||
self.global_gui_diffcontext = QtGui.QSpinBox(self._global_repo_grp)
|
||||
self.global_gui_diffcontext.setMinimum(2)
|
||||
self.global_gui_diffcontext.setProperty('value', QtCore.QVariant(5))
|
||||
self._global_repo_lyt.addWidget(self.global_gui_diffcontext, 3, 2, 1, 1)
|
||||
|
||||
# Exposed
|
||||
self.global_merge_summary = QtGui.QCheckBox(self._global_repo_grp)
|
||||
self.global_merge_summary.setText(self.tr('Summarize Merge Commits'))
|
||||
self._global_repo_lyt.addWidget(self.global_merge_summary, 4, 0, 1, 2)
|
||||
|
||||
# Exposed
|
||||
self.global_merge_diffstat = QtGui.QCheckBox(self._global_repo_grp)
|
||||
self.global_merge_diffstat.setText(self.tr('Show Diffstat After Merge'))
|
||||
self._global_repo_lyt.addWidget(self.global_merge_diffstat, 5, 0, 1, 2)
|
||||
|
||||
self._repo_horiz_layt.addWidget(self._global_repo_grp)
|
||||
self._main_layt.addLayout(self._repo_horiz_layt)
|
||||
|
||||
# Save/Cancel buttons
|
||||
self._button_layt = QtGui.QHBoxLayout()
|
||||
self._button_spacer = QtGui.QSpacerItem(1, 1,
|
||||
QtGui.QSizePolicy.Expanding,
|
||||
QtGui.QSizePolicy.Minimum)
|
||||
self._button_layt.addItem(self._button_spacer)
|
||||
|
||||
# Exposed
|
||||
self.save_button = QtGui.QPushButton(self)
|
||||
self.save_button.setText(self.tr('Save'))
|
||||
self.save_button.setDefault(True)
|
||||
self._button_layt.addWidget(self.save_button)
|
||||
|
||||
# Exposed
|
||||
self.cancel_button = QtGui.QPushButton(self)
|
||||
self.cancel_button.setText(self.tr('Cancel'))
|
||||
self._button_layt.addWidget(self.cancel_button)
|
||||
self._main_layt.addLayout(self._button_layt)
|
||||
|
||||
self.connect(self.cancel_button, SIGNAL('released()'), self.reject)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
options = OptionsView()
|
||||
options.show()
|
||||
sys.exit(app.exec_())
|
||||
+55
-26
@@ -2,18 +2,20 @@ from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola import qtutils
|
||||
from cola.views import standard
|
||||
|
||||
|
||||
class RemoteView(standard.StandardDialog):
|
||||
class RemoteView(standard.Dialog):
|
||||
"""A dialog for choosing branches."""
|
||||
|
||||
def __init__(self, parent, action):
|
||||
"""Customizes the dialog based on the remote action
|
||||
"""
|
||||
standard.StandardDialog.__init__(self, parent=parent)
|
||||
standard.Dialog.__init__(self, parent=parent)
|
||||
self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
|
||||
self.resize(550, 512)
|
||||
self.resize(666, 420)
|
||||
self._main_vbox_layt = QtGui.QVBoxLayout(self)
|
||||
|
||||
# Local branch section
|
||||
@@ -21,40 +23,27 @@ class RemoteView(standard.StandardDialog):
|
||||
# Exposed
|
||||
self.local_label = QtGui.QLabel(self)
|
||||
self.local_label.setText(self.tr('Local Branches'))
|
||||
self._local_branch_hbox_layt.addWidget(self.local_label)
|
||||
# Exposed
|
||||
self.local_branch = QtGui.QLineEdit(self)
|
||||
self._local_branch_hbox_layt.addWidget(self.local_branch)
|
||||
self._main_vbox_layt.addLayout(self._local_branch_hbox_layt)
|
||||
# Exposed
|
||||
self.local_branches = QtGui.QListWidget(self)
|
||||
self._main_vbox_layt.addWidget(self.local_branches)
|
||||
|
||||
# Remote branch section
|
||||
self._remote_branch_hbox_layt = QtGui.QHBoxLayout()
|
||||
self._remote_label = QtGui.QLabel(self)
|
||||
self._remote_label.setText(self.tr('Remote'))
|
||||
self._remote_branch_hbox_layt.addWidget(self._remote_label)
|
||||
# Exposed
|
||||
self.remotename = QtGui.QLineEdit(self)
|
||||
self._remote_branch_hbox_layt.addWidget(self.remotename)
|
||||
self._main_vbox_layt.addLayout(self._remote_branch_hbox_layt)
|
||||
# Exposed
|
||||
self.remotes = QtGui.QListWidget(self)
|
||||
self._main_vbox_layt.addWidget(self.remotes)
|
||||
|
||||
self._remote_branches_hbox_layt = QtGui.QHBoxLayout()
|
||||
# Exposed
|
||||
self.remote_label = QtGui.QLabel(self)
|
||||
self.remote_label.setText(self.tr('Remote Branch'))
|
||||
self._remote_branches_hbox_layt.addWidget(self.remote_label)
|
||||
# Exposed
|
||||
self.remote_branch = QtGui.QLineEdit(self)
|
||||
self._remote_branches_hbox_layt.addWidget(self.remote_branch)
|
||||
self._main_vbox_layt.addLayout(self._remote_branches_hbox_layt)
|
||||
|
||||
self.remote_branches = QtGui.QListWidget(self)
|
||||
self._main_vbox_layt.addWidget(self.remote_branches)
|
||||
|
||||
self._options_hbox_layt = QtGui.QHBoxLayout()
|
||||
# Exposed
|
||||
@@ -62,14 +51,13 @@ class RemoteView(standard.StandardDialog):
|
||||
self.ffwd_only_checkbox.setText(self.tr('Fast Forward Only'))
|
||||
self.ffwd_only_checkbox.setChecked(True)
|
||||
self.ffwd_only_checkbox.setObjectName("ffwd_only_checkbox")
|
||||
self._options_hbox_layt.addWidget(self.ffwd_only_checkbox)
|
||||
|
||||
# Exposed
|
||||
self.tags_checkbox = QtGui.QCheckBox(self)
|
||||
self.tags_checkbox.setText(self.tr('Include tags'))
|
||||
self._options_hbox_layt.addWidget(self.tags_checkbox)
|
||||
|
||||
self.rebase_checkbox = QtGui.QCheckBox(self)
|
||||
self.rebase_checkbox.setText(self.tr('Rebase'))
|
||||
self._options_hbox_layt.addWidget(self.rebase_checkbox)
|
||||
|
||||
self._options_spacer = QtGui.QSpacerItem(1, 1,
|
||||
QtGui.QSizePolicy.Expanding,
|
||||
@@ -78,25 +66,66 @@ class RemoteView(standard.StandardDialog):
|
||||
# Exposed
|
||||
self.action_button = QtGui.QPushButton(self)
|
||||
self.action_button.setText(self.tr('Push'))
|
||||
self._options_hbox_layt.addWidget(self.action_button)
|
||||
self.action_button.setIcon(qtutils.ok_icon())
|
||||
# Exposed
|
||||
self.cancel_button = QtGui.QPushButton(self)
|
||||
self.cancel_button.setText(self.tr('Cancel'))
|
||||
self._options_hbox_layt.addWidget(self.cancel_button)
|
||||
self._main_vbox_layt.addLayout(self._options_hbox_layt)
|
||||
self.close_button = QtGui.QPushButton(self)
|
||||
self.close_button.setText(self.tr('Close'))
|
||||
self.close_button.setIcon(qtutils.close_icon())
|
||||
|
||||
# connections
|
||||
self.connect(self.close_button, SIGNAL('released()'), self.reject)
|
||||
|
||||
if action:
|
||||
self.action_button.setText(action.title())
|
||||
self.setWindowTitle(action.title())
|
||||
|
||||
if action == 'pull':
|
||||
self.tags_checkbox.hide()
|
||||
self.ffwd_only_checkbox.hide()
|
||||
self.local_label.hide()
|
||||
self.local_branch.hide()
|
||||
self.local_branches.hide()
|
||||
if action != 'pull':
|
||||
self.remote_branch.setFocus()
|
||||
else:
|
||||
self.rebase_checkbox.hide()
|
||||
|
||||
self.connect(self.cancel_button, SIGNAL('released()'), self.reject)
|
||||
self.remotename.setFocus()
|
||||
self.layout_remotes()
|
||||
|
||||
if action == 'push':
|
||||
self.layout_local_branches()
|
||||
self.layout_remote_branches()
|
||||
else: # fetch and pull
|
||||
self.layout_remote_branches()
|
||||
self.layout_local_branches()
|
||||
|
||||
self.layout_options()
|
||||
|
||||
def layout_options(self):
|
||||
self._options_hbox_layt.addWidget(self.ffwd_only_checkbox)
|
||||
self._options_hbox_layt.addWidget(self.tags_checkbox)
|
||||
self._options_hbox_layt.addWidget(self.rebase_checkbox)
|
||||
self._options_hbox_layt.addWidget(self.action_button)
|
||||
self._options_hbox_layt.addWidget(self.close_button)
|
||||
self._main_vbox_layt.addLayout(self._options_hbox_layt)
|
||||
|
||||
def layout_local_branches(self):
|
||||
self._local_branch_hbox_layt.addWidget(self.local_label)
|
||||
self._local_branch_hbox_layt.addWidget(self.local_branch)
|
||||
self._main_vbox_layt.addLayout(self._local_branch_hbox_layt)
|
||||
self._main_vbox_layt.addWidget(self.local_branches)
|
||||
|
||||
def layout_remotes(self):
|
||||
self._remote_branch_hbox_layt.addWidget(self._remote_label)
|
||||
self._remote_branch_hbox_layt.addWidget(self.remotename)
|
||||
self._main_vbox_layt.addLayout(self._remote_branch_hbox_layt)
|
||||
self._main_vbox_layt.addWidget(self.remotes)
|
||||
|
||||
def layout_remote_branches(self):
|
||||
self._remote_branches_hbox_layt.addWidget(self.remote_label)
|
||||
self._remote_branches_hbox_layt.addWidget(self.remote_branch)
|
||||
self._main_vbox_layt.addLayout(self._remote_branches_hbox_layt)
|
||||
self._main_vbox_layt.addWidget(self.remote_branches)
|
||||
|
||||
def select_first_remote(self):
|
||||
"""Selects the first remote in the list view"""
|
||||
|
||||
@@ -4,14 +4,14 @@ from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola import qtutils
|
||||
from cola.prefs import diff_font
|
||||
from cola.qt import DiffSyntaxHighlighter
|
||||
from cola.views import standard
|
||||
from cola.views import syntax
|
||||
|
||||
|
||||
class SearchView(standard.StandardDialog):
|
||||
class SearchView(standard.Dialog):
|
||||
def __init__(self, parent=None):
|
||||
standard.StandardDialog.__init__(self, parent=parent)
|
||||
standard.Dialog.__init__(self, parent=parent)
|
||||
|
||||
self.setWindowTitle(self.tr('Search'))
|
||||
if self.parent():
|
||||
@@ -105,9 +105,9 @@ class SearchView(standard.StandardDialog):
|
||||
self.commit_text.setTabChangesFocus(True)
|
||||
self.commit_text.setReadOnly(True)
|
||||
self.commit_text.setAcceptRichText(False)
|
||||
qtutils.set_diff_font(self.commit_text)
|
||||
self._syntax = syntax.DiffSyntaxHighlighter(self.commit_text.document(),
|
||||
whitespace=False)
|
||||
self.commit_text.setFont(diff_font())
|
||||
self._syntax = DiffSyntaxHighlighter(self.commit_text.document(),
|
||||
whitespace=False)
|
||||
self._main_vbox_layt.addWidget(self.splitter)
|
||||
|
||||
self._button_grid_layt = QtGui.QGridLayout()
|
||||
|
||||
@@ -2,8 +2,8 @@ from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
|
||||
from cola import qtutils
|
||||
from cola.views.syntax import DiffSyntaxHighlighter
|
||||
from cola.prefs import diff_font
|
||||
from cola.qt import DiffSyntaxHighlighter
|
||||
|
||||
class SelectCommitsView(QtGui.QDialog):
|
||||
def __init__(self,
|
||||
@@ -80,4 +80,4 @@ class SelectCommitsView(QtGui.QDialog):
|
||||
|
||||
# Set the console font
|
||||
if syntax:
|
||||
qtutils.set_diff_font(self.commit_text)
|
||||
self.commit_text.setFont(diff_font())
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4 import QtGui
|
||||
|
||||
from cola.views import syntax
|
||||
from cola import qt
|
||||
|
||||
|
||||
def create_standard_widget(qtclass):
|
||||
@@ -12,7 +11,7 @@ def create_standard_widget(qtclass):
|
||||
def __init__(self, parent=None):
|
||||
self._qtclass = qtclass
|
||||
self._qtclass.__init__(self, parent)
|
||||
syntax.set_theme_properties(self)
|
||||
qt.set_theme_properties(self)
|
||||
|
||||
def show(self):
|
||||
"""Automatically centers and raises dialogs"""
|
||||
@@ -26,14 +25,14 @@ def create_standard_widget(qtclass):
|
||||
|
||||
self.move(x, y)
|
||||
# Call the base Qt show()
|
||||
self._qtclass.show(self)
|
||||
self.raise_()
|
||||
self._qtclass.show(self)
|
||||
|
||||
def name(self):
|
||||
"""Returns the name of the view class"""
|
||||
return self.__class__.__name__.lower()
|
||||
|
||||
def import_state(self, settings):
|
||||
def apply_state(self, settings):
|
||||
"""Imports data for view save/restore"""
|
||||
if 'width' in settings and 'height' in settings:
|
||||
w = settings.get('width')
|
||||
@@ -61,7 +60,7 @@ def create_standard_widget(qtclass):
|
||||
def style_properties(self):
|
||||
# user-definable color properties
|
||||
props = {}
|
||||
for name in syntax.default_colors:
|
||||
for name in qt.default_colors:
|
||||
props[name] = getattr(self, '_'+name)
|
||||
return props
|
||||
|
||||
@@ -70,9 +69,10 @@ def create_standard_widget(qtclass):
|
||||
self.syntax.set_colors(self.style_properties())
|
||||
self.syntax.reset()
|
||||
|
||||
syntax.install_style_properties(StandardWidget)
|
||||
qt.install_style_properties(StandardWidget)
|
||||
return StandardWidget
|
||||
|
||||
|
||||
# The base class for all cola QDialogs.
|
||||
StandardDialog = create_standard_widget(QtGui.QDialog)
|
||||
Dialog = create_standard_widget(QtGui.QDialog)
|
||||
MainWindow = create_standard_widget(QtGui.QMainWindow)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user