From ed4a943875d81732bfa3127eb252c2db2e3588f4 Mon Sep 17 00:00:00 2001 From: Gianluca Montecchi Date: Thu, 1 Oct 2009 23:39:28 +0200 Subject: [PATCH] Merged with head branch --- AUTHORS | 1 + .../Bugs-Everywhere-Web.egg-info/not-zip-safe | 0 Bugs-Everywhere-Web/beweb/__init__.py | 0 .../beweb/static/images/ds-b.png | Bin 213 -> 0 bytes .../beweb/static/images/ds-bl.png | Bin 327 -> 0 bytes .../beweb/static/images/ds-br.png | Bin 365 -> 0 bytes .../beweb/static/images/ds-l.png | Bin 197 -> 0 bytes .../beweb/static/images/ds-r.png | Bin 214 -> 0 bytes .../beweb/static/images/ds-t.png | Bin 200 -> 0 bytes .../beweb/static/images/ds-tl.png | Bin 240 -> 0 bytes .../beweb/static/images/ds-tr.png | Bin 311 -> 0 bytes .../beweb/static/images/ds2-b.png | Bin 206 -> 0 bytes .../beweb/static/images/ds2-r.png | Bin 204 -> 0 bytes .../beweb/static/images/favicon.ico | Bin 318 -> 0 bytes .../beweb/static/images/favicon.png | Bin 267 -> 0 bytes .../beweb/static/images/half-spiral.png | Bin 1112 -> 0 bytes .../beweb/static/images/header_inner.png | Bin 37537 -> 0 bytes .../beweb/static/images/info.png | Bin 2889 -> 0 bytes .../beweb/static/images/is-b.png | Bin 200 -> 0 bytes .../beweb/static/images/is-bl.png | Bin 408 -> 0 bytes .../beweb/static/images/is-br.png | Bin 304 -> 0 bytes .../beweb/static/images/is-l.png | Bin 214 -> 0 bytes .../beweb/static/images/is-r.png | Bin 197 -> 0 bytes .../beweb/static/images/is-t.png | Bin 213 -> 0 bytes .../beweb/static/images/is-tl.png | Bin 413 -> 0 bytes .../beweb/static/images/is-tr.png | Bin 414 -> 0 bytes .../beweb/static/images/ok.png | Bin 25753 -> 0 bytes .../beweb/static/images/shadows.png | Bin 3960 -> 0 bytes .../beweb/static/images/spiral.png | Bin 2120 -> 0 bytes .../beweb/static/images/tg_under_the_hood.png | Bin 4010 -> 0 bytes .../static/images/under_the_hood_blue.png | Bin 2667 -> 0 bytes .../beweb/templates/__init__.py | 0 Bugs-Everywhere-Web/beweb/tests/__init__.py | 0 Bugs-Everywhere-Web/libbe | 1 - COPYING | 28 +- Makefile | 24 +- README | 18 +- README.dev | 10 + be | 111 ++- becommands/assign.py | 29 +- becommands/close.py | 23 +- becommands/comment.py | 102 +- becommands/commit.py | 77 ++ becommands/depend.py | 23 +- becommands/diff.py | 39 +- becommands/help.py | 23 +- becommands/init.py | 23 +- becommands/list.py | 26 +- becommands/merge.py | 31 +- becommands/new.py | 42 +- becommands/open.py | 24 +- becommands/remove.py | 24 +- becommands/set.py | 70 +- becommands/severity.py | 24 +- becommands/show.py | 82 +- becommands/status.py | 63 +- becommands/tag.py | 25 +- becommands/target.py | 28 +- doc/module.mk | 18 +- {xml => interfaces/email}/catmutt | 0 interfaces/gui/beg/beg | 12 + interfaces/gui/beg/table.py | 97 ++ interfaces/gui/wxbe/wxbe | 87 ++ .../Bugs-Everywhere-Web.egg-info/SOURCES.txt | 0 .../Bugs-Everywhere-Web.egg-info/requires.txt | 0 .../sqlobject.txt | 0 .../top_level.txt | 0 .../Bugs_Everywhere_Web.egg-info/PKG-INFO | 0 .../Bugs_Everywhere_Web.egg-info/SOURCES.txt | 0 .../dependency_links.txt | 0 .../Bugs_Everywhere_Web.egg-info/not-zip-safe | 0 .../paster_plugins.txt | 0 .../Bugs_Everywhere_Web.egg-info/requires.txt | 0 .../sqlobject.txt | 0 .../top_level.txt | 0 .../web/Bugs-Everywhere-Web}/README.txt | 0 .../web/Bugs-Everywhere-Web}/beweb/app.cfg | 0 .../beweb/config.py.example | 0 .../Bugs-Everywhere-Web}/beweb/config/app.cfg | 0 .../Bugs-Everywhere-Web}/beweb/config/log.cfg | 0 .../Bugs-Everywhere-Web}/beweb/controllers.py | 0 .../Bugs-Everywhere-Web}/beweb/formatting.py | 0 .../web/Bugs-Everywhere-Web}/beweb/json.py | 0 .../web/Bugs-Everywhere-Web}/beweb/model.py | 0 .../web/Bugs-Everywhere-Web}/beweb/prest.py | 0 .../web/Bugs-Everywhere-Web}/beweb/release.py | 0 .../beweb/static/css/style.css | 0 .../beweb/templates/about.kid | 0 .../beweb/templates/bugs.kid | 0 .../beweb/templates/edit_bug.kid | 0 .../beweb/templates/edit_comment.kid | 0 .../beweb/templates/error.kid | 0 .../beweb/templates/login.kid | 0 .../beweb/templates/master.kid | 0 .../beweb/templates/projects.kid | 0 .../beweb/templates/welcome.kid | 0 .../beweb/tests/test_controllers.py | 0 .../beweb/tests/test_model.py | 0 .../web/Bugs-Everywhere-Web}/dev.cfg | 0 .../web/Bugs-Everywhere-Web/libbe/arch.py | 295 ++++++ .../web/Bugs-Everywhere-Web/libbe/beuuid.py | 61 ++ .../web/Bugs-Everywhere-Web/libbe/bug.py | 547 +++++++++++ .../web/Bugs-Everywhere-Web/libbe/bugdir.py | 676 ++++++++++++++ .../web/Bugs-Everywhere-Web/libbe/bzr.py | 101 ++ .../web/Bugs-Everywhere-Web/libbe/cmdutil.py | 218 +++++ .../web/Bugs-Everywhere-Web/libbe/comment.py | 662 +++++++++++++ .../web/Bugs-Everywhere-Web/libbe/config.py | 83 ++ .../web/Bugs-Everywhere-Web/libbe/darcs.py | 163 ++++ .../web/Bugs-Everywhere-Web/libbe/diff.py | 125 +++ .../web/Bugs-Everywhere-Web/libbe/editor.py | 101 ++ .../web/Bugs-Everywhere-Web/libbe/encoding.py | 51 + .../web/Bugs-Everywhere-Web/libbe/git.py | 120 +++ .../web/Bugs-Everywhere-Web/libbe/hg.py | 96 ++ .../web/Bugs-Everywhere-Web/libbe/mapfile.py | 127 +++ .../web/Bugs-Everywhere-Web/libbe/plugin.py | 71 ++ .../Bugs-Everywhere-Web/libbe/properties.py | 638 +++++++++++++ .../web/Bugs-Everywhere-Web/libbe/rcs.py | 876 ++++++++++++++++++ .../libbe/settings_object.py | 417 +++++++++ .../web/Bugs-Everywhere-Web/libbe/tree.py | 179 ++++ .../web/Bugs-Everywhere-Web/libbe/utility.py | 129 +++ .../web/Bugs-Everywhere-Web}/prod.cfg | 0 .../web/Bugs-Everywhere-Web}/sample-prod.cfg | 0 .../web/Bugs-Everywhere-Web}/server.log | 0 .../web/Bugs-Everywhere-Web}/setup-tables.py | 0 .../web/Bugs-Everywhere-Web}/setup.py | 0 .../web/Bugs-Everywhere-Web}/start-beweb.py | 0 interfaces/xml/be-mbox-to-xml | 130 +++ {xml => interfaces/xml}/be-xml-to-mbox | 179 ++-- libbe/arch.py | 295 ------ libbe/beuuid.py | 62 -- libbe/bug.py | 527 ----------- libbe/bugdir.py | 628 ------------- libbe/bzr.py | 104 --- libbe/cmdutil.py | 191 ---- libbe/comment.py | 538 ----------- libbe/config.py | 84 -- libbe/darcs.py | 162 ---- libbe/diff.py | 117 --- libbe/editor.py | 103 -- libbe/encoding.py | 53 -- libbe/git.py | 111 --- libbe/hg.py | 91 -- libbe/mapfile.py | 128 --- libbe/plugin.py | 72 -- libbe/properties.py | 614 ------------ libbe/rcs.py | 838 ----------------- libbe/settings_object.py | 411 -------- libbe/tree.py | 161 ---- libbe/utility.py | 93 -- {completion => misc/completion}/be.bash | 0 misc/gui/beg | 12 - misc/gui/table.py | 97 -- misc/gui/wxbe | 87 -- setup.py | 8 +- test_usage.sh | 15 +- update_copyright.sh | 57 +- 156 files changed, 6851 insertions(+), 6038 deletions(-) delete mode 100644 Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/not-zip-safe delete mode 100644 Bugs-Everywhere-Web/beweb/__init__.py delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/ds-b.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/ds-bl.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/ds-br.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/ds-l.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/ds-r.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/ds-t.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/ds-tl.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/ds-tr.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/ds2-b.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/ds2-r.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/favicon.ico delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/favicon.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/half-spiral.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/header_inner.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/info.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/is-b.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/is-bl.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/is-br.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/is-l.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/is-r.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/is-t.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/is-tl.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/is-tr.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/ok.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/shadows.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/spiral.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/tg_under_the_hood.png delete mode 100644 Bugs-Everywhere-Web/beweb/static/images/under_the_hood_blue.png delete mode 100644 Bugs-Everywhere-Web/beweb/templates/__init__.py delete mode 100644 Bugs-Everywhere-Web/beweb/tests/__init__.py delete mode 120000 Bugs-Everywhere-Web/libbe create mode 100644 becommands/commit.py rename {xml => interfaces/email}/catmutt (100%) mode change 100755 => 100644 create mode 100644 interfaces/gui/beg/beg create mode 100644 interfaces/gui/beg/table.py create mode 100644 interfaces/gui/wxbe/wxbe rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/PKG-INFO (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/SOURCES.txt (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/dependency_links.txt (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/not-zip-safe (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/paster_plugins.txt (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/requires.txt (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/sqlobject.txt (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/Bugs_Everywhere_Web.egg-info/top_level.txt (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/README.txt (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/app.cfg (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/config.py.example (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/config/app.cfg (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/config/log.cfg (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/controllers.py (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/formatting.py (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/json.py (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/model.py (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/prest.py (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/release.py (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/static/css/style.css (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/templates/about.kid (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/templates/bugs.kid (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/templates/edit_bug.kid (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/templates/edit_comment.kid (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/templates/error.kid (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/templates/login.kid (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/templates/master.kid (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/templates/projects.kid (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/templates/welcome.kid (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/tests/test_controllers.py (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/beweb/tests/test_model.py (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/dev.cfg (100%) create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/arch.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/beuuid.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/bug.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/bugdir.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/bzr.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/cmdutil.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/comment.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/config.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/darcs.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/diff.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/editor.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/encoding.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/git.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/hg.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/mapfile.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/plugin.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/properties.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/rcs.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/settings_object.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/tree.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/utility.py rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/prod.cfg (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/sample-prod.cfg (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/server.log (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/setup-tables.py (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/setup.py (100%) rename {Bugs-Everywhere-Web => interfaces/web/Bugs-Everywhere-Web}/start-beweb.py (100%) mode change 100755 => 100644 create mode 100644 interfaces/xml/be-mbox-to-xml rename {xml => interfaces/xml}/be-xml-to-mbox (51%) mode change 100755 => 100644 rename {completion => misc/completion}/be.bash (100%) diff --git a/AUTHORS b/AUTHORS index 4910d6c..6b66315 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,7 @@ Bugs Everywhere was written by: Aaron Bentley Alexander Belchenko +Alex Miller Ben Finney Chris Ball Gianluca Montecchi diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/not-zip-safe b/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/not-zip-safe deleted file mode 100644 index e69de29..0000000 diff --git a/Bugs-Everywhere-Web/beweb/__init__.py b/Bugs-Everywhere-Web/beweb/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Bugs-Everywhere-Web/beweb/static/images/ds-b.png b/Bugs-Everywhere-Web/beweb/static/images/ds-b.png deleted file mode 100644 index 790e43807020354bb6166565032abf7965153a1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^j6f{R!3HD)xzijA5L~c#`DCC7XMsm# zF#`j)FbFd;%$g$s6l5>)^mS#w$|Ei;DtNS}?g~&yw!}4}#5q4VH#M(>!MP|ku_QG` zp**uBLm?z1Rl(iUH{gAWY93IbrKgKy2*-8bBaVCwjw~z(u5k$3z4^RbynUr}V!3^p zxrK55-P+KrQ)ec)G`y>uDY;^m!bxrh4W%E9uC^?4eFiIXfQB%5y85}Sb4q9e0J*3< A+W-In diff --git a/Bugs-Everywhere-Web/beweb/static/images/ds-bl.png b/Bugs-Everywhere-Web/beweb/static/images/ds-bl.png deleted file mode 100644 index 5b43259bcabbbb8a9cfbbc05c84a398442105390..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 327 zcmeAS@N?(olHy`uVBq!ia0vp^+(0bM!3HGVG{0W}Qfx`y?k)`fL2$v|<&%LToCO|{ z#Xyz9Ak4@xYmNj^kiEpy*OmP$kGQa?Rpx>ADL^6F64!_l=ltB<)VvY~=c3falGGH1 z^30M9g^-L?1$R&1fcGh?c|e7$JzX3_G|o?*6v@Y|$m1N(@qPb$g#&*xoQ;#C1x`67!p9Cb+!YtihuGTt-=-Y1Z5%-oP@pZ|9B+D}CS9Z ziTy0QukYv*{8|3{rseLquj-#-RUTNs+bw>xw%Pjm>R_$<1l~^e3YR*@uND&m3i))) QfzD>|boFyt=akR{02P3F*#H0l diff --git a/Bugs-Everywhere-Web/beweb/static/images/ds-br.png b/Bugs-Everywhere-Web/beweb/static/images/ds-br.png deleted file mode 100644 index 6cfd62c085cb84b334bf8cf5dc786cf84699af68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 365 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S1|*9D%+3HQwj^(N7l!{JxM1({$v_d#0*}aI z1_o}RQf^^JhFNnYfP(BLp1!W^S9!#RMYM8rmTd$I$(FcAlsM<-=BDPAFgO>bCYGe8 zD3oWGWGIAWq$;?3`UbpDQOyG?yy)rT7-DgH>7+ou!v-AAzRY~z_q+PB4^e4n@DJS$~;)(Ud%C5Xw{_T{D%7;mVmq2D6H`%5pH6W#*z0K+iFFy85}Sb4q9e E04m{rga7~l diff --git a/Bugs-Everywhere-Web/beweb/static/images/ds-l.png b/Bugs-Everywhere-Web/beweb/static/images/ds-l.png deleted file mode 100644 index a6ce3ce5a020d6b7f6dbfa9c83a8edd14ce0f1c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^+(698!3HEb%tIW26kC$Fy9>jA5L~c#`DCC7XMsm# zF;Jy22s1Lwnj--eWH0gbb!ETGBQ7k?%`Ki5R#Fq;O^-g@IFN~52#Sn)5S4_<2viXM&1JsJWLy3|NFhjg=OEj=})#U)%DzA jos&HKZfe?u7L_`Y`-%ZC&VITF)WqQF>gTe~DWM4f7hO8m diff --git a/Bugs-Everywhere-Web/beweb/static/images/ds-r.png b/Bugs-Everywhere-Web/beweb/static/images/ds-r.png deleted file mode 100644 index 1ffd6f8397483e3abf2e908bb486f94457b582eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^!a&T(!3HF4{HDeLDYhhUcNd2LAh=-f^2tCE&H|6f zVg?3opi*vOMuu5)B!GhKC7!;n>{ofjg@u))KXzOK3dxqZMwB?`=jNv7l`uFLr6!i7 zrYMwWmSiY|WTYy%d-?{vPf^VSDzx%+aSY+OZhP2~i@}hCFK%LhhAHRj)12OS)NrZ9NA`njxgN@xNA D=C?pw diff --git a/Bugs-Everywhere-Web/beweb/static/images/ds-t.png b/Bugs-Everywhere-Web/beweb/static/images/ds-t.png deleted file mode 100644 index 0129b0ca09c6bca202a91c12d7d048e72ab52320..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^j6lrI!3HGv^*w(Nq}Y|gW!U_%O?XxI14-? zi-D?yL70(Y)*K0-AbW|YuPggi9&uqo{e;40)j%QH64!_l=ltB<)VvY~=c3falGGH1 z^30M9g^-L?1$R&1fcGh?c|e6ao-U3d9M^S^Y~*Dy;9%aM@&A6e!-NZsRR$`$xsNBY moa4E1cmFCTW8t2T*Nlg26_XRpW(olHF?hQAxvXlpN(Je);xVxfN<<_78OPY726N;5#=SOp8|zc8ddz#*Sbwo a=#(z(>n&8^^gjSJpTX1B&t;ucLK6VyLQCQR diff --git a/Bugs-Everywhere-Web/beweb/static/images/ds-tr.png b/Bugs-Everywhere-Web/beweb/static/images/ds-tr.png deleted file mode 100644 index 18e542e4dc5e74f6f714a1559a4d20b15296c850..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 311 zcmeAS@N?(olHy`uVBq!ia0vp^!a&T;!3HGfUfpH|q}Y|gW!U_%O?XxI14-? zi-D?uO1Xs@8D`Cq01C2~c>21sU*!=O7BY`8$bAPCk}YwKC~?lu%}vcKVQ?-=O)N=G zQ7F$W$xsN%NL6t6^bL5QqM8R(INQ_3F+}71)Jf5NM-@0++9&>fA3odYJ9l&Mc7CCz z1dE0YLB*USXFi0+ToRk|Q9riWIaEvg1@?O!~eA;@Sv`|*>V$5i%8-97&~ zW|HT*C#xh^f6(P&@X%A3bbIL?-6Q>vXPgY(+_2jA5L~c#`DCC7XMsm# zF#`j)FbFd;%$g$s6l5>)^mS#w$|EkU#JxgNeHKtiw!}4}#5q4VH#M(>!MP|ku_QG` zp**uBLm?z1Rl(iUH{gAWY93Ibk*AAe2*>r(V><;I6nI!Rey#lT<$mkx76v9Eq23uY tm%e-EAG+|&9v7CP@7+tpjhzqNWD6H#QQVjD$q}fV!PC{xWt~$(6962aJa+&9 diff --git a/Bugs-Everywhere-Web/beweb/static/images/ds2-r.png b/Bugs-Everywhere-Web/beweb/static/images/ds2-r.png deleted file mode 100644 index 0c3ea4c5586309e85dee1dcb48b4749edb87b825..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^!a&T(!3HF4{HDeLDYhhUcNd2LAh=-f^2tCE&H|6f zVg?3opi*vOMuu5)B!GhKC7!;n>{ofjg%$Nq7%Y$j3dxqZMwB?`=jNv7l`uFLr6!i7 zrYMwWmSiY|WTYy%d-?{vPf^VSDm3tPaSY+Oo_qKp?*Rr5=8fDgjeoZ%oceY>TsN}( s`y0Lb^fnchgJ#=a=B;|gt-$HIhd-S8AEUnO9iUzYPgg&ebxsLQ04OOu2><{9 diff --git a/Bugs-Everywhere-Web/beweb/static/images/favicon.ico b/Bugs-Everywhere-Web/beweb/static/images/favicon.ico deleted file mode 100644 index 339d09cec049c0787b05a59b803d5e65433d5989..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 318 zcmZQzU<5(|0RbS%!l1#(z#zuJz@P!d0zj+)#2|4nV1iH}5+nx(E-nlVKfW_C{{PRw z@Z}=|!=i-@j3R;z4E+-s7#=(VX=Y&f^P7Re#DsyFot1$hB$$C=&MXE7eqjcN>kQc}|1G;=qYZc^R{rgu*32h$xLj6ijpw40_t)lG(|+c~8R SEWdjz*c=?zC_#NeHU$8FYB?+b diff --git a/Bugs-Everywhere-Web/beweb/static/images/favicon.png b/Bugs-Everywhere-Web/beweb/static/images/favicon.png deleted file mode 100644 index 6dc53eea7519d6991b42d58a08a2bb42730ea772..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFP2=EDUWng4rWoH)VXAl%&G%;at zabXA!VaUj4nAp!SYYxN0MGTJ~Fns*N@cjqF?>`Lx|1-Y$HPHg7m$SelvY3H^TNs2H zfl4KSg6t)pzOL+7c~}_51@1a8UJn$KEpd$~an8@pP0cG|a4t$sEJ;mKD9+q>gnPb!f`$M2R|#%jGq3cLpSAFSy?6RPVG(lslhKav0j-Y@xy+h zRs#u%q{gf#jahLUoR|LNa1`C&toe^K_zaJs!1kpK{THKoSb6^+0vgER>FVdQ&MBb@ E0FHoBA^-pY diff --git a/Bugs-Everywhere-Web/beweb/static/images/half-spiral.png b/Bugs-Everywhere-Web/beweb/static/images/half-spiral.png deleted file mode 100644 index cb4b56c3a63926275bc2e9b6b15213e55bfed246..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1112 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 diff --git a/Bugs-Everywhere-Web/beweb/static/images/is-b.png b/Bugs-Everywhere-Web/beweb/static/images/is-b.png deleted file mode 100644 index 25d3cfa233357b989ab27296433f5fb1b4cf417f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^j6lrI!3HGv^*w(Nq}Y+)$J50zMB{w!q>Ftm3OugxMN<2V1acPK>t)YxgQWugTWE#XY0r&y?P-f6cMiudZ$v zTzrX7`gX^WifKW+{w~z$@zC6xXgGWMq|Y|_-!&Wp6KuGHy?AzRJytpI@AJYN@BFfR y&p*HH=$|TE78{Aj3o{&%U7eGympo&YrPiGXjC(z~JfX=d#Wzp$Py{ji7A+ diff --git a/Bugs-Everywhere-Web/beweb/static/images/is-br.png b/Bugs-Everywhere-Web/beweb/static/images/is-br.png deleted file mode 100644 index 74cbd919381c619cb8f6d06d5ace04bcf66ae080..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhY)RhkE5(ej@)Wnk16ovB4 zk_?5Aj8p}8Pv3y|DXMuug;P9T977~7=T7wQV=?4#w$*)CAERYB|H~J_T-sQX6$FHPt*}^V`TxlkHOQ`&t;ucLK6U@SZeG5 diff --git a/Bugs-Everywhere-Web/beweb/static/images/is-l.png b/Bugs-Everywhere-Web/beweb/static/images/is-l.png deleted file mode 100644 index dd567fa48348c9af158d8a5d5f67867835755c08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^!a&T(!3HF4{HDeLDYhhUcNZWH1V5d3*8?fe0*}aI z1_o}RQf^^JhFNnYfP(BLp1!W^S9zF3xC{diJX{47k}YwKC~?lu%}vcKVQ?-=O)N=G zQ7F$W$xsN%NL6t6^bL5QqM8R(XyxhR7{YPg_OK%tgCPgY!7$~CiJuZ*Rf z`K;yheCgXBPC~|q@6CRwy$!fk$L9 zP^B;kGcwGYBLNg-FY)wsWxvY9B*LzgaN(XMP)N4KHKN2hKQ}iuuY|$5C^fMpHASI3 zvm`?yBqLS9-P1SVeTr%xP@$%$i(?4Kb=HH8yayb3m^Qxt_j{2G%f4^ZpKM*K>$$@^ iCwcbW)U*jLDs>|F6$4(J{d5ngiNVv=&t;ucLK6UtU^sXH diff --git a/Bugs-Everywhere-Web/beweb/static/images/is-t.png b/Bugs-Everywhere-Web/beweb/static/images/is-t.png deleted file mode 100644 index fbb06c877a139ac8675b451ac4874be66ae72432..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^j6f{R!3HD)xziwy$!fk$L9 z0|U1(2s1Lwnj--eWH0gbb!ETG!z9A0vo&tw9-xqHiEBiObAE1aYF-J0b5UwyNotBh zd1gt5LP$ocg1e`0!21-{JfK2LPZ!4!j_bZh9Qha=Sy&ES;}Eoa^Le*;`%34;a{DrK z3*-E|wV_w1&P;G=cvm-5a>XizliUm%NbP0l+XkKjt@D3 diff --git a/Bugs-Everywhere-Web/beweb/static/images/is-tl.png b/Bugs-Everywhere-Web/beweb/static/images/is-tl.png deleted file mode 100644 index 9336290f63f4e3c36541de73c9a5f6ccad654633..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 413 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S1|*9D%+3HQwj^(N7a$D;Kb?2i11Zh|kH}&M z25z8IZed1-S#u)zyCdJDJXdJht07QyLTt|Fd8?{Jb3ruX`mYy9*E*DK6gY!^@Q7(Ll%|q zYwvPqAL%kQVpUcudUVfP`uMe|&Z{pvHn3h+EIPL{z<2ku)d5LSo5gP|y`97*7#tw( z7B1$nXy>dy$`|#Vj_wK(eq0yiv9b8vRIP)%o*%lqzAEEZ%gyVpFJ6h&a4){hyISM6 ze@}DNjqoLJvS)Fzv2&E(czs4|n(1nV-%&ZW7lki3a?QTERaE3@e)!oh3+I`xWq0h$ zW~;oQ7%o23Z^irtyJp4N7nmLj-+%TAYpMRq$D;q(Z?p0(T&tAv78q&_p00i_>zopr E0Q9z_vH$=8 diff --git a/Bugs-Everywhere-Web/beweb/static/images/is-tr.png b/Bugs-Everywhere-Web/beweb/static/images/is-tr.png deleted file mode 100644 index de74808a22c4f7d00ea674bbe8102889876662ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 414 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 diff --git a/Bugs-Everywhere-Web/beweb/static/images/shadows.png b/Bugs-Everywhere-Web/beweb/static/images/shadows.png deleted file mode 100644 index 9ddc67667c2f30b2914a66594cae911e95b52703..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3960 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 JRi_B){{`|Z?Vtbv diff --git a/Bugs-Everywhere-Web/beweb/static/images/spiral.png b/Bugs-Everywhere-Web/beweb/static/images/spiral.png deleted file mode 100644 index b4bcb1e6780c3ea11f622347b7146f2bd168b78d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2120 zcmV-O2)Fl%P)q7ZWZvOqLYltjXN;euS@p6?%rD@c5v&%Jgr(c zUf8n-N=w1vfcSWrI8oW2-seL~N{|g3khyaaKyJAO*}4^JX$j4i00#}Cp#jOu zL&Cz4*qJ(a7tsBYyu408vB^M zlWS{{yYEKA!*#Fp3q)HRl9`DN8G?NH;eQLD=OnILrIWaATR%WN^_0%SmtX#W0QBT1 zpG2HaqB5&i!SdyL=-E+ov)a_y2<7Dv7iV$U`Te??5)+ZeM*A6RZbn{uN!>0n zQA2M{jlG{26(Kn}8cGuqkRwNeTamM8HEKQnxWSzc9a1+cDzaBtTZ`O!D-scbr$n{C@vzPDRl1Cfz|+!hUUY77KGPd;!!f#l}eryTqDtE~qP^aAnl z!$?w+&WuUHvSmnA)ZhXL@Qxk2PhNS&UN${l_wO|j9UaKXk;tpBnm_#PGo69L!odd+ z;DUm_eZDt;@PPt8d$tQiMTHtyTx|Yu-n{G42g9A6p~?GvNNOrFd$w7wu~FUc%{RM1 zy!DnoFy-=Yw}#SS4#27^b-O83wBCq~Mee^p_!a!>E1lFmd(3k8-KWt_1YyY%WX25h z2cLYRimR&3a$B|_!-i@6f8q&b^JZlAYGl?d{eHoM;8N^4b2N7|$-Mu*y55Bg2zT6} z1DZZqwhX!LHnYspqiX1i6^O_4@Ao@*BBMqj`SaE?_4PX5#~&k1 zo{T*Iyjf{X46Gi_Mk>=dx z$`y!-v3~#Sukictuzo!_oz`;6$?)21P*kMd5i5$0h8Z*9#~;mhxm7tEftWHye@jdC zR|K|i*X*-5IUI2By}EKH3I;@SvaZCCKXN+NGVn>}>se z!2-z1>05UEcwMc!I(3P@sJ&fR&D2zDZiW#fz&UZEwjWKjWC{HFCmcR(EfW`~1yX4# zR8+v{pIf&ntE$wvv16_EwYFNXQh+=j4NA_`R5*7I>g%nAb8=wnRQt^c+H0LY-7HgF zta7HR&h6G&Hj&${LCHCPK7@tAzJ1oh4hQ7t!}s6A*I%0z+tH;NPA@r9Q@c9~H;Owt)D@xI zJxsiAox0rLf7^f%(F?|e1nqwwI&@}o*2_mp`+1Yw7 ztX^#&9z)@yN42CKHcWe3!G71+h+MoF0tgY^?3*@eG?+9AdFLJTA!Nt3wHlg1Yx1(P zflr7nhyWiyjx1iRr*3Sl9yZ601$UF$)TF7+J@=^V#KvkDFVw4ED-QWT3MwmM%NE$V zQyT~o5n9wwovLYZWTalMT)3ch!>Los9UXd6^5~=R$Rk?y4HotL-y+)E_2%c>Z=s|_ yt3$6>i<#C|t;*YDvbIYV`E zcS*)_N8;Vt-{0T4i*l)rclP%7^78WR?CkR9;P$YDnnM-rm~X_Hf?6gCgI!D3*xIvn zQ{CO&=H}+Sp@`Jf)V8Uj(#^}_=IZF^=*z;rmu*C&dQ{-x;HiUM_4)eK$-?jO@apR7 z4`B0d{{=xteoB8;=Pe*n1FP(i(`00 zCd0L>(Y~|l?(u6oA$UtP`27CBuA8AxB)zPixrJ$+S~$(6kGzasu$G0tl6vUv@2XQK zfQ>0ljtdM-Vqm#KFMX+T796(dp~%*3;2vD+#Qelyox?%EiL-_V~t;T(y5=&(hTV{{OkB zo72X-v!k2Uw4&C#tI^Wbv{xzOdCvVzK(S3?e5yiz@JVdkvR$b{r=6)&)weN*q>+N;^X!7@z~DD_xk&9Kqikt z9OvrnwqG-EM=`>±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 # Chris Ball # W. Trevor King -# This is free software; you may copy, modify and/or distribute this work -# under the terms of the GNU General Public License, version 2 or later. -# No warranty expressed or implied. See the file COPYING for details. - -# Makefile for Bugs Everywhere project +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. SHELL = /bin/bash PATH = /usr/bin:/bin @@ -29,6 +38,7 @@ MODULES += ${DOC_DIR} RM = rm +#PREFIX = /usr/local PREFIX = ${HOME} INSTALL_OPTIONS = "--prefix=${PREFIX}" @@ -47,12 +57,14 @@ build: libbe/_version.py .PHONY: install install: doc build python setup.py install ${INSTALL_OPTIONS} - cp -v xml/* ${PREFIX}/bin + cp -v interfaces/xml/* ${PREFIX}/bin + cp -v interfaces/email/catmutt ${PREFIX}/bin .PHONY: clean clean: $(RM) -rf ${GENERATED_FILES} +.PHONY: libbe/_version.py libbe/_version.py: bzr version-info --format python > $@ diff --git a/README b/README index 9562a75..b43c15c 100644 --- a/README +++ b/README @@ -1,12 +1,14 @@ Bugs Everywhere =============== -This is Bugs Everywhere, a bugtracker built on distributed revision control. -It works with Bazaar and Arch at the moment, but is easily extensible. It -can also function with no RCS at all. +This is Bugs Everywhere, a bugtracker built on distributed revision +control. It works with Arch, Bazaar, Darcs, Git, and Mercurial at the +moment, but is easily extensible. It can also function with no RCS at +all. The idea is to package the bug information with the source code, so that bugs can be marked 'fixed' in the branches that fix them. So, instead of -numbers, bugs have ids. +numbers, bugs have globally unique ids. + Getting started =============== @@ -15,9 +17,11 @@ set the bug root to your project root, so that Bugs Everywhere works in any part of your project tree. $ be init $PROJECT_ROOT -To create bugs, use "be new $DESCRIPTION". To comment on bugs, you can can use -"be comment $BUG_ID". To close a bug, use "be close $BUG_ID". For more -commands, see "be help". You can also look at the usage in test_usage.sh. +To create bugs, use "be new $DESCRIPTION". To comment on bugs, you +can can use "be comment $BUG_ID". To close a bug, use "be close +$BUG_ID" or "be status $BUG_ID fixed". For more commands, see "be +help". You can also look at the usage examples in test_usage.sh. + Using BeWeb, the web UI ======================= diff --git a/README.dev b/README.dev index 644d965..ddc3a88 100644 --- a/README.dev +++ b/README.dev @@ -31,6 +31,7 @@ consistent interface Again, you can just browse around in becommands to get a feel for things. + Testing ------- @@ -67,3 +68,12 @@ later. In recognition of this, cmdutil provides the default_complete function which ensures that if '--complete' is any one of the arguments, options, or option-arguments, GetCompletions will be raised with and empty list. + +Profiling +========= + +Find out which 20 calls take the most cumulative time (time of +execution + childrens' times). + + $ python -m cProfile -o profile be [command] [args] + $ python -c "import pstats; p=pstats.Stats('profile'); p.sort_stats('cumulative').print_stats(20)" diff --git a/be b/be index 2023daa..36deaba 100755 --- a/be +++ b/be @@ -3,54 +3,81 @@ # Chris Ball # Oleg Romanyshyn # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 - +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import os import sys + from libbe import cmdutil, _version -__doc__ == cmdutil.help() +__doc__ = cmdutil.help() + +usage = "be [options] [command] [command_options ...] [command_args ...]" -if len(sys.argv) == 1 or sys.argv[1] in ('--help', '-h'): - print cmdutil.help() -elif sys.argv[1] == '--complete': - for command, module in cmdutil.iter_commands(): - print command - print '\n'.join(["--help","--complete","--options","--version"]) -elif sys.argv[1] == '--version': +parser = cmdutil.CmdOptionParser(usage) +parser.command = "be" +parser.add_option("--version", action="store_true", dest="version", + help="Print version string and exit.") +parser.add_option("-d", "--dir", dest="dir", metavar="DIR", + help="Run this command from DIR instead of the current directory.") + +try: + options,args = parser.parse_args() + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + if option == "dir": + if len(args) == 0: + args = ["."] + paths = cmdutil.complete_path(args[0]) + raise cmdutil.GetCompletions(paths) +except cmdutil.GetHelp: + print cmdutil.help(parser=parser) + sys.exit(0) +except cmdutil.GetCompletions, e: + print '\n'.join(e.completions) + sys.exit(0) + +if options.version == True: print _version.version_info["revision_id"] -else: - try: - try: - sys.exit(cmdutil.execute(sys.argv[1], sys.argv[2:])) - except KeyError, e: - raise cmdutil.UserError("Unknown command \"%s\"" % e.args[0]) - except cmdutil.GetHelp: - print cmdutil.help(sys.argv[1]) - sys.exit(0) - except cmdutil.GetCompletions, e: - print '\n'.join(e.completions) - sys.exit(0) - except cmdutil.UsageError, e: - print "Invalid usage:", e - print "\nArgs:", sys.argv[1:] - print cmdutil.help(sys.argv[1]) - sys.exit(1) - except cmdutil.UserError, e: - print "ERROR:" - print e - sys.exit(1) + sys.exit(0) +if options.dir != None: + os.chdir(options.dir) + +try: + if len(args) == 0: + raise cmdutil.UsageError, "must supply a command" + sys.exit(cmdutil.execute(args[0], args[1:])) +except cmdutil.GetHelp: + print cmdutil.help(sys.argv[1]) + sys.exit(0) +except cmdutil.GetCompletions, e: + print '\n'.join(e.completions) + sys.exit(0) +except cmdutil.UnknownCommand, e: + print e + sys.exit(1) +except cmdutil.UsageError, e: + print "Invalid usage:", e + if len(args) == 0: + print cmdutil.help(parser=parser) + else: + print "\nArgs:", args + print cmdutil.help(sys.argv[1]) + sys.exit(1) +except cmdutil.UserError, e: + print "ERROR:" + print e + sys.exit(1) diff --git a/becommands/assign.py b/becommands/assign.py index 985cfdd..536bca6 100644 --- a/becommands/assign.py +++ b/becommands/assign.py @@ -2,23 +2,22 @@ # Marien Zwart # Thomas Gerigk # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Assign an individual or group to fix a bug""" -from libbe import cmdutil, bugdir, settings_object +from libbe import cmdutil, bugdir __desc__ = __doc__ def execute(args, test=False): @@ -26,7 +25,7 @@ def execute(args, test=False): >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> bd.bug_from_shortname("a").assigned is settings_object.EMPTY + >>> bd.bug_from_shortname("a").assigned is None True >>> execute(["a"], test=True) @@ -41,7 +40,7 @@ def execute(args, test=False): >>> execute(["a","none"], test=True) >>> bd._clear_bugs() - >>> bd.bug_from_shortname("a").assigned is settings_object.EMPTY + >>> bd.bug_from_shortname("a").assigned is None True """ parser = get_parser() diff --git a/becommands/close.py b/becommands/close.py index deaccce..0ba8f50 100644 --- a/becommands/close.py +++ b/becommands/close.py @@ -2,21 +2,20 @@ # Marien Zwart # Thomas Gerigk # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Close a bug""" from libbe import cmdutil, bugdir __desc__ = __doc__ diff --git a/becommands/comment.py b/becommands/comment.py index 0b3a576..55b5913 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -1,25 +1,28 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. # Chris Ball # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Add a comment to a bug""" -from libbe import cmdutil, bugdir, settings_object, editor +from libbe import cmdutil, bugdir, comment, editor import os import sys +try: # import core module, Python >= 2.5 + from xml.etree import ElementTree +except ImportError: # look for non-core module + from elementtree import ElementTree __desc__ = __doc__ def execute(args, test=False): @@ -39,7 +42,7 @@ def execute(args, test=False): True >>> comment.time <= int(time.time()) True - >>> comment.in_reply_to is settings_object.EMPTY + >>> comment.in_reply_to is None True >>> if 'EDITOR' in os.environ: @@ -77,7 +80,8 @@ def execute(args, test=False): bugname = shortname is_reply = False - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=not test) bug = bd.bug_from_shortname(bugname) bug.load_comments(load_full=False) if is_reply: @@ -88,7 +92,13 @@ def execute(args, test=False): if len(args) == 1: # try to launch an editor for comment-body entry try: - body = editor.editor_string("Please enter your comment above") + if parent == bug.comment_root: + parent_body = bug.summary+"\n" + else: + parent_body = parent.body + estr = "Please enter your comment above\n\n> %s\n" \ + % ("\n> ".join(parent_body.splitlines())) + body = editor.editor_string(estr) except editor.CantFindEditor, e: raise cmdutil.UserError, "No comment supplied, and EDITOR not specified." if body is None: @@ -108,15 +118,67 @@ def execute(args, test=False): if not body.endswith('\n'): body+='\n' - comment = parent.new_reply(body=body) - if options.content_type != None: - comment.content_type = options.content_type - bd.save() + if options.XML == False: + new = parent.new_reply(body=body) + if options.author != None: + new.From = options.author + if options.alt_id != None: + new.alt_id = options.alt_id + if options.content_type != None: + new.content_type = options.content_type + else: # import XML comment [list] + # read in the comments + str_body = body.encode("unicode_escape").replace(r'\n', '\n') + comment_list = ElementTree.XML(str_body) + if comment_list.tag not in ["bug", "comment-list"]: + raise comment.InvalidXML( + comment_list, "root element must be or ") + new_comments = [] + ids = [] + for c in bug.comment_root.traverse(): + ids.append(c.uuid) + if c.alt_id != None: + ids.append(c.alt_id) + for child in comment_list.getchildren(): + if child.tag == "comment": + new = comment.Comment(bug) + new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape")) + if new.alt_id in ids: + raise cmdutil.UserError( + "Clashing comment alt_id: %s" % new.alt_id) + ids.append(new.uuid) + if new.alt_id != None: + ids.append(new.alt_id) + if new.in_reply_to == None: + new.in_reply_to = parent.uuid + new_comments.append(new) + else: + print >> sys.stderr, "Ignoring unknown tag %s in %s" \ + % (child.tag, comment_list.tag) + try: + comment.list_to_root(new_comments,bug,root=parent, # link new comments + ignore_missing_references=options.ignore_missing_references) + except comment.MissingReference, e: + raise cmdutil.UserError(e) + # Protect against programmer error causing data loss: + kids = [c.uuid for c in parent.traverse()] + for nc in new_comments: + assert nc.uuid in kids, "%s wasn't added to %s" % (nc.uuid, parent.uuid) + nc.save() def get_parser(): parser = cmdutil.CmdOptionParser("be comment ID [COMMENT]") + parser.add_option("-a", "--author", metavar="AUTHOR", dest="author", + help="Set the comment author", default=None) + parser.add_option("--alt-id", metavar="ID", dest="alt_id", + help="Set an alternate comment ID", default=None) parser.add_option("-c", "--content-type", metavar="MIME", dest="content_type", help="Set comment content-type (e.g. text/plain)", default=None) + parser.add_option("-x", "--xml", action="store_true", default=False, + dest='XML', help="Use COMMENT to specify an XML comment description rather than the comment body. The root XML element should be either or with one or more children. The syntax for the elements should match that generated by 'be show --xml COMMENT-ID'. Unrecognized tags are ignored. Missing tags are left at the default value. The comment UUIDs are always auto-generated, so if you set a field, but no field, your will be used as the comment's . An exception is raised if conflicts with an existing comment.") + parser.add_option("-i", "--ignore-missing-references", action="store_true", + dest="ignore_missing_references", + help="For XML import, if any comment's refers to a non-existent comment, ignore it (instead of raising an exception).") return parser longhelp=""" diff --git a/becommands/commit.py b/becommands/commit.py new file mode 100644 index 0000000..4f3bdbd --- /dev/null +++ b/becommands/commit.py @@ -0,0 +1,77 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Commit the currently pending changes to the repository""" +from libbe import cmdutil, bugdir, editor, rcs +import sys +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os, time + >>> from libbe import bug + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) + >>> full_path = "testfile" + >>> test_contents = "A test file" + >>> bd.rcs.set_file_contents(full_path, test_contents) + >>> execute(["Added %s." % (full_path)], manipulate_encodings=False) # doctest: +ELLIPSIS + Committed ... + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) + if len(args) != 1: + raise cmdutil.UsageError("Please supply a commit message") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if args[0] == '-': # read summary from stdin + assert options.body != "EDITOR", \ + "Cannot spawn and editor when the summary is using stdin." + summary = sys.stdin.readline() + else: + summary = args[0] + if options.body == None: + body = None + elif options.body == "EDITOR": + body = editor.editor_string("Please enter your commit message above") + else: + body = bd.rcs.get_file_contents(options.body, allow_no_rcs=True) + try: + revision = bd.rcs.commit(summary, body=body, + allow_empty=options.allow_empty) + except rcs.EmptyCommit, e: + print e + return 1 + else: + print "Committed %s" % revision + +def get_parser(): + parser = cmdutil.CmdOptionParser("be commit COMMENT") + parser.add_option("-b", "--body", metavar="FILE", dest="body", + help='Provide a detailed body for the commit message. In the special case that FILE == "EDITOR", spawn an editor to enter the body text (in which case you cannot use stdin for the summary)', default=None) + parser.add_option("-a", "--allow-empty", dest="allow_empty", + help="Allow empty commits", + default=False, action="store_true") + return parser + +longhelp=""" +Commit the current repository status. The summary specified on the +commandline is a string (only one line) that describes the commit +briefly or "-", in which case the string will be read from stdin. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/becommands/depend.py b/becommands/depend.py index 58e4388..4a23b0f 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -1,18 +1,18 @@ # Copyright (C) 2009 W. Trevor King # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Add/remove bug dependencies""" from libbe import cmdutil, bugdir import os, copy @@ -58,7 +58,6 @@ def execute(args, test=False): else: # add the dependency estrs.append(depend_string) bugA.extra_strings = estrs # reassign to notice change - bugA.save() depends = [] for estr in bugA.extra_strings: diff --git a/becommands/diff.py b/becommands/diff.py index 2bdea93..13402c0 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -1,20 +1,19 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Compare bug reports with older tree""" from libbe import cmdutil, bugdir, diff @@ -25,10 +24,10 @@ def execute(args, test=False): """ >>> import os >>> bd = bugdir.simple_bug_dir() + >>> bd.set_sync_with_disk(True) >>> original = bd.rcs.commit("Original status") >>> bug = bd.bug_from_uuid("a") >>> bug.status = "closed" - >>> bd.save() >>> changed = bd.rcs.commit("Closed bug a") >>> os.chdir(bd.root) >>> if bd.rcs.versioned == True: @@ -38,7 +37,6 @@ def execute(args, test=False): Modified bug reports: a:cm: Bug A status: open -> closed - """ parser = get_parser() options, args = parser.parse_args(args) @@ -54,7 +52,7 @@ def execute(args, test=False): print "This directory is not revision-controlled." else: old_bd = bd.duplicate_bugdir(revision) - r,m,a = diff.diff(old_bd, bd) + r,m,a = diff.bug_diffs(old_bd, bd) optbugs = [] if options.all == True: @@ -69,7 +67,9 @@ def execute(args, test=False): for bug in optbugs: print bug.uuid else : - print diff.diff_report((r,m,a), bd).encode(bd.encoding) + rep = diff.diff_report((r,m,a), old_bd, bd).encode(bd.encoding) + if len(rep) > 0: + print rep bd.remove_duplicate_bugdir() def get_parser(): @@ -89,9 +89,10 @@ def get_parser(): return parser longhelp=""" -Uses the RCS to compare the current tree with a previous tree, and prints -a pretty report. If specifier is given, it is a specifier for the particular -previous tree to use. Specifiers are specific to their RCS. +Uses the RCS to compare the current tree with a previous tree, and +prints a pretty report. If REVISION is given, it is a specifier for +the particular previous tree to use. Specifiers are specific to their +RCS. For Arch your specifier must be a fully-qualified revision name. diff --git a/becommands/help.py b/becommands/help.py index b0b182d..a8ae338 100644 --- a/becommands/help.py +++ b/becommands/help.py @@ -1,21 +1,20 @@ # Copyright (C) 2006-2009 Aaron Bentley and Panometrics, Inc. # Thomas Gerigk # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Print help for given subcommand""" from libbe import cmdutil, utility __desc__ = __doc__ diff --git a/becommands/init.py b/becommands/init.py index 390dd15..5b2a416 100644 --- a/becommands/init.py +++ b/becommands/init.py @@ -1,20 +1,19 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Assign the root directory for bug tracking""" import os.path from libbe import cmdutil, bugdir diff --git a/becommands/list.py b/becommands/list.py index 443704b..5ba1821 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -2,21 +2,20 @@ # Chris Ball # Oleg Romanyshyn # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """List bugs""" from libbe import cmdutil, bugdir, bug import os @@ -135,11 +134,12 @@ def execute(args, test=False): return True bugs = [b for b in bd if filter(b) ] - if len(bugs) == 0: + if len(bugs) == 0 and options.xml == False: print "No matching bugs found" def list_bugs(cur_bugs, title=None, just_uuids=False, xml=False): if xml == True: + print '' % bd.encoding print "" if len(cur_bugs) > 0: if title != None and xml == False: diff --git a/becommands/merge.py b/becommands/merge.py index 4bec6bf..4aaefa8 100644 --- a/becommands/merge.py +++ b/becommands/merge.py @@ -1,19 +1,18 @@ # Copyright (C) 2008-2009 W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Merge duplicate bugs""" from libbe import cmdutil, bugdir import os, copy @@ -23,6 +22,7 @@ def execute(args, test=False): """ >>> from libbe import utility >>> bd = bugdir.simple_bug_dir() + >>> bd.set_sync_with_disk(True) >>> a = bd.bug_from_shortname("a") >>> a.comment_root.time = 0 >>> dummy = a.new_comment("Testing") @@ -36,7 +36,6 @@ def execute(args, test=False): >>> dummy.time = 1 >>> dummy = dummy.new_reply("1 2 3 4") >>> dummy.time = 2 - >>> bd.save() >>> os.chdir(bd.root) >>> execute(["a", "b"], test=True) Merging bugs a and b @@ -141,13 +140,13 @@ def execute(args, test=False): bugB.load_comments() mergeA = bugA.new_comment("Merged from bug %s" % bugB.uuid) newCommTree = copy.deepcopy(bugB.comment_root) - for comment in newCommTree.traverse(): + for comment in newCommTree.traverse(): # all descendant comments comment.bug = bugA - for comment in newCommTree: + comment.save() # force onto disk under bugA + for comment in newCommTree: # just the child comments mergeA.add_reply(comment, allow_time_inversion=True) bugB.new_comment("Merged into bug %s" % bugA.uuid) bugB.status = "closed" - bd.save() print "Merging bugs %s and %s" % (bugA.uuid, bugB.uuid) def get_parser(): diff --git a/becommands/new.py b/becommands/new.py index 32e070a..af599d7 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -1,22 +1,22 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Create a new bug""" -from libbe import cmdutil, bugdir, settings_object +from libbe import cmdutil, bugdir +import sys __desc__ = __doc__ def execute(args, test=False): @@ -36,7 +36,7 @@ def execute(args, test=False): True >>> print bug.severity minor - >>> bug.target == settings_object.EMPTY + >>> bug.target == None True """ parser = get_parser() @@ -45,16 +45,19 @@ def execute(args, test=False): if len(args) != 1: raise cmdutil.UsageError("Please supply a summary message") bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) - bug = bd.new_bug(summary=args[0]) + if args[0] == '-': # read summary from stdin + summary = sys.stdin.readline() + else: + summary = args[0] + bug = bd.new_bug(summary=summary.strip()) if options.reporter != None: bug.reporter = options.reporter else: bug.reporter = bug.creator if options.assigned != None: bug.assigned = options.assigned - elif bd.default_assignee != settings_object.EMPTY: + elif bd.default_assignee != None: bug.assigned = bd.default_assignee - bd.save() print "Created bug with ID %s" % bd.bug_shortname(bug) def get_parser(): @@ -66,8 +69,9 @@ def get_parser(): return parser longhelp=""" -Create a new bug, with a new ID. The summary specified on the commandline -is a string that describes the bug briefly. +Create a new bug, with a new ID. The summary specified on the +commandline is a string (only one line) that describes the bug briefly +or "-", in which case the string will be read from stdin. """ def help(): diff --git a/becommands/open.py b/becommands/open.py index f9abcbb..2ef5f43 100644 --- a/becommands/open.py +++ b/becommands/open.py @@ -2,21 +2,20 @@ # Marien Zwart # Thomas Gerigk # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Re-open a bug""" from libbe import cmdutil, bugdir __desc__ = __doc__ @@ -44,7 +43,6 @@ def execute(args, test=False): bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bug = bd.bug_from_shortname(args[0]) bug.status = "open" - bd.save() def get_parser(): parser = cmdutil.CmdOptionParser("be open BUG-ID") diff --git a/becommands/remove.py b/becommands/remove.py index 213a8d9..d79a7be 100644 --- a/becommands/remove.py +++ b/becommands/remove.py @@ -1,19 +1,18 @@ # Copyright (C) 2008-2009 W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Remove (delete) a bug and its comments""" from libbe import cmdutil, bugdir __desc__ = __doc__ @@ -44,7 +43,6 @@ def execute(args, test=False): bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bug = bd.bug_from_shortname(args[0]) bd.remove_bug(bug) - bd.save() print "Removed bug %s" % bug.uuid def get_parser(): diff --git a/becommands/set.py b/becommands/set.py index e771018..0c0862f 100644 --- a/becommands/set.py +++ b/becommands/set.py @@ -3,30 +3,30 @@ # Marien Zwart # Thomas Gerigk # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Change tree settings""" -from libbe import cmdutil, bugdir, settings_object +import textwrap +from libbe import cmdutil, bugdir, rcs, settings_object __desc__ = __doc__ def _value_string(bd, setting): val = bd.settings.get(setting, settings_object.EMPTY) if val == settings_object.EMPTY: default = getattr(bd, bd._setting_name_to_attr_name(setting)) - if default != settings_object.EMPTY: + if default not in [None, settings_object.EMPTY]: val = "None (%s)" % default else: val = None @@ -60,7 +60,9 @@ def execute(args, test=False): elif len(args) == 1: print _value_string(bd, args[0]) else: - if args[1] != "none": + if args[1] == "none": + setattr(bd, args[0], settings_object.EMPTY) + else: if args[0] not in bd.settings_properties: msg = "Invalid setting %s\n" % args[0] msg += 'Allowed settings:\n ' @@ -68,14 +70,35 @@ def execute(args, test=False): raise cmdutil.UserError(msg) old_setting = bd.settings.get(args[0]) setattr(bd, args[0], args[1]) - else: - del bd.settings[args[0]] - bd.save() def get_parser(): parser = cmdutil.CmdOptionParser("be set [NAME] [VALUE]") return parser +def get_bugdir_settings(): + settings = [] + for s in bugdir.BugDir.settings_properties: + settings.append(s) + settings.sort() + documented_settings = [] + for s in settings: + set = getattr(bugdir.BugDir, s) + dstr = set.__doc__.strip() + # per-setting comment adjustments + if s == "rcs_name": + lines = dstr.split('\n') + while lines[0].startswith("This property defaults to") == False: + lines.pop(0) + assert len(lines) != None, \ + "Unexpected rcs_name docstring:\n '%s'" % dstr + lines.insert( + 0, "The name of the revision control system to use.\n") + dstr = '\n'.join(lines) + doc = textwrap.wrap(dstr, width=70, initial_indent=' ', + subsequent_indent=' ') + documented_settings.append("%s\n%s" % (s, '\n'.join(doc))) + return documented_settings + longhelp=""" Show or change per-tree settings. @@ -83,14 +106,11 @@ If name and value are supplied, the name is set to a new value. If no value is specified, the current value is printed. If no arguments are provided, all names and values are listed. -Interesting settings are: -rcs_name - The name of the revision control system. "Arch" and "None" are supported. -target - The current development goal - To unset a setting, set it to "none". -""" + +Allowed settings are: + +%s""" % ('\n'.join(get_bugdir_settings()),) def help(): return get_parser().help_str() + longhelp diff --git a/becommands/severity.py b/becommands/severity.py index f8a0c02..65467e3 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -2,21 +2,20 @@ # Marien Zwart # Thomas Gerigk # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Show or change a bug's severity level""" from libbe import cmdutil, bugdir, bug __desc__ = __doc__ @@ -51,7 +50,6 @@ def execute(args, test=False): if e.name != "severity": raise e raise cmdutil.UserError ("Invalid severity level: %s" % e.value) - bd.save() def get_parser(): parser = cmdutil.CmdOptionParser("be severity BUG-ID [SEVERITY]") diff --git a/becommands/show.py b/becommands/show.py index ff434ab..e43cfb9 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -3,22 +3,22 @@ # Thomas Gerigk # Thomas Habets # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Show a particular bug""" +import sys from libbe import cmdutil, bugdir __desc__ = __doc__ @@ -40,6 +40,7 @@ def execute(args, test=False): Bug A >>> execute (["--xml", "a"], test=True) # doctest: +ELLIPSIS + a a @@ -53,27 +54,60 @@ def execute(args, test=False): parser = get_parser() options, args = parser.parse_args(args) cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) + bugid_args={-1: lambda bug : bug.active==True}) if len(args) == 0: raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) - for bugid in args: - bug = bd.bug_from_shortname(bugid) - if options.dumpXML: - print bug.xml(show_comments=True) + if options.XML: + print '' % bd.encoding + for shortname in args: + if shortname.count(':') > 1: + raise cmdutil.UserError("Invalid id '%s'." % shortname) + elif shortname.count(':') == 1: + # Split shortname generated by Comment.comment_shortnames() + bugname = shortname.split(':')[0] + is_comment = True else: - print bug.string(show_comments=True) - if bugid != args[-1]: - print "" # add a blank line between bugs + bugname = shortname + is_comment = False + if is_comment == True and options.comments == False: + continue + bug = bd.bug_from_shortname(bugname) + if is_comment == False: + if options.XML: + print bug.xml(show_comments=options.comments) + else: + print bug.string(show_comments=options.comments) + else: + comment = bug.comment_root.comment_from_shortname( + shortname, bug_shortname=bugname) + if options.XML: + print comment.xml(shortname=shortname) + else: + if len(args) == 1 and options.only_raw_body == True: + sys.__stdout__.write(comment.body) + else: + print comment.string(shortname=shortname) + if shortname != args[-1] and options.XML == False: + print "" # add a blank line between bugs/comments def get_parser(): - parser = cmdutil.CmdOptionParser("be show [options] BUG-ID [BUG-ID ...]") - parser.add_option("-x", "--xml", action="store_true", - dest='dumpXML', help="Dump as XML") + parser = cmdutil.CmdOptionParser("be show [options] ID [ID ...]") + parser.add_option("-x", "--xml", action="store_true", default=False, + dest='XML', help="Dump as XML") + parser.add_option("--only-raw-body", action="store_true", + dest='only_raw_body', + help="When printing only a single comment, just print it's body. This allows extraction of non-text content types.") + parser.add_option("-c", "--no-comments", dest="comments", + action="store_false", default=True, + help="Disable comment output. This is useful if you just want more details on a bug's current status.") return parser longhelp=""" -Show all information about a bug. +Show all information about the bugs or comments whose IDs are given. + +It's probably not a good idea to mix bug and comment IDs in a single +call, but you're free to do so if you like. """ def help(): diff --git a/becommands/status.py b/becommands/status.py index d8bd4c4..edc948d 100644 --- a/becommands/status.py +++ b/becommands/status.py @@ -1,19 +1,18 @@ # Copyright (C) 2008-2009 W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Show or change a bug's status""" from libbe import cmdutil, bugdir, bug __desc__ = __doc__ @@ -48,7 +47,6 @@ def execute(args, test=False): if e.name != "status": raise raise cmdutil.UserError ("Invalid status: %s" % e.value) - bd.save() def get_parser(): parser = cmdutil.CmdOptionParser("be status BUG-ID [STATUS]") @@ -56,24 +54,39 @@ def get_parser(): def help(): - longhelp=[""" -Show or change a bug's status. - -If no status is specified, the current value is printed. If a status -is specified, it will be assigned to the bug. - -Status levels are: -"""] try: # See if there are any per-tree status configurations bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False) except bugdir.NoBugDir, e: pass # No tree, just show the defaults longest_status_len = max([len(s) for s in bug.status_values]) - for status in bug.status_values : + active_statuses = [] + for status in bug.active_status_values : + description = bug.status_description[status] + s = "%*s : %s" % (longest_status_len, status, description) + active_statuses.append(s) + inactive_statuses = [] + for status in bug.inactive_status_values : description = bug.status_description[status] - s = "%*s : %s\n" % (longest_status_len, status, description) - longhelp.append(s) - longhelp = ''.join(longhelp) + s = "%*s : %s" % (longest_status_len, status, description) + inactive_statuses.append(s) + longhelp=""" +Show or change a bug's status. + +If no status is specified, the current value is printed. If a status +is specified, it will be assigned to the bug. + +There are two classes of statuses, active and inactive, which are only +important for commands like "be list" that show only active bugs by +default. + +Active status levels are: + %s +Inactive status levels are: + %s + +You can overide the list of allowed statuses on a per-repository basis. +See "be set --help" for more details. +""" % ('\n '.join(active_statuses), '\n '.join(inactive_statuses)) return get_parser().help_str() + longhelp def complete(options, args, parser): diff --git a/becommands/tag.py b/becommands/tag.py index ab0324e..216ffbc 100644 --- a/becommands/tag.py +++ b/becommands/tag.py @@ -1,18 +1,18 @@ # Copyright (C) 2009 W. Trevor King # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Tag a bug, or search bugs for tags""" from libbe import cmdutil, bugdir import os, copy @@ -22,6 +22,7 @@ def execute(args, test=False): """ >>> from libbe import utility >>> bd = bugdir.simple_bug_dir() + >>> bd.set_sync_with_disk(True) >>> os.chdir(bd.root) >>> a = bd.bug_from_shortname("a") >>> print a.extra_strings @@ -56,7 +57,6 @@ def execute(args, test=False): >>> a.extra_strings = [] >>> print a.extra_strings [] - >>> a.save() >>> execute(["a"], test=True) >>> bd._clear_bugs() # resync our copy of bug >>> a = bd.bug_from_shortname("a") @@ -102,7 +102,6 @@ def execute(args, test=False): else: # add the tag estrs.append(tag_string) bug.extra_strings = estrs # reassign to notice change - bug.save() tags = [] for estr in bug.extra_strings: diff --git a/becommands/target.py b/becommands/target.py index 283998a..527b16a 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -4,23 +4,22 @@ # Marien Zwart # Thomas Gerigk # W. Trevor King -# # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Show or change a bug's target for fixing""" -from libbe import cmdutil, bugdir, settings_object +from libbe import cmdutil, bugdir __desc__ = __doc__ def execute(args, test=False): @@ -56,7 +55,7 @@ def execute(args, test=False): return bug = bd.bug_from_shortname(args[0]) if len(args) == 1: - if bug.target is None or bug.target is settings_object.EMPTY: + if bug.target is None: print "No target assigned." else: print bug.target @@ -66,7 +65,6 @@ def execute(args, test=False): bug.target = None else: bug.target = args[1] - bd.save() def get_parser(): parser = cmdutil.CmdOptionParser("be target BUG-ID [TARGET]\nor: be target --list") diff --git a/doc/module.mk b/doc/module.mk index 828f15f..7791f48 100644 --- a/doc/module.mk +++ b/doc/module.mk @@ -4,9 +4,21 @@ # Part of Bugs Everywhere, a distributed bug tracking system. # # Copyright (C) 2008-2009 Chris Ball -# This is free software; you may copy, modify and/or distribute this work -# under the terms of the GNU General Public License, version 2 or later. -# No warranty expressed or implied. See the file COPYING for details. +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # Makefile module for documentation diff --git a/xml/catmutt b/interfaces/email/catmutt old mode 100755 new mode 100644 similarity index 100% rename from xml/catmutt rename to interfaces/email/catmutt diff --git a/interfaces/gui/beg/beg b/interfaces/gui/beg/beg new file mode 100644 index 0000000..55e537d --- /dev/null +++ b/interfaces/gui/beg/beg @@ -0,0 +1,12 @@ +#!/usr/bin/env python +import table +from Tkinter import * +from libbe import bugdir + +tk = Tk() +Label(tk, text="Bug list").pack() +mlb = table.MultiListbox(tk, (('Severity', 4), ('Creator', 8), ('Summary', 40))) +for bug in [b for b in bugdir.tree_root(".").list() if b.active]: + mlb.insert(END, (bug.severity, bug.creator, bug.summary)) +mlb.pack(expand=YES,fill=BOTH) +tk.mainloop() diff --git a/interfaces/gui/beg/table.py b/interfaces/gui/beg/table.py new file mode 100644 index 0000000..2865f28 --- /dev/null +++ b/interfaces/gui/beg/table.py @@ -0,0 +1,97 @@ +from Tkinter import * + +class MultiListbox(Frame): + def __init__(self, master, lists): + Frame.__init__(self, master) + self.lists = [] + for l,w in lists: + frame = Frame(self); frame.pack(side=LEFT, expand=YES, fill=BOTH) + Label(frame, text=l, borderwidth=1, relief=RAISED).pack(fill=X) + lb = Listbox(frame, width=w, borderwidth=0, selectborderwidth=0, + relief=FLAT, exportselection=FALSE) + lb.pack(expand=YES, fill=BOTH) + self.lists.append(lb) + lb.bind('', lambda e, s=self: s._select(e.y)) + lb.bind('', lambda e, s=self: s._select(e.y)) + lb.bind('', lambda e: 'break') + lb.bind('', lambda e, s=self: s._b2motion(e.x, e.y)) + lb.bind('', lambda e, s=self: s._button2(e.x, e.y)) + frame = Frame(self); frame.pack(side=LEFT, fill=Y) + Label(frame, borderwidth=1, relief=RAISED).pack(fill=X) + sb = Scrollbar(frame, orient=VERTICAL, command=self._scroll) + sb.pack(expand=YES, fill=Y) + self.lists[0]['yscrollcommand']=sb.set + + def _select(self, y): + row = self.lists[0].nearest(y) + self.selection_clear(0, END) + self.selection_set(row) + return 'break' + + def _button2(self, x, y): + for l in self.lists: l.scan_mark(x, y) + return 'break' + + def _b2motion(self, x, y): + for l in self.lists: l.scan_dragto(x, y) + return 'break' + + def _scroll(self, *args): + for l in self.lists: + apply(l.yview, args) + + def curselection(self): + return self.lists[0].curselection() + + def delete(self, first, last=None): + for l in self.lists: + l.delete(first, last) + + def get(self, first, last=None): + result = [] + for l in self.lists: + result.append(l.get(first,last)) + if last: return apply(map, [None] + result) + return result + + def index(self, index): + self.lists[0].index(index) + + def insert(self, index, *elements): + for e in elements: + i = 0 + for l in self.lists: + l.insert(index, e[i]) + i = i + 1 + + def size(self): + return self.lists[0].size() + + def see(self, index): + for l in self.lists: + l.see(index) + + def selection_anchor(self, index): + for l in self.lists: + l.selection_anchor(index) + + def selection_clear(self, first, last=None): + for l in self.lists: + l.selection_clear(first, last) + + def selection_includes(self, index): + return self.lists[0].selection_includes(index) + + def selection_set(self, first, last=None): + for l in self.lists: + l.selection_set(first, last) + +if __name__ == '__main__': + tk = Tk() + Label(tk, text='MultiListbox').pack() + mlb = MultiListbox(tk, (('Subject', 40), ('Sender', 20), ('Date', 10))) + for i in range(1000): + mlb.insert(END, ('Important Message: %d' % i, 'John Doe', '10/10/%04d' % (1900+i))) + mlb.pack(expand=YES,fill=BOTH) + tk.mainloop() + diff --git a/interfaces/gui/wxbe/wxbe b/interfaces/gui/wxbe/wxbe new file mode 100644 index 0000000..e71ae0c --- /dev/null +++ b/interfaces/gui/wxbe/wxbe @@ -0,0 +1,87 @@ +#!/usr/bin/env python +import wx +from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin +import sys, os.path +from libbe import bugdir, names +from libbe.bug import cmp_status, cmp_severity, cmp_time, cmp_full + +class MyApp(wx.App): + def OnInit(self): + frame = BugListFrame(None, title="Bug List") + frame.Show(True) + self.SetTopWindow(frame) + return True + +class BugListFrame(wx.Frame): + def __init__(self, *args, **kwargs): + wx.Frame.__init__(self, *args, **kwargs) + bugs = BugList(self) + + # Widgets to display/sort/edit will go in this panel + # for now it is just a placeholder + panel = wx.Panel(self) + panel.SetBackgroundColour("RED") + + vbox = wx.BoxSizer(wx.VERTICAL) + vbox.Add(panel, 0, wx.EXPAND) + vbox.Add(bugs, 1, wx.EXPAND) + + self.SetAutoLayout(True) + self.SetSizer(vbox) + self.Layout() + +class BugList(wx.ListCtrl, ListCtrlAutoWidthMixin): + def __init__(self, parent): + wx.ListCtrl.__init__(self, parent, + style=wx.LC_REPORT) + ListCtrlAutoWidthMixin.__init__(self) + + self.bugdir = bugdir.tree_root(".") + self.buglist = list(self.bugdir.list()) + self.buglist.sort() + self.columns = ("id", "status", "severity", "summary") + + dataIndex = 0 + for x in range(len(self.columns)): + self.InsertColumn(x, self.columns[x].capitalize()) + self.SetColumnWidth(x, wx.LIST_AUTOSIZE_USEHEADER) + for bug in [b for b in self.buglist if b.active]: + name = names.unique_name(bug, self.buglist) + id = self.InsertStringItem(self.GetItemCount(), name) + self.SetStringItem(id, 1, bug.status) + self.SetStringItem(id, 2, bug.severity) + self.SetStringItem(id, 3, bug.summary) + self.SetItemData(id, dataIndex) # set keys for each line + dataIndex += 1 + self.EnsureVisible(id) + for x in range(len(self.columns)): + self.SetColumnWidth(x, wx.LIST_AUTOSIZE) + conts_width = self.GetColumnWidth(x) + self.SetColumnWidth(x, wx.LIST_AUTOSIZE_USEHEADER) + if conts_width > self.GetColumnWidth(x): + self.SetColumnWidth(x, conts_width) + + self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColumnClick) + self.bugcmp_fn = cmp_full + # For reasons I don't understant, sorting is broken... + #self.SortItems(self.Sorter) + #self.Refresh() + def Sorter(self, key1, key2): + """Get bug info from the keys and pass to self.bugcmp_fn""" + bug1 = self.buglist[key1-1] + bug2 = self.buglist[key2-1] + # Another way of getting bug information + #bug1uuid = self.GetItem(key1, 0).GetText() + #bug2uuid = self.GetItem(key2, 0).GetText() + #print bug1uuid, bug2uuid + #bug1 = self.bugdir.get_bug(bug1uuid) + #bug2 = self.bugdir.get_bug(bug1uuid) + print self.bugcmp_fn(bug1,bug2) + return self.bugcmp_fn(bug1,bug2) + def OnColumnClick(self, event): + """Resort bug list depending on which column was clicked""" + print "TODO: sort by column %d" % event.Column + # change self.bugcmp_fn and resort, but I can't get it working + +app = MyApp() +app.MainLoop() diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/PKG-INFO b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/PKG-INFO similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/PKG-INFO rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/PKG-INFO diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/SOURCES.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/SOURCES.txt similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/SOURCES.txt rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/SOURCES.txt diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/dependency_links.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/dependency_links.txt similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/dependency_links.txt rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/dependency_links.txt diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/not-zip-safe b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/not-zip-safe similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/not-zip-safe rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/not-zip-safe diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/paster_plugins.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/paster_plugins.txt similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/paster_plugins.txt rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/paster_plugins.txt diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/requires.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/requires.txt similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/requires.txt rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/requires.txt diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/sqlobject.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/sqlobject.txt similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/sqlobject.txt rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/sqlobject.txt diff --git a/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/top_level.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/top_level.txt similarity index 100% rename from Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/top_level.txt rename to interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/top_level.txt diff --git a/Bugs-Everywhere-Web/README.txt b/interfaces/web/Bugs-Everywhere-Web/README.txt similarity index 100% rename from Bugs-Everywhere-Web/README.txt rename to interfaces/web/Bugs-Everywhere-Web/README.txt diff --git a/Bugs-Everywhere-Web/beweb/app.cfg b/interfaces/web/Bugs-Everywhere-Web/beweb/app.cfg similarity index 100% rename from Bugs-Everywhere-Web/beweb/app.cfg rename to interfaces/web/Bugs-Everywhere-Web/beweb/app.cfg diff --git a/Bugs-Everywhere-Web/beweb/config.py.example b/interfaces/web/Bugs-Everywhere-Web/beweb/config.py.example similarity index 100% rename from Bugs-Everywhere-Web/beweb/config.py.example rename to interfaces/web/Bugs-Everywhere-Web/beweb/config.py.example diff --git a/Bugs-Everywhere-Web/beweb/config/app.cfg b/interfaces/web/Bugs-Everywhere-Web/beweb/config/app.cfg similarity index 100% rename from Bugs-Everywhere-Web/beweb/config/app.cfg rename to interfaces/web/Bugs-Everywhere-Web/beweb/config/app.cfg diff --git a/Bugs-Everywhere-Web/beweb/config/log.cfg b/interfaces/web/Bugs-Everywhere-Web/beweb/config/log.cfg similarity index 100% rename from Bugs-Everywhere-Web/beweb/config/log.cfg rename to interfaces/web/Bugs-Everywhere-Web/beweb/config/log.cfg diff --git a/Bugs-Everywhere-Web/beweb/controllers.py b/interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py similarity index 100% rename from Bugs-Everywhere-Web/beweb/controllers.py rename to interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py diff --git a/Bugs-Everywhere-Web/beweb/formatting.py b/interfaces/web/Bugs-Everywhere-Web/beweb/formatting.py similarity index 100% rename from Bugs-Everywhere-Web/beweb/formatting.py rename to interfaces/web/Bugs-Everywhere-Web/beweb/formatting.py diff --git a/Bugs-Everywhere-Web/beweb/json.py b/interfaces/web/Bugs-Everywhere-Web/beweb/json.py similarity index 100% rename from Bugs-Everywhere-Web/beweb/json.py rename to interfaces/web/Bugs-Everywhere-Web/beweb/json.py diff --git a/Bugs-Everywhere-Web/beweb/model.py b/interfaces/web/Bugs-Everywhere-Web/beweb/model.py similarity index 100% rename from Bugs-Everywhere-Web/beweb/model.py rename to interfaces/web/Bugs-Everywhere-Web/beweb/model.py diff --git a/Bugs-Everywhere-Web/beweb/prest.py b/interfaces/web/Bugs-Everywhere-Web/beweb/prest.py similarity index 100% rename from Bugs-Everywhere-Web/beweb/prest.py rename to interfaces/web/Bugs-Everywhere-Web/beweb/prest.py diff --git a/Bugs-Everywhere-Web/beweb/release.py b/interfaces/web/Bugs-Everywhere-Web/beweb/release.py similarity index 100% rename from Bugs-Everywhere-Web/beweb/release.py rename to interfaces/web/Bugs-Everywhere-Web/beweb/release.py diff --git a/Bugs-Everywhere-Web/beweb/static/css/style.css b/interfaces/web/Bugs-Everywhere-Web/beweb/static/css/style.css similarity index 100% rename from Bugs-Everywhere-Web/beweb/static/css/style.css rename to interfaces/web/Bugs-Everywhere-Web/beweb/static/css/style.css diff --git a/Bugs-Everywhere-Web/beweb/templates/about.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/about.kid similarity index 100% rename from Bugs-Everywhere-Web/beweb/templates/about.kid rename to interfaces/web/Bugs-Everywhere-Web/beweb/templates/about.kid diff --git a/Bugs-Everywhere-Web/beweb/templates/bugs.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/bugs.kid similarity index 100% rename from Bugs-Everywhere-Web/beweb/templates/bugs.kid rename to interfaces/web/Bugs-Everywhere-Web/beweb/templates/bugs.kid diff --git a/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid similarity index 100% rename from Bugs-Everywhere-Web/beweb/templates/edit_bug.kid rename to interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid diff --git a/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid similarity index 100% rename from Bugs-Everywhere-Web/beweb/templates/edit_comment.kid rename to interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid diff --git a/Bugs-Everywhere-Web/beweb/templates/error.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/error.kid similarity index 100% rename from Bugs-Everywhere-Web/beweb/templates/error.kid rename to interfaces/web/Bugs-Everywhere-Web/beweb/templates/error.kid diff --git a/Bugs-Everywhere-Web/beweb/templates/login.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/login.kid similarity index 100% rename from Bugs-Everywhere-Web/beweb/templates/login.kid rename to interfaces/web/Bugs-Everywhere-Web/beweb/templates/login.kid diff --git a/Bugs-Everywhere-Web/beweb/templates/master.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/master.kid similarity index 100% rename from Bugs-Everywhere-Web/beweb/templates/master.kid rename to interfaces/web/Bugs-Everywhere-Web/beweb/templates/master.kid diff --git a/Bugs-Everywhere-Web/beweb/templates/projects.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/projects.kid similarity index 100% rename from Bugs-Everywhere-Web/beweb/templates/projects.kid rename to interfaces/web/Bugs-Everywhere-Web/beweb/templates/projects.kid diff --git a/Bugs-Everywhere-Web/beweb/templates/welcome.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/welcome.kid similarity index 100% rename from Bugs-Everywhere-Web/beweb/templates/welcome.kid rename to interfaces/web/Bugs-Everywhere-Web/beweb/templates/welcome.kid diff --git a/Bugs-Everywhere-Web/beweb/tests/test_controllers.py b/interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_controllers.py similarity index 100% rename from Bugs-Everywhere-Web/beweb/tests/test_controllers.py rename to interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_controllers.py diff --git a/Bugs-Everywhere-Web/beweb/tests/test_model.py b/interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_model.py similarity index 100% rename from Bugs-Everywhere-Web/beweb/tests/test_model.py rename to interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_model.py diff --git a/Bugs-Everywhere-Web/dev.cfg b/interfaces/web/Bugs-Everywhere-Web/dev.cfg similarity index 100% rename from Bugs-Everywhere-Web/dev.cfg rename to interfaces/web/Bugs-Everywhere-Web/dev.cfg diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/arch.py b/interfaces/web/Bugs-Everywhere-Web/libbe/arch.py new file mode 100644 index 0000000..2f45aa9 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/arch.py @@ -0,0 +1,295 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Ben Finney +# James Rowe +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import codecs +import os +import re +import shutil +import sys +import time +import unittest +import doctest + +import config +from beuuid import uuid_gen +import rcs +from rcs import RCS + +DEFAULT_CLIENT = "tla" + +client = config.get_val("arch_client", default=DEFAULT_CLIENT) + +def new(): + return Arch() + +class Arch(RCS): + name = "Arch" + client = client + versioned = True + _archive_name = None + _archive_dir = None + _tmp_archive = False + _project_name = None + _tmp_project = False + _arch_paramdir = os.path.expanduser("~/.arch-params") + def _rcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _rcs_detect(self, path): + """Detect whether a directory is revision-controlled using Arch""" + if self._u_search_parent_directories(path, "{arch}") != None : + config.set_val("arch_client", client) + return True + return False + def _rcs_init(self, path): + self._create_archive(path) + self._create_project(path) + self._add_project_code(path) + def _create_archive(self, path): + # Create a new archive + # http://regexps.srparish.net/tutorial-tla/new-archive.html#Creating_a_New_Archive + assert self._archive_name == None + id = self.get_user_id() + name, email = self._u_parse_id(id) + if email == None: + email = "%s@example.com" % name + trailer = "%s-%s" % ("bugs-everywhere-auto", uuid_gen()[0:8]) + self._archive_name = "%s--%s" % (email, trailer) + self._archive_dir = "/tmp/%s" % trailer + self._tmp_archive = True + self._u_invoke_client("make-archive", self._archive_name, + self._archive_dir, directory=path) + def _invoke_client(self, *args, **kwargs): + """ + Invoke the client on our archive. + """ + assert self._archive_name != None + command = args[0] + if len(args) > 1: + tailargs = args[1:] + else: + tailargs = [] + arglist = [command, "-A", self._archive_name] + arglist.extend(tailargs) + args = tuple(arglist) + return self._u_invoke_client(*args, **kwargs) + def _remove_archive(self): + assert self._tmp_archive == True + assert self._archive_dir != None + assert self._archive_name != None + os.remove(os.path.join(self._arch_paramdir, + "=locations", self._archive_name)) + shutil.rmtree(self._archive_dir) + self._tmp_archive = False + self._archive_dir = False + self._archive_name = False + def _create_project(self, path): + """ + Create a temporary Arch project in the directory PATH. This + project will be removed by + __del__->cleanup->_rcs_cleanup->_remove_project + """ + # http://mwolson.org/projects/GettingStartedWithArch.html + # http://regexps.srparish.net/tutorial-tla/new-project.html#Starting_a_New_Project + category = "bugs-everywhere" + branch = "mainline" + version = "0.1" + self._project_name = "%s--%s--%s" % (category, branch, version) + self._invoke_client("archive-setup", self._project_name, + directory=path) + self._tmp_project = True + def _remove_project(self): + assert self._tmp_project == True + assert self._project_name != None + assert self._archive_dir != None + shutil.rmtree(os.path.join(self._archive_dir, self._project_name)) + self._tmp_project = False + self._project_name = False + def _archive_project_name(self): + assert self._archive_name != None + assert self._project_name != None + return "%s/%s" % (self._archive_name, self._project_name) + def _adjust_naming_conventions(self, path): + """ + By default, Arch restricts source code filenames to + ^[_=a-zA-Z0-9].*$ + See + http://regexps.srparish.net/tutorial-tla/naming-conventions.html + Since our bug directory '.be' doesn't satisfy these conventions, + we need to adjust them. + + The conventions are specified in + project-root/{arch}/=tagging-method + """ + tagpath = os.path.join(path, "{arch}", "=tagging-method") + lines_out = [] + f = codecs.open(tagpath, "r", self.encoding) + for line in f: + if line.startswith("source "): + lines_out.append("source ^[._=a-zA-X0-9].*$\n") + else: + lines_out.append(line) + f.close() + f = codecs.open(tagpath, "w", self.encoding) + f.write("".join(lines_out)) + f.close() + + def _add_project_code(self, path): + # http://mwolson.org/projects/GettingStartedWithArch.html + # http://regexps.srparish.net/tutorial-tla/new-source.html + # http://regexps.srparish.net/tutorial-tla/importing-first.html + self._invoke_client("init-tree", self._project_name, + directory=path) + self._adjust_naming_conventions(path) + self._invoke_client("import", "--summary", "Began versioning", + directory=path) + def _rcs_cleanup(self): + if self._tmp_project == True: + self._remove_project() + if self._tmp_archive == True: + self._remove_archive() + + def _rcs_root(self, path): + if not os.path.isdir(path): + dirname = os.path.dirname(path) + else: + dirname = path + status,output,error = self._u_invoke_client("tree-root", dirname) + root = output.rstrip('\n') + + self._get_archive_project_name(root) + + return root + + def _get_archive_name(self, root): + status,output,error = self._u_invoke_client("archives") + lines = output.split('\n') + # e.g. output: + # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52 + # /tmp/BEtestXXXXXX/rootdir + # (+ repeats) + for archive,location in zip(lines[::2], lines[1::2]): + if os.path.realpath(location) == os.path.realpath(root): + self._archive_name = archive + assert self._archive_name != None + + def _get_archive_project_name(self, root): + # get project names + status,output,error = self._u_invoke_client("tree-version", directory=root) + # e.g output + # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52/be--mainline--0.1 + archive_name,project_name = output.rstrip('\n').split('/') + self._archive_name = archive_name + self._project_name = project_name + def _rcs_get_user_id(self): + try: + status,output,error = self._u_invoke_client('my-id') + return output.rstrip('\n') + except Exception, e: + if 'no arch user id set' in e.args[0]: + return None + else: + raise + def _rcs_set_user_id(self, value): + self._u_invoke_client('my-id', value) + def _rcs_add(self, path): + self._u_invoke_client("add-id", path) + realpath = os.path.realpath(self._u_abspath(path)) + pathAdded = realpath in self._list_added(self.rootdir) + if self.paranoid and not pathAdded: + self._force_source(path) + def _list_added(self, root): + assert os.path.exists(root) + assert os.access(root, os.X_OK) + root = os.path.realpath(root) + status,output,error = self._u_invoke_client("inventory", "--source", + "--both", "--all", root) + inv_str = output.rstrip('\n') + return [os.path.join(root, p) for p in inv_str.split('\n')] + def _add_dir_rule(self, rule, dirname, root): + inv_path = os.path.join(dirname, '.arch-inventory') + f = codecs.open(inv_path, "a", self.encoding) + f.write(rule) + f.close() + if os.path.realpath(inv_path) not in self._list_added(root): + paranoid = self.paranoid + self.paranoid = False + self.add(inv_path) + self.paranoid = paranoid + def _force_source(self, path): + rule = "source %s\n" % self._u_rel_path(path) + self._add_dir_rule(rule, os.path.dirname(path), self.rootdir) + if os.path.realpath(path) not in self._list_added(self.rootdir): + raise CantAddFile(path) + def _rcs_remove(self, path): + if not '.arch-ids' in path: + self._u_invoke_client("delete-id", path) + def _rcs_update(self, path): + pass + def _rcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + else: + status,output,error = \ + self._invoke_client("file-find", path, revision) + relpath = output.rstrip('\n') + abspath = os.path.join(self.rootdir, relpath) + f = codecs.open(abspath, "r", self.encoding) + contents = f.read() + f.close() + return contents + def _rcs_duplicate_repo(self, directory, revision=None): + if revision == None: + RCS._rcs_duplicate_repo(self, directory, revision) + else: + status,output,error = \ + self._u_invoke_client("get", revision,directory) + def _rcs_commit(self, commitfile, allow_empty=False): + if allow_empty == False: + # arch applies empty commits without complaining, so check first + status,output,error = self._u_invoke_client("changes",expect=(0,1)) + if status == 0: + raise rcs.EmptyCommit() + summary,body = self._u_parse_commitfile(commitfile) + args = ["commit", "--summary", summary] + if body != None: + args.extend(["--log-message",body]) + status,output,error = self._u_invoke_client(*args) + revision = None + revline = re.compile("[*] committed (.*)") + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 1 + revpath = match.groups()[0] + assert not " " in revpath, revpath + assert revpath.startswith(self._archive_project_name()+'--') + revision = revpath[len(self._archive_project_name()+'--'):] + return revpath + +class CantAddFile(Exception): + def __init__(self, file): + self.file = file + Exception.__init__(self, "Can't automatically add file %s" % file) + + + +rcs.make_rcs_testcase_subclasses(Arch, sys.modules[__name__]) + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/beuuid.py b/interfaces/web/Bugs-Everywhere-Web/libbe/beuuid.py new file mode 100644 index 0000000..bc47208 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/beuuid.py @@ -0,0 +1,61 @@ +# Copyright (C) 2008-2009 W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Backwards compatibility support for Python 2.4. Once people give up +on 2.4 ;), the uuid call should be merged into bugdir.py +""" + +import unittest + +try: + from uuid import uuid4 # Python >= 2.5 + def uuid_gen(): + id = uuid4() + idstr = id.urn + start = "urn:uuid:" + assert idstr.startswith(start) + return idstr[len(start):] +except ImportError: + import os + import sys + from subprocess import Popen, PIPE + + def uuid_gen(): + # Shell-out to system uuidgen + args = ['uuidgen', 'r'] + try: + if sys.platform != "win32": + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) + else: + # win32 don't have os.execvp() so have to run command in a shell + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, + shell=True, cwd=cwd) + except OSError, e : + strerror = "%s\nwhile executing %s" % (e.args[1], args) + raise OSError, strerror + output, error = q.communicate() + status = q.wait() + if status != 0: + strerror = "%s\nwhile executing %s" % (status, args) + raise Exception, strerror + return output.rstrip('\n') + +class UUIDtestCase(unittest.TestCase): + def testUUID_gen(self): + id = uuid_gen() + self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) + +suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/bug.py b/interfaces/web/Bugs-Everywhere-Web/libbe/bug.py new file mode 100644 index 0000000..c1e5481 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/bug.py @@ -0,0 +1,547 @@ +# Copyright (C) 2008-2009 Chris Ball +# Thomas Habets +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import os +import os.path +import errno +import time +import types +import xml.sax.saxutils +import doctest + +from beuuid import uuid_gen +from properties import Property, doc_property, local_property, \ + defaulting_property, checked_property, cached_property, \ + primed_property, change_hook_property, settings_property +import settings_object +import mapfile +import comment +import utility + + +### Define and describe valid bug categories +# Use a tuple of (category, description) tuples since we don't have +# ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/ + +# in order of increasing severity. (name, description) pairs +severity_def = ( + ("wishlist","A feature that could improve usefulness, but not a bug."), + ("minor","The standard bug level."), + ("serious","A bug that requires workarounds."), + ("critical","A bug that prevents some features from working at all."), + ("fatal","A bug that makes the package unusable.")) + +# in order of increasing resolution +# roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html +active_status_def = ( + ("unconfirmed","A possible bug which lacks independent existance confirmation."), + ("open","A working bug that has not been assigned to a developer."), + ("assigned","A working bug that has been assigned to a developer."), + ("test","The code has been adjusted, but the fix is still being tested.")) +inactive_status_def = ( + ("closed", "The bug is no longer relevant."), + ("fixed", "The bug should no longer occur."), + ("wontfix","It's not a bug, it's a feature.")) + + +### Convert the description tuples to more useful formats + +severity_values = () +severity_description = {} +severity_index = {} +def load_severities(severity_def): + global severity_values + global severity_description + global severity_index + if severity_def == None: + return + severity_values = tuple([val for val,description in severity_def]) + severity_description = dict(severity_def) + severity_index = {} + for i,severity in enumerate(severity_values): + severity_index[severity] = i +load_severities(severity_def) + +active_status_values = [] +inactive_status_values = [] +status_values = [] +status_description = {} +status_index = {} +def load_status(active_status_def, inactive_status_def): + global active_status_values + global inactive_status_values + global status_values + global status_description + global status_index + if active_status_def == None: + active_status_def = globals()["active_status_def"] + if inactive_status_def == None: + inactive_status_def = globals()["inactive_status_def"] + active_status_values = tuple([val for val,description in active_status_def]) + inactive_status_values = tuple([val for val,description in inactive_status_def]) + status_values = active_status_values + inactive_status_values + status_description = dict(tuple(active_status_def) + tuple(inactive_status_def)) + status_index = {} + for i,status in enumerate(status_values): + status_index[status] = i +load_status(active_status_def, inactive_status_def) + + +class Bug(settings_object.SavedSettingsObject): + """ + >>> b = Bug() + >>> print b.status + open + >>> print b.severity + minor + + There are two formats for time, int and string. Setting either + one will adjust the other appropriately. The string form is the + one stored in the bug's settings file on disk. + >>> print type(b.time) + + >>> print type(b.time_string) + + >>> b.time = 0 + >>> print b.time_string + Thu, 01 Jan 1970 00:00:00 +0000 + >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000" + >>> b.time + 60 + >>> print b.settings["time"] + Thu, 01 Jan 1970 00:01:00 +0000 + """ + settings_properties = [] + required_saved_properties = [] + _prop_save_settings = settings_object.prop_save_settings + _prop_load_settings = settings_object.prop_load_settings + def _versioned_property(settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"]=required_saved_properties + return settings_object.versioned_property(**kwargs) + + @_versioned_property(name="severity", + doc="A measure of the bug's importance", + default="minor", + check_fn=lambda s: s in severity_values, + require_save=True) + def severity(): return {} + + @_versioned_property(name="status", + doc="The bug's current status", + default="open", + check_fn=lambda s: s in status_values, + require_save=True) + def status(): return {} + + @property + def active(self): + return self.status in active_status_values + + @_versioned_property(name="target", + doc="The deadline for fixing this bug") + def target(): return {} + + @_versioned_property(name="creator", + doc="The user who entered the bug into the system") + def creator(): return {} + + @_versioned_property(name="reporter", + doc="The user who reported the bug") + def reporter(): return {} + + @_versioned_property(name="assigned", + doc="The developer in charge of the bug") + def assigned(): return {} + + @_versioned_property(name="time", + doc="An RFC 2822 timestamp for bug creation") + def time_string(): return {} + + def _get_time(self): + if self.time_string == None: + return None + return utility.str_to_time(self.time_string) + def _set_time(self, value): + self.time_string = utility.time_to_str(value) + time = property(fget=_get_time, + fset=_set_time, + doc="An integer version of .time_string") + + def _extra_strings_check_fn(value): + return utility.iterable_full_of_strings(value, \ + alternative=settings_object.EMPTY) + def _extra_strings_change_hook(self, old, new): + self.extra_strings.sort() # to make merging easier + self._prop_save_settings(old, new) + @_versioned_property(name="extra_strings", + doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/.py.", + default=[], + check_fn=_extra_strings_check_fn, + change_hook=_extra_strings_change_hook, + mutable=True) + def extra_strings(): return {} + + @_versioned_property(name="summary", + doc="A one-line bug description") + def summary(): return {} + + def _get_comment_root(self, load_full=False): + if self.sync_with_disk: + return comment.loadComments(self, load_full=load_full) + else: + return comment.Comment(self, uuid=comment.INVALID_UUID) + + @Property + @cached_property(generator=_get_comment_root) + @local_property("comment_root") + @doc_property(doc="The trunk of the comment tree") + def comment_root(): return {} + + def _get_rcs(self): + if hasattr(self.bugdir, "rcs"): + return self.bugdir.rcs + + @Property + @cached_property(generator=_get_rcs) + @local_property("rcs") + @doc_property(doc="A revision control system instance.") + def rcs(): return {} + + def __init__(self, bugdir=None, uuid=None, from_disk=False, + load_comments=False, summary=None): + settings_object.SavedSettingsObject.__init__(self) + self.bugdir = bugdir + self.uuid = uuid + if from_disk == True: + self.sync_with_disk = True + else: + self.sync_with_disk = False + if uuid == None: + self.uuid = uuid_gen() + self.time = int(time.time()) # only save to second precision + if self.rcs != None: + self.creator = self.rcs.get_user_id() + self.summary = summary + + def __repr__(self): + return "Bug(uuid=%r)" % self.uuid + + def set_sync_with_disk(self, value): + self.sync_with_disk = value + for comment in self.comments(): + comment.set_sync_with_disk(value) + + def _setting_attr_string(self, setting): + value = getattr(self, setting) + if value == None: + return "" + return str(value) + + def xml(self, show_comments=False): + if self.bugdir == None: + shortname = self.uuid + else: + shortname = self.bugdir.bug_shortname(self) + + if self.time == None: + timestring = "" + else: + timestring = utility.time_to_str(self.time) + + info = [("uuid", self.uuid), + ("short-name", shortname), + ("severity", self.severity), + ("status", self.status), + ("assigned", self.assigned), + ("target", self.target), + ("reporter", self.reporter), + ("creator", self.creator), + ("created", timestring), + ("summary", self.summary)] + ret = '\n' + for (k,v) in info: + if v is not None: + ret += ' <%s>%s\n' % (k,xml.sax.saxutils.escape(v),k) + for estr in self.extra_strings: + ret += ' %s\n' % estr + if show_comments == True: + comout = self.comment_root.xml_thread(auto_name_map=True, + bug_shortname=shortname) + if len(comout) > 0: + ret += comout+'\n' + ret += '' + return ret + + def string(self, shortlist=False, show_comments=False): + if self.bugdir == None: + shortname = self.uuid + else: + shortname = self.bugdir.bug_shortname(self) + if shortlist == False: + if self.time == None: + timestring = "" + else: + htime = utility.handy_time(self.time) + timestring = "%s (%s)" % (htime, self.time_string) + info = [("ID", self.uuid), + ("Short name", shortname), + ("Severity", self.severity), + ("Status", self.status), + ("Assigned", self._setting_attr_string("assigned")), + ("Target", self._setting_attr_string("target")), + ("Reporter", self._setting_attr_string("reporter")), + ("Creator", self._setting_attr_string("creator")), + ("Created", timestring)] + longest_key_len = max([len(k) for k,v in info]) + infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info] + bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n') + else: + statuschar = self.status[0] + severitychar = self.severity[0] + chars = "%c%c" % (statuschar, severitychar) + bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n')) + + if show_comments == True: + # take advantage of the string_thread(auto_name_map=True) + # SIDE-EFFECT of sorting by comment time. + comout = self.comment_root.string_thread(flatten=False, + auto_name_map=True, + bug_shortname=shortname) + output = bugout + '\n' + comout.rstrip('\n') + else : + output = bugout + return output + + def __str__(self): + return self.string(shortlist=True) + + def __cmp__(self, other): + return cmp_full(self, other) + + def get_path(self, name=None): + my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid) + if name is None: + return my_dir + assert name in ["values", "comments"] + return os.path.join(my_dir, name) + + def load_settings(self): + self.settings = mapfile.map_load(self.rcs, self.get_path("values")) + self._setup_saved_settings() + + def load_comments(self, load_full=True): + if load_full == True: + # Force a complete load of the whole comment tree + self.comment_root = self._get_comment_root(load_full=True) + else: + # Setup for fresh lazy-loading. Clear _comment_root, so + # _get_comment_root returns a fresh version. Turn of + # syncing temporarily so we don't write our blank comment + # tree to disk. + self.sync_with_disk = False + self.comment_root = None + self.sync_with_disk = True + + def save_settings(self): + assert self.summary != None, "Can't save blank bug" + + self.rcs.mkdir(self.get_path()) + path = self.get_path("values") + mapfile.map_save(self.rcs, path, self._get_saved_settings()) + + def save(self): + """ + Save any loaded contents to disk. Because of lazy loading of + comments, this is actually not too inefficient. + + However, if self.sync_with_disk = True, then any changes are + automatically written to disk as soon as they happen, so + calling this method will just waste time (unless something + else has been messing with your on-disk files). + """ + self.save_settings() + if len(self.comment_root) > 0: + comment.saveComments(self) + + def remove(self): + self.comment_root.remove() + path = self.get_path() + self.rcs.recursive_remove(path) + + def comments(self): + for comment in self.comment_root.traverse(): + yield comment + + def new_comment(self, body=None): + comm = self.comment_root.new_reply(body=body) + return comm + + def comment_from_shortname(self, shortname, *args, **kwargs): + return self.comment_root.comment_from_shortname(shortname, + *args, **kwargs) + + def comment_from_uuid(self, uuid): + return self.comment_root.comment_from_uuid(uuid) + + def comment_shortnames(self, shortname=None): + """ + SIDE-EFFECT : Comment.comment_shortnames will sort the comment + tree by comment.time + """ + for id, comment in self.comment_root.comment_shortnames(shortname): + yield (id, comment) + + +# The general rule for bug sorting is that "more important" bugs are +# less than "less important" bugs. This way sorting a list of bugs +# will put the most important bugs first in the list. When relative +# importance is unclear, the sorting follows some arbitrary convention +# (i.e. dictionary order). + +def cmp_severity(bug_1, bug_2): + """ + Compare the severity levels of two bugs, with more severe bugs + comparing as less. + >>> bugA = Bug() + >>> bugB = Bug() + >>> bugA.severity = bugB.severity = "wishlist" + >>> cmp_severity(bugA, bugB) == 0 + True + >>> bugB.severity = "minor" + >>> cmp_severity(bugA, bugB) > 0 + True + >>> bugA.severity = "critical" + >>> cmp_severity(bugA, bugB) < 0 + True + """ + if not hasattr(bug_2, "severity") : + return 1 + return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity]) + +def cmp_status(bug_1, bug_2): + """ + Compare the status levels of two bugs, with more 'open' bugs + comparing as less. + >>> bugA = Bug() + >>> bugB = Bug() + >>> bugA.status = bugB.status = "open" + >>> cmp_status(bugA, bugB) == 0 + True + >>> bugB.status = "closed" + >>> cmp_status(bugA, bugB) < 0 + True + >>> bugA.status = "fixed" + >>> cmp_status(bugA, bugB) > 0 + True + """ + if not hasattr(bug_2, "status") : + return 1 + val_2 = status_index[bug_2.status] + return cmp(status_index[bug_1.status], status_index[bug_2.status]) + +def cmp_attr(bug_1, bug_2, attr, invert=False): + """ + Compare a general attribute between two bugs using the conventional + comparison rule for that attribute type. If invert == True, sort + *against* that convention. + >>> attr="severity" + >>> bugA = Bug() + >>> bugB = Bug() + >>> bugA.severity = "critical" + >>> bugB.severity = "wishlist" + >>> cmp_attr(bugA, bugB, attr) < 0 + True + >>> cmp_attr(bugA, bugB, attr, invert=True) > 0 + True + >>> bugB.severity = "critical" + >>> cmp_attr(bugA, bugB, attr) == 0 + True + """ + if not hasattr(bug_2, attr) : + return 1 + val_1 = getattr(bug_1, attr) + val_2 = getattr(bug_2, attr) + if val_1 == None: val_1 = None + if val_2 == None: val_2 = None + + if invert == True : + return -cmp(val_1, val_2) + else : + return cmp(val_1, val_2) + +# alphabetical rankings (a < z) +cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator") +cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned") +# chronological rankings (newer < older) +cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True) + +def cmp_comments(bug_1, bug_2): + """ + Compare two bugs' comments lists. Doesn't load any new comments, + so you should call each bug's .load_comments() first if you want a + full comparison. + """ + comms_1 = sorted(bug_1.comments(), key = lambda comm : comm.uuid) + comms_2 = sorted(bug_2.comments(), key = lambda comm : comm.uuid) + result = cmp(len(comms_1), len(comms_2)) + if result != 0: + return result + for c_1,c_2 in zip(comms_1, comms_2): + result = cmp(c_1, c_2) + if result != 0: + return result + return 0 + +DEFAULT_CMP_FULL_CMP_LIST = \ + (cmp_status,cmp_severity,cmp_assigned,cmp_time,cmp_creator,cmp_comments) + +class BugCompoundComparator (object): + def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): + self.cmp_list = cmp_list + def __call__(self, bug_1, bug_2): + for comparison in self.cmp_list : + val = comparison(bug_1, bug_2) + if val != 0 : + return val + return 0 + +cmp_full = BugCompoundComparator() + + +# define some bonus cmp_* functions +def cmp_last_modified(bug_1, bug_2): + """ + Like cmp_time(), but use most recent comment instead of bug + creation for the timestamp. + """ + def last_modified(bug): + time = bug.time + for comment in bug.comment_root.traverse(): + if comment.time > time: + time = comment.time + return time + val_1 = last_modified(bug_1) + val_2 = last_modified(bug_2) + return -cmp(val_1, val_2) + + +suite = doctest.DocTestSuite() diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/bugdir.py b/interfaces/web/Bugs-Everywhere-Web/libbe/bugdir.py new file mode 100644 index 0000000..6e020ee --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/bugdir.py @@ -0,0 +1,676 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Alexander Belchenko +# Chris Ball +# Oleg Romanyshyn +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import os +import os.path +import errno +import time +import copy +import unittest +import doctest + +from properties import Property, doc_property, local_property, \ + defaulting_property, checked_property, fn_checked_property, \ + cached_property, primed_property, change_hook_property, \ + settings_property +import settings_object +import mapfile +import bug +import rcs +import encoding +import utility + + +class NoBugDir(Exception): + def __init__(self, path): + msg = "The directory \"%s\" has no bug directory." % path + Exception.__init__(self, msg) + self.path = path + +class NoRootEntry(Exception): + def __init__(self, path): + self.path = path + Exception.__init__(self, "Specified root does not exist: %s" % path) + +class AlreadyInitialized(Exception): + def __init__(self, path): + self.path = path + Exception.__init__(self, + "Specified root is already initialized: %s" % path) + +class MultipleBugMatches(ValueError): + def __init__(self, shortname, matches): + msg = ("More than one bug matches %s. " + "Please be more specific.\n%s" % (shortname, matches)) + ValueError.__init__(self, msg) + self.shortname = shortname + self.matches = matches + + +TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n" + + +class BugDir (list, settings_object.SavedSettingsObject): + """ + Sink to existing root + ====================== + + Consider the following usage case: + You have a bug directory rooted in + /path/to/source + by which I mean the '.be' directory is at + /path/to/source/.be + However, you're of in some subdirectory like + /path/to/source/GUI/testing + and you want to comment on a bug. Setting sink_to_root=True wen + you initialize your BugDir will cause it to search for the '.be' + file in the ancestors of the path you passed in as 'root'. + /path/to/source/GUI/testing/.be miss + /path/to/source/GUI/.be miss + /path/to/source/.be hit! + So it still roots itself appropriately without much work for you. + + File-system access + ================== + + BugDirs live completely in memory when .sync_with_disk is False. + This is the default configuration setup by BugDir(from_disk=False). + If .sync_with_disk == True (e.g. BugDir(from_disk=True)), then + any changes to the BugDir will be immediately written to disk. + + If you want to change .sync_with_disk, we suggest you use + .set_sync_with_disk(), which propogates the new setting through to + all bugs/comments/etc. that have been loaded into memory. If + you've been living in memory and want to move to + .sync_with_disk==True, but you're not sure if anything has been + changed in memoryy, a call to save() is a safe move. + + Regardless of .sync_with_disk, a call to .save() will write out + all the contents that the BugDir instance has loaded into memory. + If sync_with_disk has been True over the course of all interesting + changes, this .save() call will be a waste of time. + + The BugDir will only load information from the file system when it + loads new bugs/comments that it doesn't already have in memory, or + when it explicitly asked to do so (e.g. .load() or + __init__(from_disk=True)). + + Allow RCS initialization + ======================== + + This one is for testing purposes. Setting it to True allows the + BugDir to search for an installed RCS backend and initialize it in + the root directory. This is a convenience option for supporting + tests of versioning functionality (e.g. .duplicate_bugdir). + + Disable encoding manipulation + ============================= + + This one is for testing purposed. You might have non-ASCII + Unicode in your bugs, comments, files, etc. BugDir instances try + and support your preferred encoding scheme (e.g. "utf-8") when + dealing with stream and file input/output. For stream output, + this involves replacing sys.stdout and sys.stderr + (libbe.encode.set_IO_stream_encodings). However this messes up + doctest's output catching. In order to support doctest tests + using BugDirs, set manipulate_encodings=False, and stick to ASCII + in your tests. + """ + + settings_properties = [] + required_saved_properties = [] + _prop_save_settings = settings_object.prop_save_settings + _prop_load_settings = settings_object.prop_load_settings + def _versioned_property(settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"]=required_saved_properties + return settings_object.versioned_property(**kwargs) + + @_versioned_property(name="target", + doc="The current project development target.") + def target(): return {} + + def _guess_encoding(self): + return encoding.get_encoding() + def _check_encoding(value): + if value != None: + return encoding.known_encoding(value) + def _setup_encoding(self, new_encoding): + # change hook called before generator. + if new_encoding not in [None, settings_object.EMPTY]: + if self._manipulate_encodings == True: + encoding.set_IO_stream_encodings(new_encoding) + def _set_encoding(self, old_encoding, new_encoding): + self._setup_encoding(new_encoding) + self._prop_save_settings(old_encoding, new_encoding) + + @_versioned_property(name="encoding", + doc="""The default input/output encoding to use (e.g. "utf-8").""", + change_hook=_set_encoding, + generator=_guess_encoding, + check_fn=_check_encoding) + def encoding(): return {} + + def _setup_user_id(self, user_id): + self.rcs.user_id = user_id + def _guess_user_id(self): + return self.rcs.get_user_id() + def _set_user_id(self, old_user_id, new_user_id): + self._setup_user_id(new_user_id) + self._prop_save_settings(old_user_id, new_user_id) + + @_versioned_property(name="user_id", + doc= +"""The user's prefered name, e.g. 'John Doe '. Note +that the Arch RCS backend *enforces* ids with this format.""", + change_hook=_set_user_id, + generator=_guess_user_id) + def user_id(): return {} + + @_versioned_property(name="default_assignee", + doc= +"""The default assignee for new bugs e.g. 'John Doe '.""") + def default_assignee(): return {} + + @_versioned_property(name="rcs_name", + doc="""The name of the current RCS. Kept seperate to make saving/loading +settings easy. Don't set this attribute. Set .rcs instead, and +.rcs_name will be automatically adjusted.""", + default="None", + allowed=["None", "Arch", "bzr", "darcs", "git", "hg"]) + def rcs_name(): return {} + + def _get_rcs(self, rcs_name=None): + """Get and root a new revision control system""" + if rcs_name == None: + rcs_name = self.rcs_name + new_rcs = rcs.rcs_by_name(rcs_name) + self._change_rcs(None, new_rcs) + return new_rcs + def _change_rcs(self, old_rcs, new_rcs): + new_rcs.encoding = self.encoding + new_rcs.root(self.root) + self.rcs_name = new_rcs.name + + @Property + @change_hook_property(hook=_change_rcs) + @cached_property(generator=_get_rcs) + @local_property("rcs") + @doc_property(doc="A revision control system instance.") + def rcs(): return {} + + def _bug_map_gen(self): + map = {} + for bug in self: + map[bug.uuid] = bug + for uuid in self.list_uuids(): + if uuid not in map: + map[uuid] = None + self._bug_map_value = map # ._bug_map_value used by @local_property + + def _extra_strings_check_fn(value): + return utility.iterable_full_of_strings(value, \ + alternative=settings_object.EMPTY) + def _extra_strings_change_hook(self, old, new): + self.extra_strings.sort() # to make merging easier + self._prop_save_settings(old, new) + @_versioned_property(name="extra_strings", + doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/.py.", + default=[], + check_fn=_extra_strings_check_fn, + change_hook=_extra_strings_change_hook, + mutable=True) + def extra_strings(): return {} + + @Property + @primed_property(primer=_bug_map_gen) + @local_property("bug_map") + @doc_property(doc="A dict of (bug-uuid, bug-instance) pairs.") + def _bug_map(): return {} + + def _setup_severities(self, severities): + if severities not in [None, settings_object.EMPTY]: + bug.load_severities(severities) + def _set_severities(self, old_severities, new_severities): + self._setup_severities(new_severities) + self._prop_save_settings(old_severities, new_severities) + @_versioned_property(name="severities", + doc="The allowed bug severities and their descriptions.", + change_hook=_set_severities) + def severities(): return {} + + def _setup_status(self, active_status, inactive_status): + bug.load_status(active_status, inactive_status) + def _set_active_status(self, old_active_status, new_active_status): + self._setup_status(new_active_status, self.inactive_status) + self._prop_save_settings(old_active_status, new_active_status) + @_versioned_property(name="active_status", + doc="The allowed active bug states and their descriptions.", + change_hook=_set_active_status) + def active_status(): return {} + + def _set_inactive_status(self, old_inactive_status, new_inactive_status): + self._setup_status(self.active_status, new_inactive_status) + self._prop_save_settings(old_inactive_status, new_inactive_status) + @_versioned_property(name="inactive_status", + doc="The allowed inactive bug states and their descriptions.", + change_hook=_set_inactive_status) + def inactive_status(): return {} + + + def __init__(self, root=None, sink_to_existing_root=True, + assert_new_BugDir=False, allow_rcs_init=False, + manipulate_encodings=True, + from_disk=False, rcs=None): + list.__init__(self) + settings_object.SavedSettingsObject.__init__(self) + self._manipulate_encodings = manipulate_encodings + if root == None: + root = os.getcwd() + if sink_to_existing_root == True: + self.root = self._find_root(root) + else: + if not os.path.exists(root): + raise NoRootEntry(root) + self.root = root + # get a temporary rcs until we've loaded settings + self.sync_with_disk = False + self.rcs = self._guess_rcs() + + if from_disk == True: + self.sync_with_disk = True + self.load() + else: + self.sync_with_disk = False + if assert_new_BugDir == True: + if os.path.exists(self.get_path()): + raise AlreadyInitialized, self.get_path() + if rcs == None: + rcs = self._guess_rcs(allow_rcs_init) + self.rcs = rcs + self._setup_user_id(self.user_id) + + def set_sync_with_disk(self, value): + self.sync_with_disk = value + for bug in self: + bug.set_sync_with_disk(value) + + def _find_root(self, path): + """ + Search for an existing bug database dir and it's ancestors and + return a BugDir rooted there. + """ + if not os.path.exists(path): + raise NoRootEntry(path) + versionfile=utility.search_parent_directories(path, + os.path.join(".be", "version")) + if versionfile != None: + beroot = os.path.dirname(versionfile) + root = os.path.dirname(beroot) + return root + else: + beroot = utility.search_parent_directories(path, ".be") + if beroot == None: + raise NoBugDir(path) + return beroot + + def get_version(self, path=None, use_none_rcs=False): + if use_none_rcs == True: + RCS = rcs.rcs_by_name("None") + RCS.root(self.root) + RCS.encoding = encoding.get_encoding() + else: + RCS = self.rcs + + if path == None: + path = self.get_path("version") + tree_version = RCS.get_file_contents(path) + return tree_version + + def set_version(self): + self.rcs.mkdir(self.get_path()) + self.rcs.set_file_contents(self.get_path("version"), + TREE_VERSION_STRING) + + def get_path(self, *args): + my_dir = os.path.join(self.root, ".be") + if len(args) == 0: + return my_dir + assert args[0] in ["version", "settings", "bugs"], str(args) + return os.path.join(my_dir, *args) + + def _guess_rcs(self, allow_rcs_init=False): + deepdir = self.get_path() + if not os.path.exists(deepdir): + deepdir = os.path.dirname(deepdir) + new_rcs = rcs.detect_rcs(deepdir) + install = False + if new_rcs.name == "None": + if allow_rcs_init == True: + new_rcs = rcs.installed_rcs() + new_rcs.init(self.root) + return new_rcs + + def load(self): + version = self.get_version(use_none_rcs=True) + if version != TREE_VERSION_STRING: + raise NotImplementedError, \ + "BugDir cannot handle version '%s' yet." % version + else: + if not os.path.exists(self.get_path()): + raise NoBugDir(self.get_path()) + self.load_settings() + + self.rcs = rcs.rcs_by_name(self.rcs_name) + self._setup_user_id(self.user_id) + + def load_all_bugs(self): + "Warning: this could take a while." + self._clear_bugs() + for uuid in self.list_uuids(): + self._load_bug(uuid) + + def save(self): + """ + Save any loaded contents to disk. Because of lazy loading of + bugs and comments, this is actually not too inefficient. + + However, if self.sync_with_disk = True, then any changes are + automatically written to disk as soon as they happen, so + calling this method will just waste time (unless something + else has been messing with your on-disk files). + """ + self.set_version() + self.save_settings() + for bug in self: + bug.save() + + def load_settings(self): + self.settings = self._get_settings(self.get_path("settings")) + self._setup_saved_settings() + self._setup_user_id(self.user_id) + self._setup_encoding(self.encoding) + self._setup_severities(self.severities) + self._setup_status(self.active_status, self.inactive_status) + + def _get_settings(self, settings_path): + allow_no_rcs = not self.rcs.path_in_root(settings_path) + # allow_no_rcs=True should only be for the special case of + # configuring duplicate bugdir settings + + try: + settings = mapfile.map_load(self.rcs, settings_path, allow_no_rcs) + except rcs.NoSuchFile: + settings = {"rcs_name": "None"} + return settings + + def save_settings(self): + settings = self._get_saved_settings() + self._save_settings(self.get_path("settings"), settings) + + def _save_settings(self, settings_path, settings): + allow_no_rcs = not self.rcs.path_in_root(settings_path) + # allow_no_rcs=True should only be for the special case of + # configuring duplicate bugdir settings + self.rcs.mkdir(self.get_path(), allow_no_rcs) + mapfile.map_save(self.rcs, settings_path, settings, allow_no_rcs) + + def duplicate_bugdir(self, revision): + duplicate_path = self.rcs.duplicate_repo(revision) + + # setup revision RCS as None, since the duplicate may not be + # initialized for versioning + duplicate_settings_path = os.path.join(duplicate_path, + ".be", "settings") + duplicate_settings = self._get_settings(duplicate_settings_path) + if "rcs_name" in duplicate_settings: + duplicate_settings["rcs_name"] = "None" + duplicate_settings["user_id"] = self.user_id + if "disabled" in bug.status_values: + # Hack to support old versions of BE bugs + duplicate_settings["inactive_status"] = self.inactive_status + self._save_settings(duplicate_settings_path, duplicate_settings) + + return BugDir(duplicate_path, from_disk=True, manipulate_encodings=self._manipulate_encodings) + + def remove_duplicate_bugdir(self): + self.rcs.remove_duplicate_repo() + + def list_uuids(self): + uuids = [] + if os.path.exists(self.get_path()): + # list the uuids on disk + for uuid in os.listdir(self.get_path("bugs")): + if not (uuid.startswith('.')): + uuids.append(uuid) + yield uuid + # and the ones that are still just in memory + for bug in self: + if bug.uuid not in uuids: + uuids.append(bug.uuid) + yield bug.uuid + + def _clear_bugs(self): + while len(self) > 0: + self.pop() + self._bug_map_gen() + + def _load_bug(self, uuid): + bg = bug.Bug(bugdir=self, uuid=uuid, from_disk=True) + self.append(bg) + self._bug_map_gen() + return bg + + def new_bug(self, uuid=None, summary=None): + bg = bug.Bug(bugdir=self, uuid=uuid, summary=summary) + bg.set_sync_with_disk(self.sync_with_disk) + if bg.sync_with_disk == True: + bg.save() + self.append(bg) + self._bug_map_gen() + return bg + + def remove_bug(self, bug): + self.remove(bug) + bug.remove() + + def bug_shortname(self, bug): + """ + Generate short names from uuids. Picks the minimum number of + characters (>=3) from the beginning of the uuid such that the + short names are unique. + + Obviously, as the number of bugs in the database grows, these + short names will cease to be unique. The complete uuid should be + used for long term reference. + """ + chars = 3 + for uuid in self._bug_map.keys(): + if bug.uuid == uuid: + continue + while (bug.uuid[:chars] == uuid[:chars]): + chars+=1 + return bug.uuid[:chars] + + def bug_from_shortname(self, shortname): + """ + >>> bd = simple_bug_dir() + >>> bug_a = bd.bug_from_shortname('a') + >>> print type(bug_a) + + >>> print bug_a + a:om: Bug A + """ + matches = [] + self._bug_map_gen() + for uuid in self._bug_map.keys(): + if uuid.startswith(shortname): + matches.append(uuid) + if len(matches) > 1: + raise MultipleBugMatches(shortname, matches) + if len(matches) == 1: + return self.bug_from_uuid(matches[0]) + raise KeyError("No bug matches %s" % shortname) + + def bug_from_uuid(self, uuid): + if not self.has_bug(uuid): + raise KeyError("No bug matches %s\n bug map: %s\n root: %s" \ + % (uuid, self._bug_map, self.root)) + if self._bug_map[uuid] == None: + self._load_bug(uuid) + return self._bug_map[uuid] + + def has_bug(self, bug_uuid): + if bug_uuid not in self._bug_map: + self._bug_map_gen() + if bug_uuid not in self._bug_map: + return False + return True + + +def simple_bug_dir(): + """ + For testing + >>> bugdir = simple_bug_dir() + >>> ls = list(bugdir.list_uuids()) + >>> ls.sort() + >>> print ls + ['a', 'b'] + """ + dir = utility.Dir() + assert os.path.exists(dir.path) + bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True, + manipulate_encodings=False) + bugdir._dir_ref = dir # postpone cleanup since dir.__del__() removes dir. + bug_a = bugdir.new_bug("a", summary="Bug A") + bug_a.creator = "John Doe " + bug_a.time = 0 + bug_b = bugdir.new_bug("b", summary="Bug B") + bug_b.creator = "Jane Doe " + bug_b.time = 0 + bug_b.status = "closed" + bugdir.save() + return bugdir + + +class BugDirTestCase(unittest.TestCase): + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + def setUp(self): + self.dir = utility.Dir() + self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, + allow_rcs_init=True) + self.rcs = self.bugdir.rcs + def tearDown(self): + self.rcs.cleanup() + self.dir.cleanup() + def fullPath(self, path): + return os.path.join(self.dir.path, path) + def assertPathExists(self, path): + fullpath = self.fullPath(path) + self.failUnless(os.path.exists(fullpath)==True, + "path %s does not exist" % fullpath) + self.assertRaises(AlreadyInitialized, BugDir, + self.dir.path, assertNewBugDir=True) + def versionTest(self): + if self.rcs.versioned == False: + return + original = self.bugdir.rcs.commit("Began versioning") + bugA = self.bugdir.bug_from_uuid("a") + bugA.status = "fixed" + self.bugdir.save() + new = self.rcs.commit("Fixed bug a") + dupdir = self.bugdir.duplicate_bugdir(original) + self.failUnless(dupdir.root != self.bugdir.root, + "%s, %s" % (dupdir.root, self.bugdir.root)) + bugAorig = dupdir.bug_from_uuid("a") + self.failUnless(bugA != bugAorig, + "\n%s\n%s" % (bugA.string(), bugAorig.string())) + bugAorig.status = "fixed" + self.failUnless(bug.cmp_status(bugA, bugAorig)==0, + "%s, %s" % (bugA.status, bugAorig.status)) + self.failUnless(bug.cmp_severity(bugA, bugAorig)==0, + "%s, %s" % (bugA.severity, bugAorig.severity)) + self.failUnless(bug.cmp_assigned(bugA, bugAorig)==0, + "%s, %s" % (bugA.assigned, bugAorig.assigned)) + self.failUnless(bug.cmp_time(bugA, bugAorig)==0, + "%s, %s" % (bugA.time, bugAorig.time)) + self.failUnless(bug.cmp_creator(bugA, bugAorig)==0, + "%s, %s" % (bugA.creator, bugAorig.creator)) + self.failUnless(bugA == bugAorig, + "\n%s\n%s" % (bugA.string(), bugAorig.string())) + self.bugdir.remove_duplicate_bugdir() + self.failUnless(os.path.exists(dupdir.root)==False, str(dupdir.root)) + def testRun(self): + self.bugdir.new_bug(uuid="a", summary="Ant") + self.bugdir.new_bug(uuid="b", summary="Cockroach") + self.bugdir.new_bug(uuid="c", summary="Praying mantis") + length = len(self.bugdir) + self.failUnless(length == 3, "%d != 3 bugs" % length) + uuids = list(self.bugdir.list_uuids()) + self.failUnless(len(uuids) == 3, "%d != 3 uuids" % len(uuids)) + self.failUnless(uuids == ["a","b","c"], str(uuids)) + bugA = self.bugdir.bug_from_uuid("a") + bugAprime = self.bugdir.bug_from_shortname("a") + self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime)) + self.bugdir.save() + self.versionTest() + def testComments(self, sync_with_disk=False): + if sync_with_disk == True: + self.bugdir.set_sync_with_disk(True) + self.bugdir.new_bug(uuid="a", summary="Ant") + bug = self.bugdir.bug_from_uuid("a") + comm = bug.comment_root + rep = comm.new_reply("Ants are small.") + rep.new_reply("And they have six legs.") + if sync_with_disk == False: + self.bugdir.save() + self.bugdir._clear_bugs() + bug = self.bugdir.bug_from_uuid("a") + bug.load_comments() + self.failUnless(len(bug.comment_root)==1, len(bug.comment_root)) + for index,comment in enumerate(bug.comments()): + if index == 0: + repLoaded = comment + self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid) + self.failUnless(comment.sync_with_disk == True, + comment.sync_with_disk) + #load_settings() + self.failUnless(comment.content_type == "text/plain", + comment.content_type) + self.failUnless(repLoaded.settings["Content-type"]=="text/plain", + repLoaded.settings) + self.failUnless(repLoaded.body == "Ants are small.", + repLoaded.body) + elif index == 1: + self.failUnless(comment.in_reply_to == repLoaded.uuid, + repLoaded.uuid) + self.failUnless(comment.body == "And they have six legs.", + comment.body) + else: + self.failIf(True, "Invalid comment: %d\n%s" % (index, comment)) + def testSyncedComments(self): + self.testComments(sync_with_disk=True) + +unitsuite = unittest.TestLoader().loadTestsFromTestCase(BugDirTestCase) +suite = unittest.TestSuite([unitsuite])#, doctest.DocTestSuite()]) diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/bzr.py b/interfaces/web/Bugs-Everywhere-Web/libbe/bzr.py new file mode 100644 index 0000000..d7cd1e5 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/bzr.py @@ -0,0 +1,101 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Ben Finney +# Marien Zwart +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os +import re +import sys +import unittest +import doctest + +import rcs +from rcs import RCS + +def new(): + return Bzr() + +class Bzr(RCS): + name = "bzr" + client = "bzr" + versioned = True + def _rcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _rcs_detect(self, path): + if self._u_search_parent_directories(path, ".bzr") != None : + return True + return False + def _rcs_root(self, path): + """Find the root of the deepest repository containing path.""" + status,output,error = self._u_invoke_client("root", path) + return output.rstrip('\n') + def _rcs_init(self, path): + self._u_invoke_client("init", directory=path) + def _rcs_get_user_id(self): + status,output,error = self._u_invoke_client("whoami") + return output.rstrip('\n') + def _rcs_set_user_id(self, value): + self._u_invoke_client("whoami", value) + def _rcs_add(self, path): + self._u_invoke_client("add", path) + def _rcs_remove(self, path): + # --force to also remove unversioned files. + self._u_invoke_client("remove", "--force", path) + def _rcs_update(self, path): + pass + def _rcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + else: + status,output,error = \ + self._u_invoke_client("cat","-r",revision,path) + return output + def _rcs_duplicate_repo(self, directory, revision=None): + if revision == None: + RCS._rcs_duplicate_repo(self, directory, revision) + else: + self._u_invoke_client("branch", "--revision", revision, + ".", directory) + def _rcs_commit(self, commitfile, allow_empty=False): + args = ["commit", "--file", commitfile] + if allow_empty == True: + args.append("--unchanged") + status,output,error = self._u_invoke_client(*args) + else: + kwargs = {"expect":(0,3)} + status,output,error = self._u_invoke_client(*args, **kwargs) + if status != 0: + strings = ["ERROR: no changes to commit.", # bzr 1.3.1 + "ERROR: No changes to commit."] # bzr 1.15.1 + if self._u_any_in_string(strings, error) == True: + raise rcs.EmptyCommit() + else: + raise rcs.CommandError(args, status, error) + revision = None + revline = re.compile("Committed revision (.*)[.]") + match = revline.search(error) + assert match != None, output+error + assert len(match.groups()) == 1 + revision = match.groups()[0] + return revision + + +rcs.make_rcs_testcase_subclasses(Bzr, sys.modules[__name__]) + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/cmdutil.py b/interfaces/web/Bugs-Everywhere-Web/libbe/cmdutil.py new file mode 100644 index 0000000..853a75a --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/cmdutil.py @@ -0,0 +1,218 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Oleg Romanyshyn +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import glob +import optparse +import os +from textwrap import TextWrapper +from StringIO import StringIO +import sys +import doctest + +import bugdir +import plugin +import encoding + + +class UserError(Exception): + def __init__(self, msg): + Exception.__init__(self, msg) + +class UnknownCommand(UserError): + def __init__(self, cmd): + Exception.__init__(self, "Unknown command '%s'" % cmd) + self.cmd = cmd + +class UsageError(Exception): + pass + +class GetHelp(Exception): + pass + +class GetCompletions(Exception): + def __init__(self, completions=[]): + msg = "Get allowed completions" + Exception.__init__(self, msg) + self.completions = completions + +def iter_commands(): + for name, module in plugin.iter_plugins("becommands"): + yield name.replace("_", "-"), module + +def get_command(command_name): + """Retrieves the module for a user command + + >>> try: + ... get_command("asdf") + ... except UnknownCommand, e: + ... print e + Unknown command 'asdf' + >>> repr(get_command("list")).startswith(" 0: + max_pos_arg = max(bugid_args.keys()) + else: + max_pos_arg = -1 + for pos,value in enumerate(args): + if value == "--complete": + filter = None + if pos in bugid_args: + filter = bugid_args[pos] + if pos > max_pos_arg and -1 in bugid_args: + filter = bugid_args[-1] + if filter != None: + bugshortnames = [] + try: + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + bd.load_all_bugs() + bugs = [bug for bug in bd if filter(bug) == True] + bugshortnames = [bd.bug_shortname(bug) for bug in bugs] + except bugdir.NoBugDir: + pass + raise GetCompletions(bugshortnames) + raise GetCompletions() + +def complete_path(path): + """List possible path completions for path.""" + comps = glob.glob(path+"*") + glob.glob(path+"/*") + if len(comps) == 1 and os.path.isdir(comps[0]): + comps.extend(glob.glob(comps[0]+"/*")) + return comps + +def underlined(instring): + """Produces a version of a string that is underlined with '=' + + >>> underlined("Underlined String") + 'Underlined String\\n=================' + """ + + return "%s\n%s" % (instring, "="*len(instring)) + + +def _test(): + import doctest + import sys + doctest.testmod() + +if __name__ == "__main__": + _test() + +suite = doctest.DocTestSuite() diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/comment.py b/interfaces/web/Bugs-Everywhere-Web/libbe/comment.py new file mode 100644 index 0000000..3249e8b --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/comment.py @@ -0,0 +1,662 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2008-2009 Chris Ball +# Thomas Habets +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import base64 +import os +import os.path +import sys +import time +import types +try: # import core module, Python >= 2.5 + from xml.etree import ElementTree +except ImportError: # look for non-core module + from elementtree import ElementTree +import xml.sax.saxutils +import doctest + +from beuuid import uuid_gen +from properties import Property, doc_property, local_property, \ + defaulting_property, checked_property, cached_property, \ + primed_property, change_hook_property, settings_property +import settings_object +import mapfile +from tree import Tree +import utility + + +class InvalidShortname(KeyError): + def __init__(self, shortname, shortnames): + msg = "Invalid shortname %s\n%s" % (shortname, shortnames) + KeyError.__init__(self, msg) + self.shortname = shortname + self.shortnames = shortnames + +class InvalidXML(ValueError): + def __init__(self, element, comment): + msg = "Invalid comment xml: %s\n %s\n" \ + % (comment, ElementTree.tostring(element)) + ValueError.__init__(self, msg) + self.element = element + self.comment = comment + +class MissingReference(ValueError): + def __init__(self, comment): + msg = "Missing reference to %s" % (comment.in_reply_to) + ValueError.__init__(self, msg) + self.reference = comment.in_reply_to + self.comment = comment + +INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!" + +def list_to_root(comments, bug, root=None, + ignore_missing_references=False): + """ + Convert a raw list of comments to single root comment. We use a + dummy root comment by default, because there can be several + comment threads rooted on the same parent bug. To simplify + comment interaction, we condense these threads into a single + thread with a Comment dummy root. Can also be used to append + a list of subcomments to a non-dummy root comment, so long as + all the new comments are descendants of the root comment. + + No Comment method should use the dummy comment. + """ + root_comments = [] + uuid_map = {} + for comment in comments: + assert comment.uuid != None + uuid_map[comment.uuid] = comment + for comment in comments: + if comment.alt_id != None and comment.alt_id not in uuid_map: + uuid_map[comment.alt_id] = comment + if root == None: + root = Comment(bug, uuid=INVALID_UUID) + else: + uuid_map[root.uuid] = root + for comm in comments: + if comm.in_reply_to == INVALID_UUID: + comm.in_reply_to = None + rep = comm.in_reply_to + if rep == None or rep == bug.uuid: + root_comments.append(comm) + else: + parentUUID = comm.in_reply_to + try: + parent = uuid_map[parentUUID] + parent.add_reply(comm) + except KeyError, e: + if ignore_missing_references == True: + print >> sys.stderr, \ + "Ignoring missing reference to %s" % parentUUID + comm.in_reply_to = None + root_comments.append(comm) + else: + raise MissingReference(comm) + root.extend(root_comments) + return root + +def loadComments(bug, load_full=False): + """ + Set load_full=True when you want to load the comment completely + from disk *now*, rather than waiting and lazy loading as required. + """ + path = bug.get_path("comments") + if not os.path.isdir(path): + return Comment(bug, uuid=INVALID_UUID) + comments = [] + for uuid in os.listdir(path): + if uuid.startswith('.'): + continue + comm = Comment(bug, uuid, from_disk=True) + comm.set_sync_with_disk(bug.sync_with_disk) + if load_full == True: + comm.load_settings() + dummy = comm.body # force the body to load + comments.append(comm) + return list_to_root(comments, bug) + +def saveComments(bug): + for comment in bug.comment_root.traverse(): + comment.save() + + +class Comment(Tree, settings_object.SavedSettingsObject): + """ + >>> c = Comment() + >>> c.uuid != None + True + >>> c.uuid = "some-UUID" + >>> print c.content_type + text/plain + """ + + settings_properties = [] + required_saved_properties = [] + _prop_save_settings = settings_object.prop_save_settings + _prop_load_settings = settings_object.prop_load_settings + def _versioned_property(settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"]=required_saved_properties + return settings_object.versioned_property(**kwargs) + + @_versioned_property(name="Alt-id", + doc="Alternate ID for linking imported comments. Internally comments are linked (via In-reply-to) to the parent's UUID. However, these UUIDs are generated internally, so Alt-id is provided as a user-controlled linking target.") + def alt_id(): return {} + + @_versioned_property(name="From", + doc="The author of the comment") + def From(): return {} + + @_versioned_property(name="In-reply-to", + doc="UUID for parent comment or bug") + def in_reply_to(): return {} + + @_versioned_property(name="Content-type", + doc="Mime type for comment body", + default="text/plain", + require_save=True) + def content_type(): return {} + + @_versioned_property(name="Date", + doc="An RFC 2822 timestamp for comment creation") + def time_string(): return {} + + def _get_time(self): + if self.time_string == None: + return None + return utility.str_to_time(self.time_string) + def _set_time(self, value): + self.time_string = utility.time_to_str(value) + time = property(fget=_get_time, + fset=_set_time, + doc="An integer version of .time_string") + + def _get_comment_body(self): + if self.rcs != None and self.sync_with_disk == True: + import rcs + binary = not self.content_type.startswith("text/") + return self.rcs.get_file_contents(self.get_path("body"), binary=binary) + def _set_comment_body(self, old=None, new=None, force=False): + if (self.rcs != None and self.sync_with_disk == True) or force==True: + assert new != None, "Can't save empty comment" + binary = not self.content_type.startswith("text/") + self.rcs.set_file_contents(self.get_path("body"), new, binary=binary) + + @Property + @change_hook_property(hook=_set_comment_body) + @cached_property(generator=_get_comment_body) + @local_property("body") + @doc_property(doc="The meat of the comment") + def body(): return {} + + def _get_rcs(self): + if hasattr(self.bug, "rcs"): + return self.bug.rcs + + @Property + @cached_property(generator=_get_rcs) + @local_property("rcs") + @doc_property(doc="A revision control system instance.") + def rcs(): return {} + + def _extra_strings_check_fn(value): + return utility.iterable_full_of_strings(value, \ + alternative=settings_object.EMPTY) + def _extra_strings_change_hook(self, old, new): + self.extra_strings.sort() # to make merging easier + self._prop_save_settings(old, new) + @_versioned_property(name="extra_strings", + doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/.py.", + default=[], + check_fn=_extra_strings_check_fn, + change_hook=_extra_strings_change_hook, + mutable=True) + def extra_strings(): return {} + + def __init__(self, bug=None, uuid=None, from_disk=False, + in_reply_to=None, body=None): + """ + Set from_disk=True to load an old comment. + Set from_disk=False to create a new comment. + + The uuid option is required when from_disk==True. + + The in_reply_to and body options are only used if + from_disk==False (the default). When from_disk==True, they are + loaded from the bug database. + + in_reply_to should be the uuid string of the parent comment. + """ + Tree.__init__(self) + settings_object.SavedSettingsObject.__init__(self) + self.bug = bug + self.uuid = uuid + if from_disk == True: + self.sync_with_disk = True + else: + self.sync_with_disk = False + if uuid == None: + self.uuid = uuid_gen() + self.time = int(time.time()) # only save to second precision + if self.rcs != None: + self.From = self.rcs.get_user_id() + self.in_reply_to = in_reply_to + self.body = body + + def set_sync_with_disk(self, value): + self.sync_with_disk = True + + def traverse(self, *args, **kwargs): + """Avoid working with the possible dummy root comment""" + for comment in Tree.traverse(self, *args, **kwargs): + if comment.uuid == INVALID_UUID: + continue + yield comment + + def _setting_attr_string(self, setting): + value = getattr(self, setting) + if value == None: + return "" + return str(value) + + def xml(self, indent=0, shortname=None): + """ + >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") + >>> comm.uuid = "0123" + >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> print comm.xml(indent=2, shortname="com-1") + + 0123 + com-1 + + Thu, 01 Jan 1970 00:00:00 +0000 + text/plain + Some + insightful + remarks + + """ + if shortname == None: + shortname = self.uuid + if self.content_type.startswith("text/"): + body = (self.body or "").rstrip('\n') + else: + maintype,subtype = self.content_type.split('/',1) + msg = email.mime.base.MIMEBase(maintype, subtype) + msg.set_payload(self.body or "") + email.encoders.encode_base64(msg) + body = base64.encodestring(self.body or "") + info = [("uuid", self.uuid), + ("alt-id", self.alt_id), + ("short-name", shortname), + ("in-reply-to", self.in_reply_to), + ("from", self._setting_attr_string("From")), + ("date", self.time_string), + ("content-type", self.content_type), + ("body", body)] + lines = [""] + for (k,v) in info: + if v != None: + lines.append(' <%s>%s' % (k,xml.sax.saxutils.escape(v),k)) + lines.append("") + istring = ' '*indent + sep = '\n' + istring + return istring + sep.join(lines).rstrip('\n') + + def from_xml(self, xml_string, verbose=True): + """ + Note: If alt-id is not given, translates any fields to + fields. + >>> commA = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") + >>> commA.uuid = "0123" + >>> commA.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> xml = commA.xml(shortname="com-1") + >>> commB = Comment() + >>> commB.from_xml(xml) + >>> attrs=['uuid','alt_id','in_reply_to','From','time_string','content_type','body'] + >>> for attr in attrs: # doctest: +ELLIPSIS + ... if getattr(commB, attr) != getattr(commA, attr): + ... estr = "Mismatch on %s: '%s' should be '%s'" + ... args = (attr, getattr(commB, attr), getattr(commA, attr)) + ... print estr % args + Mismatch on uuid: '...' should be '0123' + Mismatch on alt_id: '0123' should be 'None' + >>> print commB.alt_id + 0123 + >>> commA.From + >>> commB.From + """ + if type(xml_string) == types.UnicodeType: + xml_string = xml_string.strip().encode("unicode_escape") + comment = ElementTree.XML(xml_string) + if comment.tag != "comment": + raise InvalidXML(comment, "root element must be ") + tags=['uuid','alt-id','in-reply-to','from','date','content-type','body'] + uuid = None + body = None + for child in comment.getchildren(): + if child.tag == "short-name": + pass + elif child.tag in tags: + if child.text == None or len(child.text) == 0: + text = settings_object.EMPTY + else: + text = xml.sax.saxutils.unescape(child.text) + text = unicode(text).decode("unicode_escape").strip() + if child.tag == "uuid": + uuid = text + continue # don't set the bug's uuid tag. + if child.tag == "body": + body = text + continue # don't set the bug's body yet. + elif child.tag == 'from': + attr_name = "From" + elif child.tag == 'date': + attr_name = 'time_string' + else: + attr_name = child.tag.replace('-','_') + setattr(self, attr_name, text) + elif verbose == True: + print >> sys.stderr, "Ignoring unknown tag %s in %s" \ + % (child.tag, comment.tag) + if self.alt_id == None and uuid not in [None, self.uuid]: + self.alt_id = uuid + if body != None: + if self.content_type.startswith("text/"): + self.body = body+"\n" # restore trailing newline + else: + self.body = base64.decodestring(body) + + def string(self, indent=0, shortname=None): + """ + >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") + >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> print comm.string(indent=2, shortname="com-1") + --------- Comment --------- + Name: com-1 + From: + Date: Thu, 01 Jan 1970 00:00:00 +0000 + + Some + insightful + remarks + """ + if shortname == None: + shortname = self.uuid + lines = [] + lines.append("--------- Comment ---------") + lines.append("Name: %s" % shortname) + lines.append("From: %s" % (self._setting_attr_string("From"))) + lines.append("Date: %s" % self.time_string) + lines.append("") + if self.content_type.startswith("text/"): + lines.extend((self.body or "").splitlines()) + else: + lines.append("Content type %s not printable. Try XML output instead" % self.content_type) + + istring = ' '*indent + sep = '\n' + istring + return istring + sep.join(lines).rstrip('\n') + + def __str__(self): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> comm.uuid = "com-1" + >>> comm.time_string = "Thu, 20 Nov 2008 15:55:11 +0000" + >>> comm.From = "Jane Doe " + >>> print comm + --------- Comment --------- + Name: com-1 + From: Jane Doe + Date: Thu, 20 Nov 2008 15:55:11 +0000 + + Some insightful remarks + """ + return self.string() + + def get_path(self, name=None): + my_dir = os.path.join(self.bug.get_path("comments"), self.uuid) + if name is None: + return my_dir + assert name in ["values", "body"] + return os.path.join(my_dir, name) + + def load_settings(self): + self.settings = mapfile.map_load(self.rcs, self.get_path("values")) + self._setup_saved_settings() + + def save_settings(self): + self.rcs.mkdir(self.get_path()) + path = self.get_path("values") + mapfile.map_save(self.rcs, path, self._get_saved_settings()) + + def save(self): + """ + Save any loaded contents to disk. + + However, if self.sync_with_disk = True, then any changes are + automatically written to disk as soon as they happen, so + calling this method will just waste time (unless something + else has been messing with your on-disk files). + """ + assert self.body != None, "Can't save blank comment" + self.save_settings() + self._set_comment_body(new=self.body, force=True) + + def remove(self): + for comment in self.traverse(): + path = comment.get_path() + self.rcs.recursive_remove(path) + + def add_reply(self, reply, allow_time_inversion=False): + if self.uuid != INVALID_UUID: + reply.in_reply_to = self.uuid + self.append(reply) + #raise Exception, "adding reply \n%s\n%s" % (self, reply) + + def new_reply(self, body=None): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> repA = comm.new_reply("Critique original comment") + >>> repB = repA.new_reply("Begin flamewar :p") + >>> repB.in_reply_to == repA.uuid + True + """ + reply = Comment(self.bug, body=body) + if self.bug != None: + reply.set_sync_with_disk(self.bug.sync_with_disk) + if reply.sync_with_disk == True: + reply.save() + self.add_reply(reply) + return reply + + def string_thread(self, string_method_name="string", name_map={}, + indent=0, flatten=True, + auto_name_map=False, bug_shortname=None): + """ + Return a string displaying a thread of comments. + bug_shortname is only used if auto_name_map == True. + + string_method_name (defaults to "string") is the name of the + Comment method used to generate the output string for each + Comment in the thread. The method must take the arguments + indent and shortname. + + SIDE-EFFECT: if auto_name_map==True, calls comment_shortnames() + which will sort the tree by comment.time. Avoid by calling + name_map = {} + for shortname,comment in comm.comment_shortnames(bug_shortname): + name_map[comment.uuid] = shortname + comm.sort(key=lambda c : c.From) # your sort + comm.string_thread(name_map=name_map) + + >>> a = Comment(bug=None, uuid="a", body="Insightful remarks") + >>> a.time = utility.str_to_time("Thu, 20 Nov 2008 01:00:00 +0000") + >>> b = a.new_reply("Critique original comment") + >>> b.uuid = "b" + >>> b.time = utility.str_to_time("Thu, 20 Nov 2008 02:00:00 +0000") + >>> c = b.new_reply("Begin flamewar :p") + >>> c.uuid = "c" + >>> c.time = utility.str_to_time("Thu, 20 Nov 2008 03:00:00 +0000") + >>> d = a.new_reply("Useful examples") + >>> d.uuid = "d" + >>> d.time = utility.str_to_time("Thu, 20 Nov 2008 04:00:00 +0000") + >>> a.sort(key=lambda comm : comm.time) + >>> print a.string_thread(flatten=True) + --------- Comment --------- + Name: a + From: + Date: Thu, 20 Nov 2008 01:00:00 +0000 + + Insightful remarks + --------- Comment --------- + Name: b + From: + Date: Thu, 20 Nov 2008 02:00:00 +0000 + + Critique original comment + --------- Comment --------- + Name: c + From: + Date: Thu, 20 Nov 2008 03:00:00 +0000 + + Begin flamewar :p + --------- Comment --------- + Name: d + From: + Date: Thu, 20 Nov 2008 04:00:00 +0000 + + Useful examples + >>> print a.string_thread(auto_name_map=True, bug_shortname="bug-1") + --------- Comment --------- + Name: bug-1:1 + From: + Date: Thu, 20 Nov 2008 01:00:00 +0000 + + Insightful remarks + --------- Comment --------- + Name: bug-1:2 + From: + Date: Thu, 20 Nov 2008 02:00:00 +0000 + + Critique original comment + --------- Comment --------- + Name: bug-1:3 + From: + Date: Thu, 20 Nov 2008 03:00:00 +0000 + + Begin flamewar :p + --------- Comment --------- + Name: bug-1:4 + From: + Date: Thu, 20 Nov 2008 04:00:00 +0000 + + Useful examples + """ + if auto_name_map == True: + name_map = {} + for shortname,comment in self.comment_shortnames(bug_shortname): + name_map[comment.uuid] = shortname + stringlist = [] + for depth,comment in self.thread(flatten=flatten): + ind = 2*depth+indent + if comment.uuid in name_map: + sname = name_map[comment.uuid] + else: + sname = None + string_fn = getattr(comment, string_method_name) + stringlist.append(string_fn(indent=ind, shortname=sname)) + return '\n'.join(stringlist) + + def xml_thread(self, name_map={}, indent=0, + auto_name_map=False, bug_shortname=None): + return self.string_thread(string_method_name="xml", name_map=name_map, + indent=indent, auto_name_map=auto_name_map, + bug_shortname=bug_shortname) + + def comment_shortnames(self, bug_shortname=None): + """ + Iterate through (id, comment) pairs, in time order. + (This is a user-friendly id, not the comment uuid). + + SIDE-EFFECT : will sort the comment tree by comment.time + + >>> a = Comment(bug=None, uuid="a") + >>> b = a.new_reply() + >>> b.uuid = "b" + >>> c = b.new_reply() + >>> c.uuid = "c" + >>> d = a.new_reply() + >>> d.uuid = "d" + >>> for id,name in a.comment_shortnames("bug-1"): + ... print id, name.uuid + bug-1:1 a + bug-1:2 b + bug-1:3 c + bug-1:4 d + """ + if bug_shortname == None: + bug_shortname = "" + self.sort(key=lambda comm : comm.time) + for num,comment in enumerate(self.traverse()): + yield ("%s:%d" % (bug_shortname, num+1), comment) + + def comment_from_shortname(self, comment_shortname, *args, **kwargs): + """ + Use a comment shortname to look up a comment. + >>> a = Comment(bug=None, uuid="a") + >>> b = a.new_reply() + >>> b.uuid = "b" + >>> c = b.new_reply() + >>> c.uuid = "c" + >>> d = a.new_reply() + >>> d.uuid = "d" + >>> comm = a.comment_from_shortname("bug-1:3", bug_shortname="bug-1") + >>> id(comm) == id(c) + True + """ + for cur_name, comment in self.comment_shortnames(*args, **kwargs): + if comment_shortname == cur_name: + return comment + raise InvalidShortname(comment_shortname, + list(self.comment_shortnames(*args, **kwargs))) + + def comment_from_uuid(self, uuid): + """ + Use a comment shortname to look up a comment. + >>> a = Comment(bug=None, uuid="a") + >>> b = a.new_reply() + >>> b.uuid = "b" + >>> c = b.new_reply() + >>> c.uuid = "c" + >>> d = a.new_reply() + >>> d.uuid = "d" + >>> comm = a.comment_from_uuid("d") + >>> id(comm) == id(d) + True + """ + for comment in self.traverse(): + if comment.uuid == uuid: + return comment + raise KeyError(uuid) + +suite = doctest.DocTestSuite() diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/config.py b/interfaces/web/Bugs-Everywhere-Web/libbe/config.py new file mode 100644 index 0000000..5e343b9 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/config.py @@ -0,0 +1,83 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import ConfigParser +import codecs +import locale +import os.path +import sys +import doctest + +default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() + +def path(): + """Return the path to the per-user config file""" + return os.path.expanduser("~/.bugs_everywhere") + +def set_val(name, value, section="DEFAULT", encoding=None): + """Set a value in the per-user config file + + :param name: The name of the value to set + :param value: The new value to set (or None to delete the value) + :param section: The section to store the name/value in + """ + if encoding == None: + encoding = default_encoding + config = ConfigParser.ConfigParser() + if os.path.exists(path()) == False: # touch file or config + open(path(), "w").close() # read chokes on missing file + f = codecs.open(path(), "r", encoding) + config.readfp(f, path()) + f.close() + if value is not None: + config.set(section, name, value) + else: + config.remove_option(section, name) + f = codecs.open(path(), "w", encoding) + config.write(f) + f.close() + +def get_val(name, section="DEFAULT", default=None, encoding=None): + """ + Get a value from the per-user config file + + :param name: The name of the value to get + :section: The section that the name is in + :return: The value, or None + >>> get_val("junk") is None + True + >>> set_val("junk", "random") + >>> get_val("junk") + u'random' + >>> set_val("junk", None) + >>> get_val("junk") is None + True + """ + if os.path.exists(path()): + if encoding == None: + encoding = default_encoding + config = ConfigParser.ConfigParser() + f = codecs.open(path(), "r", encoding) + config.readfp(f, path()) + f.close() + try: + return config.get(section, name) + except ConfigParser.NoOptionError: + return default + else: + return default + +suite = doctest.DocTestSuite() diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/darcs.py b/interfaces/web/Bugs-Everywhere-Web/libbe/darcs.py new file mode 100644 index 0000000..e7132c0 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/darcs.py @@ -0,0 +1,163 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import codecs +import os +import re +import sys +import unittest +import doctest + +import rcs +from rcs import RCS + +def new(): + return Darcs() + +class Darcs(RCS): + name="darcs" + client="darcs" + versioned=True + def _rcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _rcs_detect(self, path): + if self._u_search_parent_directories(path, "_darcs") != None : + return True + return False + def _rcs_root(self, path): + """Find the root of the deepest repository containing path.""" + # Assume that nothing funny is going on; in particular, that we aren't + # dealing with a bare repo. + if os.path.isdir(path) != True: + path = os.path.dirname(path) + darcs_dir = self._u_search_parent_directories(path, "_darcs") + if darcs_dir == None: + return None + return os.path.dirname(darcs_dir) + def _rcs_init(self, path): + self._u_invoke_client("init", directory=path) + def _rcs_get_user_id(self): + # following http://darcs.net/manual/node4.html#SECTION00410030000000000000 + # as of June 29th, 2009 + if self.rootdir == None: + return None + darcs_dir = os.path.join(self.rootdir, "_darcs") + if darcs_dir != None: + for pref_file in ["author", "email"]: + pref_path = os.path.join(darcs_dir, "prefs", pref_file) + if os.path.exists(pref_path): + return self.get_file_contents(pref_path) + for env_variable in ["DARCS_EMAIL", "EMAIL"]: + if env_variable in os.environ: + return os.environ[env_variable] + return None + def _rcs_set_user_id(self, value): + if self.rootdir == None: + self.root(".") + if self.rootdir == None: + raise rcs.SettingIDnotSupported + author_path = os.path.join(self.rootdir, "_darcs", "prefs", "author") + f = codecs.open(author_path, "w", self.encoding) + f.write(value) + f.close() + def _rcs_add(self, path): + if os.path.isdir(path): + return + self._u_invoke_client("add", path) + def _rcs_remove(self, path): + if not os.path.isdir(self._u_abspath(path)): + os.remove(os.path.join(self.rootdir, path)) # darcs notices removal + def _rcs_update(self, path): + pass # darcs notices changes + def _rcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return RCS._rcs_get_file_contents(self, path, revision, + binary=binary) + else: + try: + return self._u_invoke_client("show", "contents", "--patch", revision, path) + except rcs.CommandError: + # Darcs versions < 2.0.0pre2 lack the "show contents" command + + status,output,error = self._u_invoke_client("diff", "--unified", + "--from-patch", + revision, path) + major_patch = output + status,output,error = self._u_invoke_client("diff", "--unified", + "--patch", + revision, path) + target_patch = output + + # "--output -" to be supported in GNU patch > 2.5.9 + # but that hasn't been released as of June 30th, 2009. + + # Rewrite path to status before the patch we want + args=["patch", "--reverse", path] + status,output,error = self._u_invoke(args, stdin=major_patch) + # Now apply the patch we want + args=["patch", path] + status,output,error = self._u_invoke(args, stdin=target_patch) + + if os.path.exists(os.path.join(self.rootdir, path)) == True: + contents = RCS._rcs_get_file_contents(self, path, + binary=binary) + else: + contents = "" + + # Now restore path to it's current incarnation + args=["patch", "--reverse", path] + status,output,error = self._u_invoke(args, stdin=target_patch) + args=["patch", path] + status,output,error = self._u_invoke(args, stdin=major_patch) + current_contents = RCS._rcs_get_file_contents(self, path, + binary=binary) + return contents + def _rcs_duplicate_repo(self, directory, revision=None): + if revision==None: + RCS._rcs_duplicate_repo(self, directory, revision) + else: + self._u_invoke_client("put", "--to-patch", revision, directory) + def _rcs_commit(self, commitfile, allow_empty=False): + id = self.get_user_id() + if '@' not in id: + id = "%s <%s@invalid.com>" % (id, id) + args = ['record', '--all', '--author', id, '--logfile', commitfile] + status,output,error = self._u_invoke_client(*args) + empty_strings = ["No changes!"] + revision = None + if self._u_any_in_string(empty_strings, output) == True: + if allow_empty == False: + raise rcs.EmptyCommit() + else: # we need a extra call to get the current revision + args = ["changes", "--last=1", "--xml"] + status,output,error = self._u_invoke_client(*args) + revline = re.compile("[ \t]*(.*)") + # note that darcs does _not_ make an empty revision. + # this returns the last non-empty revision id... + else: + revline = re.compile("Finished recording patch '(.*)'") + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 1 + revision = match.groups()[0] + return revision + + +rcs.make_rcs_testcase_subclasses(Darcs, sys.modules[__name__]) + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/diff.py b/interfaces/web/Bugs-Everywhere-Web/libbe/diff.py new file mode 100644 index 0000000..ba48efc --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/diff.py @@ -0,0 +1,125 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Compare two bug trees""" +from libbe import cmdutil, bugdir, bug +from libbe.utility import time_to_str +import doctest + +def bug_diffs(old_bugdir, new_bugdir): + added = [] + removed = [] + modified = [] + for uuid in old_bugdir.list_uuids(): + old_bug = old_bugdir.bug_from_uuid(uuid) + try: + new_bug = new_bugdir.bug_from_uuid(uuid) + old_bug.load_comments() + new_bug.load_comments() + if old_bug != new_bug: + modified.append((old_bug, new_bug)) + except KeyError: + removed.append(old_bug) + for uuid in new_bugdir.list_uuids(): + if not old_bugdir.has_bug(uuid): + new_bug = new_bugdir.bug_from_uuid(uuid) + added.append(new_bug) + return (removed, modified, added) + +def diff_report(bug_diffs_data, old_bugdir, new_bugdir): + bugs_removed,bugs_modified,bugs_added = bug_diffs_data + def modified_cmp(left, right): + return bug.cmp_severity(left[1], right[1]) + + bugs_added.sort(bug.cmp_severity) + bugs_removed.sort(bug.cmp_severity) + bugs_modified.sort(modified_cmp) + lines = [] + + if old_bugdir.settings != new_bugdir.settings: + bugdir_settings = sorted(new_bugdir.settings_properties) + bugdir_settings.remove("rcs_name") # tweaked by bugdir.duplicate_bugdir + change_list = change_lines(old_bugdir, new_bugdir, bugdir_settings) + if len(change_list) > 0: + lines.append("Modified bug directory:") + change_strings = ["%s: %s -> %s" % f for f in change_list] + lines.extend(change_strings) + lines.append("") + if len(bugs_added) > 0: + lines.append("New bug reports:") + for bg in bugs_added: + lines.extend(bg.string(shortlist=True).splitlines()) + lines.append("") + if len(bugs_modified) > 0: + printed = False + for old_bug, new_bug in bugs_modified: + change_str = bug_changes(old_bug, new_bug) + if change_str is None: + continue + if not printed: + printed = True + lines.append("Modified bug reports:") + lines.extend(change_str.splitlines()) + if printed == True: + lines.append("") + if len(bugs_removed) > 0: + lines.append("Removed bug reports:") + for bg in bugs_removed: + lines.extend(bg.string(shortlist=True).splitlines()) + lines.append("") + + return "\n".join(lines).rstrip("\n") + +def change_lines(old, new, attributes): + change_list = [] + for attr in attributes: + old_attr = getattr(old, attr) + new_attr = getattr(new, attr) + if old_attr != new_attr: + change_list.append((attr, old_attr, new_attr)) + if len(change_list) >= 0: + return change_list + else: + return None + +def bug_changes(old, new): + bug_settings = sorted(new.settings_properties) + change_list = change_lines(old, new, bug_settings) + change_strings = ["%s: %s -> %s" % f for f in change_list] + + old_comment_ids = [c.uuid for c in old.comments()] + new_comment_ids = [c.uuid for c in new.comments()] + for comment_id in new_comment_ids: + if comment_id not in old_comment_ids: + summary = comment_summary(new.comment_from_uuid(comment_id), "new") + change_strings.append(summary) + for comment_id in old_comment_ids: + if comment_id not in new_comment_ids: + summary = comment_summary(new.comment_from_uuid(comment_id), + "removed") + change_strings.append(summary) + + if len(change_strings) == 0: + return None + return "%s\n %s" % (new.string(shortlist=True), + " \n".join(change_strings)) + + +def comment_summary(comment, status): + return "%8s comment from %s on %s" % (status, comment.From, + time_to_str(comment.time)) + +suite = doctest.DocTestSuite() diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/editor.py b/interfaces/web/Bugs-Everywhere-Web/libbe/editor.py new file mode 100644 index 0000000..93144b8 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/editor.py @@ -0,0 +1,101 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2008-2009 W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import codecs +import locale +import os +import sys +import tempfile +import doctest + +default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() + +comment_marker = u"== Anything below this line will be ignored\n" + +class CantFindEditor(Exception): + def __init__(self): + Exception.__init__(self, "Can't find editor to get string from") + +def editor_string(comment=None, encoding=None): + """Invokes the editor, and returns the user-produced text as a string + + >>> if "EDITOR" in os.environ: + ... del os.environ["EDITOR"] + >>> if "VISUAL" in os.environ: + ... del os.environ["VISUAL"] + >>> editor_string() + Traceback (most recent call last): + CantFindEditor: Can't find editor to get string from + >>> os.environ["EDITOR"] = "echo bar > " + >>> editor_string() + u'bar\\n' + >>> os.environ["VISUAL"] = "echo baz > " + >>> editor_string() + u'baz\\n' + >>> del os.environ["EDITOR"] + >>> del os.environ["VISUAL"] + """ + if encoding == None: + encoding = default_encoding + for name in ('VISUAL', 'EDITOR'): + try: + editor = os.environ[name] + break + except KeyError: + pass + else: + raise CantFindEditor() + fhandle, fname = tempfile.mkstemp() + try: + if comment is not None: + os.write(fhandle, '\n'+comment_string(comment)) + os.close(fhandle) + oldmtime = os.path.getmtime(fname) + os.system("%s %s" % (editor, fname)) + f = codecs.open(fname, "r", encoding) + output = trimmed_string(f.read()) + f.close() + if output.rstrip('\n') == "": + output = None + finally: + os.unlink(fname) + return output + + +def comment_string(comment): + """ + >>> comment_string('hello') == comment_marker+"hello" + True + """ + return comment_marker + comment + + +def trimmed_string(instring): + """ + >>> trimmed_string("hello\\n"+comment_marker) + u'hello\\n' + >>> trimmed_string("hi!\\n" + comment_string('Booga')) + u'hi!\\n' + """ + out = [] + for line in instring.splitlines(True): + if line.startswith(comment_marker): + break + out.append(line) + return ''.join(out) + +suite = doctest.DocTestSuite() diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/encoding.py b/interfaces/web/Bugs-Everywhere-Web/libbe/encoding.py new file mode 100644 index 0000000..d603602 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/encoding.py @@ -0,0 +1,51 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2008-2009 W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import codecs +import locale +import sys +import doctest + +def get_encoding(): + """ + Guess a useful input/output/filesystem encoding... Maybe we need + seperate encodings for input/output and filesystem? Hmm... + """ + encoding = locale.getpreferredencoding() or sys.getdefaultencoding() + if sys.platform != 'win32' or sys.version_info[:2] > (2, 3): + encoding = locale.getlocale(locale.LC_TIME)[1] or encoding + # Python 2.3 on windows doesn't know about 'XYZ' alias for 'cpXYZ' + return encoding + +def known_encoding(encoding): + """ + >>> known_encoding("highly-unlikely-encoding") + False + >>> known_encoding(get_encoding()) + True + """ + try: + codecs.lookup(encoding) + return True + except LookupError: + return False + +def set_IO_stream_encodings(encoding): + sys.stdin = codecs.getreader(encoding)(sys.__stdin__) + sys.stdout = codecs.getwriter(encoding)(sys.__stdout__) + sys.stderr = codecs.getwriter(encoding)(sys.__stderr__) + +suite = doctest.DocTestSuite() diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/git.py b/interfaces/web/Bugs-Everywhere-Web/libbe/git.py new file mode 100644 index 0000000..2f9ffa9 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/git.py @@ -0,0 +1,120 @@ +# Copyright (C) 2008-2009 Ben Finney +# Chris Ball +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os +import re +import sys +import unittest +import doctest + +import rcs +from rcs import RCS + +def new(): + return Git() + +class Git(RCS): + name="git" + client="git" + versioned=True + def _rcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _rcs_detect(self, path): + if self._u_search_parent_directories(path, ".git") != None : + return True + return False + def _rcs_root(self, path): + """Find the root of the deepest repository containing path.""" + # Assume that nothing funny is going on; in particular, that we aren't + # dealing with a bare repo. + if os.path.isdir(path) != True: + path = os.path.dirname(path) + status,output,error = self._u_invoke_client("rev-parse", "--git-dir", + directory=path) + gitdir = os.path.join(path, output.rstrip('\n')) + dirname = os.path.abspath(os.path.dirname(gitdir)) + return dirname + def _rcs_init(self, path): + self._u_invoke_client("init", directory=path) + def _rcs_get_user_id(self): + status,output,error = self._u_invoke_client("config", "user.name") + name = output.rstrip('\n') + status,output,error = self._u_invoke_client("config", "user.email") + email = output.rstrip('\n') + if name != "" or email != "": # got something! + # guess missing info, if necessary + if name == "": + name = self._u_get_fallback_username() + if email == "": + email = self._u_get_fallback_email() + return self._u_create_id(name, email) + return None # Git has no infomation + def _rcs_set_user_id(self, value): + name,email = self._u_parse_id(value) + if email != None: + self._u_invoke_client("config", "user.email", email) + self._u_invoke_client("config", "user.name", name) + def _rcs_add(self, path): + if os.path.isdir(path): + return + self._u_invoke_client("add", path) + def _rcs_remove(self, path): + if not os.path.isdir(self._u_abspath(path)): + self._u_invoke_client("rm", "-f", path) + def _rcs_update(self, path): + self._rcs_add(path) + def _rcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + else: + arg = "%s:%s" % (revision,path) + status,output,error = self._u_invoke_client("show", arg) + return output + def _rcs_duplicate_repo(self, directory, revision=None): + if revision==None: + RCS._rcs_duplicate_repo(self, directory, revision) + else: + #self._u_invoke_client("archive", revision, directory) # makes tarball + self._u_invoke_client("clone", "--no-checkout",".",directory) + self._u_invoke_client("checkout", revision, directory=directory) + def _rcs_commit(self, commitfile, allow_empty=False): + args = ['commit', '--all', '--file', commitfile] + if allow_empty == True: + args.append("--allow-empty") + status,output,error = self._u_invoke_client(*args) + else: + kwargs = {"expect":(0,1)} + status,output,error = self._u_invoke_client(*args, **kwargs) + strings = ["nothing to commit", + "nothing added to commit"] + if self._u_any_in_string(strings, output) == True: + raise rcs.EmptyCommit() + revision = None + revline = re.compile("(.*) (.*)[:\]] (.*)") + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 3 + revision = match.groups()[1] + return revision + + +rcs.make_rcs_testcase_subclasses(Git, sys.modules[__name__]) + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/hg.py b/interfaces/web/Bugs-Everywhere-Web/libbe/hg.py new file mode 100644 index 0000000..a20eeb5 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/hg.py @@ -0,0 +1,96 @@ +# Copyright (C) 2007-2009 Aaron Bentley and Panometrics, Inc. +# Ben Finney +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os +import re +import sys +import unittest +import doctest + +import rcs +from rcs import RCS + +def new(): + return Hg() + +class Hg(RCS): + name="hg" + client="hg" + versioned=True + def _rcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _rcs_detect(self, path): + """Detect whether a directory is revision-controlled using Mercurial""" + if self._u_search_parent_directories(path, ".hg") != None: + return True + return False + def _rcs_root(self, path): + status,output,error = self._u_invoke_client("root", directory=path) + return output.rstrip('\n') + def _rcs_init(self, path): + self._u_invoke_client("init", directory=path) + def _rcs_get_user_id(self): + status,output,error = self._u_invoke_client("showconfig","ui.username") + return output.rstrip('\n') + def _rcs_set_user_id(self, value): + """ + Supported by the Config Extension, but that is not part of + standard Mercurial. + http://www.selenic.com/mercurial/wiki/index.cgi/ConfigExtension + """ + raise rcs.SettingIDnotSupported + def _rcs_add(self, path): + self._u_invoke_client("add", path) + def _rcs_remove(self, path): + self._u_invoke_client("rm", "--force", path) + def _rcs_update(self, path): + pass + def _rcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + else: + status,output,error = \ + self._u_invoke_client("cat","-r",revision,path) + return output + def _rcs_duplicate_repo(self, directory, revision=None): + if revision == None: + return RCS._rcs_duplicate_repo(self, directory, revision) + else: + self._u_invoke_client("archive", "--rev", revision, directory) + def _rcs_commit(self, commitfile, allow_empty=False): + args = ['commit', '--logfile', commitfile] + status,output,error = self._u_invoke_client(*args) + if allow_empty == False: + strings = ["nothing changed"] + if self._u_any_in_string(strings, output) == True: + raise rcs.EmptyCommit() + status,output,error = self._u_invoke_client('identify') + revision = None + revline = re.compile("(.*) tip") + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 1 + revision = match.groups()[0] + return revision + + +rcs.make_rcs_testcase_subclasses(Hg, sys.modules[__name__]) + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/mapfile.py b/interfaces/web/Bugs-Everywhere-Web/libbe/mapfile.py new file mode 100644 index 0000000..b959d76 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/mapfile.py @@ -0,0 +1,127 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import yaml +import os.path +import errno +import utility +import doctest + +class IllegalKey(Exception): + def __init__(self, key): + Exception.__init__(self, 'Illegal key "%s"' % key) + self.key = key + +class IllegalValue(Exception): + def __init__(self, value): + Exception.__init__(self, 'Illegal value "%s"' % value) + self.value = value + +def generate(map): + """Generate a YAML mapfile content string. + >>> generate({"q":"p"}) + 'q: p\\n\\n' + >>> generate({"q":u"Fran\u00e7ais"}) + 'q: Fran\\xc3\\xa7ais\\n\\n' + >>> generate({"q":u"hello"}) + 'q: hello\\n\\n' + >>> generate({"q=":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "q=" + >>> generate({"q:":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "q:" + >>> generate({"q\\n":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "q\\n" + >>> generate({"":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "" + >>> generate({">q":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key ">q" + >>> generate({"q":"p\\n"}) + Traceback (most recent call last): + IllegalValue: Illegal value "p\\n" + """ + keys = map.keys() + keys.sort() + for key in keys: + try: + assert not key.startswith('>') + assert('\n' not in key) + assert('=' not in key) + assert(':' not in key) + assert(len(key) > 0) + except AssertionError: + raise IllegalKey(unicode(key).encode('unicode_escape')) + if "\n" in map[key]: + raise IllegalValue(unicode(map[key]).encode('unicode_escape')) + + lines = [] + for key in keys: + lines.append(yaml.safe_dump({key: map[key]}, + default_flow_style=False, + allow_unicode=True)) + lines.append("") + return '\n'.join(lines) + +def parse(contents): + """ + Parse a YAML mapfile string. + >>> parse('q: p\\n\\n')['q'] + 'p' + >>> parse('q: \\'p\\'\\n\\n')['q'] + 'p' + >>> contents = generate({"a":"b", "c":"d", "e":"f"}) + >>> dict = parse(contents) + >>> dict["a"] + 'b' + >>> dict["c"] + 'd' + >>> dict["e"] + 'f' + """ + old_format = False + for line in contents.splitlines(): + if len(line.split("=")) == 2: + old_format = True + break + if old_format: # translate to YAML. Hack to deal with old BE bugs. + newlines = [] + for line in contents.splitlines(): + line = line.rstrip('\n') + if len(line) == 0: + continue + fields = line.split("=") + if len(fields) == 2: + key,value = fields + newlines.append('%s: "%s"' % (key, value.replace('"','\\"'))) + else: + newlines.append(line) + contents = '\n'.join(newlines) + return yaml.load(contents) or {} + +def map_save(rcs, path, map, allow_no_rcs=False): + """Save the map as a mapfile to the specified path""" + contents = generate(map) + rcs.set_file_contents(path, contents, allow_no_rcs) + +def map_load(rcs, path, allow_no_rcs=False): + contents = rcs.get_file_contents(path, allow_no_rcs=allow_no_rcs) + return parse(contents) + +suite = doctest.DocTestSuite() diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/plugin.py b/interfaces/web/Bugs-Everywhere-Web/libbe/plugin.py new file mode 100644 index 0000000..0545fd7 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/plugin.py @@ -0,0 +1,71 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Marien Zwart +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import os +import os.path +import sys +import doctest + +def my_import(mod_name): + module = __import__(mod_name) + components = mod_name.split('.') + for comp in components[1:]: + module = getattr(module, comp) + return module + +def iter_plugins(prefix): + """ + >>> "list" in [n for n,m in iter_plugins("becommands")] + True + >>> "plugin" in [n for n,m in iter_plugins("libbe")] + True + """ + modfiles = os.listdir(os.path.join(plugin_path, prefix)) + modfiles.sort() + for modfile in modfiles: + if modfile.startswith('.'): + continue # the occasional emacs temporary file + if modfile.endswith(".py") and modfile != "__init__.py": + yield modfile[:-3], my_import(prefix+"."+modfile[:-3]) + + +def get_plugin(prefix, name): + """ + >>> get_plugin("becommands", "asdf") is None + True + >>> q = repr(get_plugin("becommands", "list")) + >>> q.startswith(" +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +This module provides a series of useful decorators for defining +various types of properties. For example usage, consider the +unittests at the end of the module. + +See + http://www.python.org/dev/peps/pep-0318/ +and + http://www.phyast.pitt.edu/~micheles/python/documentation.html +for more information on decorators. +""" + +import copy +import types +import unittest + + +class ValueCheckError (ValueError): + def __init__(self, name, value, allowed): + action = "in" # some list of allowed values + if type(allowed) == types.FunctionType: + action = "allowed by" # some allowed-value check function + msg = "%s not %s %s for %s" % (value, action, allowed, name) + ValueError.__init__(self, msg) + self.name = name + self.value = value + self.allowed = allowed + +def Property(funcs): + """ + End a chain of property decorators, returning a property. + """ + args = {} + args["fget"] = funcs.get("fget", None) + args["fset"] = funcs.get("fset", None) + args["fdel"] = funcs.get("fdel", None) + args["doc"] = funcs.get("doc", None) + + #print "Creating a property with" + #for key, val in args.items(): print key, value + return property(**args) + +def doc_property(doc=None): + """ + Add a docstring to a chain of property decorators. + """ + def decorator(funcs=None): + """ + Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...} + or a function fn() returning such a dict. + """ + if hasattr(funcs, "__call__"): + funcs = funcs() # convert from function-arg to dict + funcs["doc"] = doc + return funcs + return decorator + +def local_property(name, null=None, mutable_null=False): + """ + Define get/set access to per-parent-instance local storage. Uses + .__value to store the value for a particular owner instance. + If the .__value attribute does not exist, returns null. + + If mutable_null == True, we only release deepcopies of the null to + the outside world. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget", None) + fset = funcs.get("fset", None) + def _fget(self): + if fget is not None: + fget(self) + if mutable_null == True: + ret_null = copy.deepcopy(null) + else: + ret_null = null + value = getattr(self, "_%s_value" % name, ret_null) + return value + def _fset(self, value): + setattr(self, "_%s_value" % name, value) + if fset is not None: + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + funcs["name"] = name + return funcs + return decorator + +def settings_property(name, null=None): + """ + Similar to local_property, except where local_property stores the + value in instance.__value, settings_property stores the + value in instance.settings[name]. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget", None) + fset = funcs.get("fset", None) + def _fget(self): + if fget is not None: + fget(self) + value = self.settings.get(name, null) + return value + def _fset(self, value): + self.settings[name] = value + if fset is not None: + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + funcs["name"] = name + return funcs + return decorator + + +# Allow comparison and caching with _original_ values for mutables, +# since +# +# >>> a = [] +# >>> b = a +# >>> b.append(1) +# >>> a +# [1] +# >>> a==b +# True +def _hash_mutable_value(value): + return repr(value) +def _init_mutable_property_cache(self): + if not hasattr(self, "_mutable_property_cache_hash"): + # first call to _fget for any mutable property + self._mutable_property_cache_hash = {} + self._mutable_property_cache_copy = {} +def _set_cached_mutable_property(self, cacher_name, property_name, value): + _init_mutable_property_cache(self) + self._mutable_property_cache_hash[(cacher_name, property_name)] = \ + _hash_mutable_value(value) + self._mutable_property_cache_copy[(cacher_name, property_name)] = \ + copy.deepcopy(value) +def _get_cached_mutable_property(self, cacher_name, property_name, default=None): + _init_mutable_property_cache(self) + if (cacher_name, property_name) not in self._mutable_property_cache_copy: + return default + return self._mutable_property_cache_copy[(cacher_name, property_name)] +def _cmp_cached_mutable_property(self, cacher_name, property_name, value): + _init_mutable_property_cache(self) + if (cacher_name, property_name) not in self._mutable_property_cache_hash: + return 1 # any value > non-existant old hash + old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)] + return cmp(_hash_mutable_value(value), old_hash) + + +def defaulting_property(default=None, null=None, + mutable_default=False): + """ + Define a default value for get access to a property. + If the stored value is null, then default is returned. + + If mutable_default == True, we only release deepcopies of the + default to the outside world. + + null should never escape to the outside world, so don't worry + about it being a mutable. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self): + value = fget(self) + if value == null: + if mutable_default == True: + return copy.deepcopy(default) + else: + return default + return value + def _fset(self, value): + if value == default: + value = null + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +def fn_checked_property(value_allowed_fn): + """ + Define allowed values for get/set access to a property. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self): + value = fget(self) + if value_allowed_fn(value) != True: + raise ValueCheckError(name, value, value_allowed_fn) + return value + def _fset(self, value): + if value_allowed_fn(value) != True: + raise ValueCheckError(name, value, value_allowed_fn) + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +def checked_property(allowed=[]): + """ + Define allowed values for get/set access to a property. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self): + value = fget(self) + if value not in allowed: + raise ValueCheckError(name, value, allowed) + return value + def _fset(self, value): + if value not in allowed: + raise ValueCheckError(name, value, allowed) + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +def cached_property(generator, initVal=None, mutable=False): + """ + Allow caching of values generated by generator(instance), where + instance is the instance to which this property belongs. Uses + .__cache to store a cache flag for a particular owner + instance. + + When the cache flag is True or missing and the stored value is + initVal, the first fget call triggers the generator function, + whose output is stored in __cached_value. That and + subsequent calls to fget will return this cached value. + + If the input value is no longer initVal (e.g. a value has been + loaded from disk or set with fset), that value overrides any + cached value, and this property has no effect. + + When the cache flag is False and the stored value is initVal, the + generator is not cached, but is called on every fget. + + The cache flag is missing on initialization. Particular instances + may override by setting their own flag. + + In the case that mutable == True, all caching is disabled and the + generator is called whenever the cached value would otherwise be + used. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + name = funcs.get("name", "") + def _fget(self): + cache = getattr(self, "_%s_cache" % name, True) + value = fget(self) + if value == initVal: + if cache == True and mutable == False: + if hasattr(self, "_%s_cached_value" % name): + value = getattr(self, "_%s_cached_value" % name) + else: + value = generator(self) + setattr(self, "_%s_cached_value" % name, value) + else: + value = generator(self) + return value + funcs["fget"] = _fget + return funcs + return decorator + +def primed_property(primer, initVal=None): + """ + Just like a cached_property, except that instead of returning a + new value and running fset to cache it, the primer performs some + background manipulation (e.g. loads data into instance.settings) + such that a _second_ pass through fget succeeds. + + The 'cache' flag becomes a 'prime' flag, with priming taking place + whenever .__prime is True, or is False or missing and + value == initVal. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + name = funcs.get("name", "") + def _fget(self): + prime = getattr(self, "_%s_prime" % name, False) + if prime == False: + value = fget(self) + if prime == True or (prime == False and value == initVal): + primer(self) + value = fget(self) + return value + funcs["fget"] = _fget + return funcs + return decorator + +def change_hook_property(hook, mutable=False): + """ + Call the function hook(instance, old_value, new_value) whenever a + value different from the current value is set (instance is a a + reference to the class instance to which this property belongs). + This is useful for saving changes to disk, etc. This function is + called _after_ the new value has been stored, allowing you to + change the stored value if you want. + + In the case of mutables, things are slightly trickier. Because + the property-owning class has no way of knowing when the value + changes. We work around this by caching a private deepcopy of the + mutable value, and checking for changes whenever the property is + set (obviously) or retrieved (to check for external changes). So + long as you're conscientious about accessing the property after + making external modifications, mutability woln't be a problem. + t.x.append(5) # external modification + t.x # dummy access notices change and triggers hook + See testChangeHookMutableProperty for an example of the expected + behavior. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self, new_value=None, from_fset=False): # only used if mutable == True + if from_fset == True: + value = new_value # compare new value with cached + else: + value = fget(self) # compare current value with cached + if _cmp_cached_mutable_property(self, "change hook property", name, value) != 0: + # there has been a change, cache new value + old_value = _get_cached_mutable_property(self, "change hook property", name) + _set_cached_mutable_property(self, "change hook property", name, value) + if from_fset == True: # return previously cached value + value = old_value + else: # the value changed while we weren't looking + hook(self, old_value, value) + return value + def _fset(self, value): + if mutable == True: # get cached previous value + old_value = _fget(self, new_value=value, from_fset=True) + else: + old_value = fget(self) + fset(self, value) + if value != old_value: + hook(self, old_value, value) + if mutable == True: + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + + +class DecoratorTests(unittest.TestCase): + def testLocalDoc(self): + class Test(object): + @Property + @doc_property("A fancy property") + def x(): + return {} + self.failUnless(Test.x.__doc__ == "A fancy property", + Test.x.__doc__) + def testLocalProperty(self): + class Test(object): + @Property + @local_property(name="LOCAL") + def x(): + return {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("_LOCAL_value" in dir(t), dir(t)) + self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value) + def testSettingsProperty(self): + class Test(object): + @Property + @settings_property(name="attr") + def x(): + return {} + def __init__(self): + self.settings = {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("attr" in t.settings, t.settings) + self.failUnless(t.settings["attr"] == 'z', t.settings["attr"]) + def testDefaultingLocalProperty(self): + class Test(object): + @Property + @defaulting_property(default='y', null='x') + @local_property(name="DEFAULT", null=5) + def x(): return {} + t = Test() + self.failUnless(t.x == 5, str(t.x)) + t.x = 'x' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'y' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'z' + self.failUnless(t.x == 'z', str(t.x)) + t.x = 5 + self.failUnless(t.x == 5, str(t.x)) + def testCheckedLocalProperty(self): + class Test(object): + @Property + @checked_property(allowed=['x', 'y', 'z']) + @local_property(name="CHECKED") + def x(): return {} + def __init__(self): + self._CHECKED_value = 'x' + t = Test() + self.failUnless(t.x == 'x', str(t.x)) + try: + t.x = None + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + def testTwoCheckedLocalProperties(self): + class Test(object): + @Property + @checked_property(allowed=['x', 'y', 'z']) + @local_property(name="X") + def x(): return {} + + @Property + @checked_property(allowed=['a', 'b', 'c']) + @local_property(name="A") + def a(): return {} + def __init__(self): + self._A_value = 'a' + self._X_value = 'x' + t = Test() + try: + t.x = 'a' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + t.x = 'x' + t.x = 'y' + t.x = 'z' + try: + t.a = 'x' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + t.a = 'a' + t.a = 'b' + t.a = 'c' + def testFnCheckedLocalProperty(self): + class Test(object): + @Property + @fn_checked_property(lambda v : v in ['x', 'y', 'z']) + @local_property(name="CHECKED") + def x(): return {} + def __init__(self): + self._CHECKED_value = 'x' + t = Test() + self.failUnless(t.x == 'x', str(t.x)) + try: + t.x = None + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + def testCachedLocalProperty(self): + class Gen(object): + def __init__(self): + self.i = 0 + def __call__(self, owner): + self.i += 1 + return self.i + class Test(object): + @Property + @cached_property(generator=Gen(), initVal=None) + @local_property(name="CACHED") + def x(): return {} + t = Test() + self.failIf("_CACHED_cache" in dir(t), getattr(t, "_CACHED_cache", None)) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + t.x = 8 + self.failUnless(t.x == 8, t.x) + self.failUnless(t.x == 8, t.x) + t._CACHED_cache = False # Caching is off, but the stored value + val = t.x # is 8, not the initVal (None), so we + self.failUnless(val == 8, val) # get 8. + t._CACHED_value = None # Now we've set the stored value to None + val = t.x # so future calls to fget (like this) + self.failUnless(val == 2, val) # will call the generator every time... + val = t.x + self.failUnless(val == 3, val) + val = t.x + self.failUnless(val == 4, val) + t._CACHED_cache = True # We turn caching back on, and get + self.failUnless(t.x == 1, str(t.x)) # the original cached value. + del t._CACHED_cached_value # Removing that value forces a + self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call + self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which + self.failUnless(t.x == 5, str(t.x)) # we get the new cached value. + def testPrimedLocalProperty(self): + class Test(object): + def prime(self): + self.settings["PRIMED"] = "initialized" + @Property + @primed_property(primer=prime, initVal=None) + @settings_property(name="PRIMED") + def x(): return {} + def __init__(self): + self.settings={} + t = Test() + self.failIf("_PRIMED_prime" in dir(t), getattr(t, "_PRIMED_prime", None)) + self.failUnless(t.x == "initialized", t.x) + t.x = 1 + self.failUnless(t.x == 1, t.x) + t.x = None + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = True + t.x = 3 + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = False + t.x = 3 + self.failUnless(t.x == 3, t.x) + def testChangeHookLocalProperty(self): + class Test(object): + def _hook(self, old, new): + self.old = old + self.new = new + + @Property + @change_hook_property(_hook) + @local_property(name="HOOKED") + def x(): return {} + t = Test() + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 2 + self.failUnless(t.old == 1, t.old) + self.failUnless(t.new == 2, t.new) + def testChangeHookMutableProperty(self): + class Test(object): + def _hook(self, old, new): + self.old = old + self.new = new + self.hook_calls += 1 + + @Property + @change_hook_property(_hook, mutable=True) + @local_property(name="HOOKED") + def x(): return {} + t = Test() + t.hook_calls = 0 + t.x = [] + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == [], t.new) + self.failUnless(t.hook_calls == 1, t.hook_calls) + a = t.x + a.append(5) + t.x = a + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 2, t.hook_calls) + t.x = [] + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [], t.new) + self.failUnless(t.hook_calls == 3, t.hook_calls) + # now append without reassigning. this doesn't trigger the + # change, since we don't ever set t.x, only get it and mess + # with it. It does, however, update our t.new, since t.new = + # t.x and is not a static copy. + t.x.append(5) + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 3, t.hook_calls) + # however, the next t.x get _will_ notice the change... + a = t.x + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 4, t.hook_calls) + t.x.append(6) # this append(6) is not noticed yet + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5,6], t.new) + self.failUnless(t.hook_calls == 4, t.hook_calls) + # this append(7) is not noticed, but the t.x get causes the + # append(6) to be noticed + t.x.append(7) + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [5,6,7], t.new) + self.failUnless(t.hook_calls == 5, t.hook_calls) + a = t.x # now the append(7) is noticed + self.failUnless(t.old == [5,6], t.old) + self.failUnless(t.new == [5,6,7], t.new) + self.failUnless(t.hook_calls == 6, t.hook_calls) + + +suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) + diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/rcs.py b/interfaces/web/Bugs-Everywhere-Web/libbe/rcs.py new file mode 100644 index 0000000..294b8e0 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/rcs.py @@ -0,0 +1,876 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Alexander Belchenko +# Ben Finney +# Chris Ball +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from subprocess import Popen, PIPE +import codecs +import os +import os.path +import re +from socket import gethostname +import shutil +import sys +import tempfile +import unittest +import doctest + +from utility import Dir, search_parent_directories + + +def _get_matching_rcs(matchfn): + """Return the first module for which matchfn(RCS_instance) is true""" + import arch + import bzr + import darcs + import git + import hg + for module in [arch, bzr, darcs, git, hg]: + rcs = module.new() + if matchfn(rcs) == True: + return rcs + del(rcs) + return RCS() + +def rcs_by_name(rcs_name): + """Return the module for the RCS with the given name""" + return _get_matching_rcs(lambda rcs: rcs.name == rcs_name) + +def detect_rcs(dir): + """Return an RCS instance for the rcs being used in this directory""" + return _get_matching_rcs(lambda rcs: rcs.detect(dir)) + +def installed_rcs(): + """Return an instance of an installed RCS""" + return _get_matching_rcs(lambda rcs: rcs.installed()) + + +class CommandError(Exception): + def __init__(self, command, status, err_str): + strerror = ["Command failed (%d):\n %s\n" % (status, err_str), + "while executing\n %s" % command] + Exception.__init__(self, "\n".join(strerror)) + self.command = command + self.status = status + self.err_str = err_str + +class SettingIDnotSupported(NotImplementedError): + pass + +class RCSnotRooted(Exception): + def __init__(self): + msg = "RCS not rooted" + Exception.__init__(self, msg) + +class PathNotInRoot(Exception): + def __init__(self, path, root): + msg = "Path '%s' not in root '%s'" % (path, root) + Exception.__init__(self, msg) + self.path = path + self.root = root + +class NoSuchFile(Exception): + def __init__(self, pathname, root="."): + path = os.path.abspath(os.path.join(root, pathname)) + Exception.__init__(self, "No such file: %s" % path) + +class EmptyCommit(Exception): + def __init__(self): + Exception.__init__(self, "No changes to commit") + + +def new(): + return RCS() + +class RCS(object): + """ + This class implements a 'no-rcs' interface. + + Support for other RCSs can be added by subclassing this class, and + overriding methods _rcs_*() with code appropriate for your RCS. + + The methods _u_*() are utility methods available to the _rcs_*() + methods. + """ + name = "None" + client = "" # command-line tool for _u_invoke_client + versioned = False + def __init__(self, paranoid=False, encoding=sys.getdefaultencoding()): + self.paranoid = paranoid + self.verboseInvoke = False + self.rootdir = None + self._duplicateBasedir = None + self._duplicateDirname = None + self.encoding = encoding + def __del__(self): + self.cleanup() + + def _rcs_help(self): + """ + Return the command help string. + (Allows a simple test to see if the client is installed.) + """ + pass + def _rcs_detect(self, path=None): + """ + Detect whether a directory is revision controlled with this RCS. + """ + return True + def _rcs_root(self, path): + """ + Get the RCS root. This is the default working directory for + future invocations. You would normally set this to the root + directory for your RCS. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + if path == "": + path = os.path.abspath(".") + return path + def _rcs_init(self, path): + """ + Begin versioning the tree based at path. + """ + pass + def _rcs_cleanup(self): + """ + Remove any cruft that _rcs_init() created outside of the + versioned tree. + """ + pass + def _rcs_get_user_id(self): + """ + Get the RCS's suggested user id (e.g. "John Doe "). + If the RCS has not been configured with a username, return None. + """ + return None + def _rcs_set_user_id(self, value): + """ + Set the RCS's suggested user id (e.g "John Doe "). + This is run if the RCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + raise SettingIDnotSupported + def _rcs_add(self, path): + """ + Add the already created file at path to version control. + """ + pass + def _rcs_remove(self, path): + """ + Remove the file at path from version control. Optionally + remove the file from the filesystem as well. + """ + pass + def _rcs_update(self, path): + """ + Notify the versioning system of changes to the versioned file + at path. + """ + pass + def _rcs_get_file_contents(self, path, revision=None, binary=False): + """ + Get the file contents as they were in a given revision. + Revision==None specifies the current revision. + """ + assert revision == None, \ + "The %s RCS does not support revision specifiers" % self.name + if binary == False: + f = codecs.open(os.path.join(self.rootdir, path), "r", self.encoding) + else: + f = open(os.path.join(self.rootdir, path), "rb") + contents = f.read() + f.close() + return contents + def _rcs_duplicate_repo(self, directory, revision=None): + """ + Get the repository as it was in a given revision. + revision==None specifies the current revision. + dir specifies a directory to create the duplicate in. + """ + shutil.copytree(self.rootdir, directory, True) + def _rcs_commit(self, commitfile, allow_empty=False): + """ + Commit the current working directory, using the contents of + commitfile as the comment. Return the name of the old + revision (or None if commits are not supported). + + If allow_empty == False, raise EmptyCommit if there are no + changes to commit. + """ + return None + def installed(self): + try: + self._rcs_help() + return True + except OSError, e: + if e.errno == errno.ENOENT: + return False + except CommandError: + return False + def detect(self, path="."): + """ + Detect whether a directory is revision controlled with this RCS. + """ + return self._rcs_detect(path) + def root(self, path): + """ + Set the root directory to the path's RCS root. This is the + default working directory for future invocations. + """ + self.rootdir = self._rcs_root(path) + def init(self, path): + """ + Begin versioning the tree based at path. + Also roots the rcs at path. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + self._rcs_init(path) + self.root(path) + def cleanup(self): + self._rcs_cleanup() + def get_user_id(self): + """ + Get the RCS's suggested user id (e.g. "John Doe "). + If the RCS has not been configured with a username, return the user's + id. You can override the automatic lookup procedure by setting the + RCS.user_id attribute to a string of your choice. + """ + if hasattr(self, "user_id"): + if self.user_id != None: + return self.user_id + id = self._rcs_get_user_id() + if id == None: + name = self._u_get_fallback_username() + email = self._u_get_fallback_email() + id = self._u_create_id(name, email) + print >> sys.stderr, "Guessing id '%s'" % id + try: + self.set_user_id(id) + except SettingIDnotSupported: + pass + return id + def set_user_id(self, value): + """ + Set the RCS's suggested user id (e.g "John Doe "). + This is run if the RCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + self._rcs_set_user_id(value) + def add(self, path): + """ + Add the already created file at path to version control. + """ + self._rcs_add(self._u_rel_path(path)) + def remove(self, path): + """ + Remove a file from both version control and the filesystem. + """ + self._rcs_remove(self._u_rel_path(path)) + if os.path.exists(path): + os.remove(path) + def recursive_remove(self, dirname): + """ + Remove a file/directory and all its decendents from both + version control and the filesystem. + """ + if not os.path.exists(dirname): + raise NoSuchFile(dirname) + for dirpath,dirnames,filenames in os.walk(dirname, topdown=False): + filenames.extend(dirnames) + for path in filenames: + fullpath = os.path.join(dirpath, path) + if os.path.exists(fullpath) == False: + continue + self._rcs_remove(self._u_rel_path(fullpath)) + if os.path.exists(dirname): + shutil.rmtree(dirname) + def update(self, path): + """ + Notify the versioning system of changes to the versioned file + at path. + """ + self._rcs_update(self._u_rel_path(path)) + def get_file_contents(self, path, revision=None, allow_no_rcs=False, binary=False): + """ + Get the file as it was in a given revision. + Revision==None specifies the current revision. + """ + if not os.path.exists(path): + raise NoSuchFile(path) + if self._use_rcs(path, allow_no_rcs): + relpath = self._u_rel_path(path) + contents = self._rcs_get_file_contents(relpath,revision,binary=binary) + else: + f = codecs.open(path, "r", self.encoding) + contents = f.read() + f.close() + return contents + def set_file_contents(self, path, contents, allow_no_rcs=False, binary=False): + """ + Set the file contents under version control. + """ + add = not os.path.exists(path) + if binary == False: + f = codecs.open(path, "w", self.encoding) + else: + f = open(path, "wb") + f.write(contents) + f.close() + + if self._use_rcs(path, allow_no_rcs): + if add: + self.add(path) + else: + self.update(path) + def mkdir(self, path, allow_no_rcs=False, check_parents=True): + """ + Create (if neccessary) a directory at path under version + control. + """ + if check_parents == True: + parent = os.path.dirname(path) + if not os.path.exists(parent): # recurse through parents + self.mkdir(parent, allow_no_rcs, check_parents) + if not os.path.exists(path): + os.mkdir(path) + if self._use_rcs(path, allow_no_rcs): + self.add(path) + else: + assert os.path.isdir(path) + if self._use_rcs(path, allow_no_rcs): + #self.update(path)# Don't update directories. Changing files + pass # underneath them should be sufficient. + + def duplicate_repo(self, revision=None): + """ + Get the repository as it was in a given revision. + revision==None specifies the current revision. + Return the path to the arbitrary directory at the base of the new repo. + """ + # Dirname in Baseir to protect against simlink attacks. + if self._duplicateBasedir == None: + self._duplicateBasedir = tempfile.mkdtemp(prefix='BErcs') + self._duplicateDirname = \ + os.path.join(self._duplicateBasedir, "duplicate") + self._rcs_duplicate_repo(directory=self._duplicateDirname, + revision=revision) + return self._duplicateDirname + def remove_duplicate_repo(self): + """ + Clean up a duplicate repo created with duplicate_repo(). + """ + if self._duplicateBasedir != None: + shutil.rmtree(self._duplicateBasedir) + self._duplicateBasedir = None + self._duplicateDirname = None + def commit(self, summary, body=None, allow_empty=False): + """ + Commit the current working directory, with a commit message + string summary and body. Return the name of the old revision + (or None if versioning is not supported). + + If allow_empty == False (the default), raise EmptyCommit if + there are no changes to commit. + """ + summary = summary.strip()+'\n' + if body is not None: + summary += '\n' + body.strip() + '\n' + descriptor, filename = tempfile.mkstemp() + revision = None + try: + temp_file = os.fdopen(descriptor, 'wb') + temp_file.write(summary) + temp_file.flush() + self.precommit() + revision = self._rcs_commit(filename, allow_empty=allow_empty) + temp_file.close() + self.postcommit() + finally: + os.remove(filename) + return revision + def precommit(self): + """ + Executed before all attempted commits. + """ + pass + def postcommit(self): + """ + Only executed after successful commits. + """ + pass + def _u_any_in_string(self, list, string): + """ + Return True if any of the strings in list are in string. + Otherwise return False. + """ + for list_string in list: + if list_string in string: + return True + return False + def _u_invoke(self, args, stdin=None, expect=(0,), cwd=None): + """ + expect should be a tuple of allowed exit codes. cwd should be + the directory from which the command will be executed. + """ + if cwd == None: + cwd = self.rootdir + if self.verboseInvoke == True: + print >> sys.stderr, "%s$ %s" % (cwd, " ".join(args)) + try : + if sys.platform != "win32": + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd) + else: + # win32 don't have os.execvp() so have to run command in a shell + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, + shell=True, cwd=cwd) + except OSError, e : + raise CommandError(args, e.args[0], e) + output, error = q.communicate(input=stdin) + status = q.wait() + if self.verboseInvoke == True: + print >> sys.stderr, "%d\n%s%s" % (status, output, error) + if status not in expect: + raise CommandError(args, status, error) + return status, output, error + def _u_invoke_client(self, *args, **kwargs): + directory = kwargs.get('directory',None) + expect = kwargs.get('expect', (0,)) + stdin = kwargs.get('stdin', None) + cl_args = [self.client] + cl_args.extend(args) + return self._u_invoke(cl_args, stdin=stdin,expect=expect,cwd=directory) + def _u_search_parent_directories(self, path, filename): + """ + Find the file (or directory) named filename in path or in any + of path's parents. + + e.g. + search_parent_directories("/a/b/c", ".be") + will return the path to the first existing file from + /a/b/c/.be + /a/b/.be + /a/.be + /.be + or None if none of those files exist. + """ + return search_parent_directories(path, filename) + def _use_rcs(self, path, allow_no_rcs): + """ + Try and decide if _rcs_add/update/mkdir/etc calls will + succeed. Returns True is we think the rcs_call would + succeeed, and False otherwise. + """ + use_rcs = True + exception = None + if self.rootdir != None: + if self.path_in_root(path) == False: + use_rcs = False + exception = PathNotInRoot(path, self.rootdir) + else: + use_rcs = False + exception = RCSnotRooted + if use_rcs == False and allow_no_rcs==False: + raise exception + return use_rcs + def path_in_root(self, path, root=None): + """ + Return the relative path to path from root. + >>> rcs = new() + >>> rcs.path_in_root("/a.b/c/.be", "/a.b/c") + True + >>> rcs.path_in_root("/a.b/.be", "/a.b/c") + False + """ + if root == None: + if self.rootdir == None: + raise RCSnotRooted + root = self.rootdir + path = os.path.abspath(path) + absRoot = os.path.abspath(root) + absRootSlashedDir = os.path.join(absRoot,"") + if not path.startswith(absRootSlashedDir): + return False + return True + def _u_rel_path(self, path, root=None): + """ + Return the relative path to path from root. + >>> rcs = new() + >>> rcs._u_rel_path("/a.b/c/.be", "/a.b/c") + '.be' + """ + if root == None: + if self.rootdir == None: + raise RCSnotRooted + root = self.rootdir + path = os.path.abspath(path) + absRoot = os.path.abspath(root) + absRootSlashedDir = os.path.join(absRoot,"") + if not path.startswith(absRootSlashedDir): + raise PathNotInRoot(path, absRootSlashedDir) + assert path != absRootSlashedDir, \ + "file %s == root directory %s" % (path, absRootSlashedDir) + relpath = path[len(absRootSlashedDir):] + return relpath + def _u_abspath(self, path, root=None): + """ + Return the absolute path from a path realtive to root. + >>> rcs = new() + >>> rcs._u_abspath(".be", "/a.b/c") + '/a.b/c/.be' + """ + if root == None: + assert self.rootdir != None, "RCS not rooted" + root = self.rootdir + return os.path.abspath(os.path.join(root, path)) + def _u_create_id(self, name, email=None): + """ + >>> rcs = new() + >>> rcs._u_create_id("John Doe", "jdoe@example.com") + 'John Doe ' + >>> rcs._u_create_id("John Doe") + 'John Doe' + """ + assert len(name) > 0 + if email == None or len(email) == 0: + return name + else: + return "%s <%s>" % (name, email) + def _u_parse_id(self, value): + """ + >>> rcs = new() + >>> rcs._u_parse_id("John Doe ") + ('John Doe', 'jdoe@example.com') + >>> rcs._u_parse_id("John Doe") + ('John Doe', None) + >>> try: + ... rcs._u_parse_id("John Doe ") + ... except AssertionError: + ... print "Invalid match" + Invalid match + """ + emailexp = re.compile("(.*) <([^>]*)>(.*)") + match = emailexp.search(value) + if match == None: + email = None + name = value + else: + assert len(match.groups()) == 3 + assert match.groups()[2] == "", match.groups() + email = match.groups()[1] + name = match.groups()[0] + assert name != None + assert len(name) > 0 + return (name, email) + def _u_get_fallback_username(self): + name = None + for envariable in ["LOGNAME", "USERNAME"]: + if os.environ.has_key(envariable): + name = os.environ[envariable] + break + assert name != None + return name + def _u_get_fallback_email(self): + hostname = gethostname() + name = self._u_get_fallback_username() + return "%s@%s" % (name, hostname) + def _u_parse_commitfile(self, commitfile): + """ + Split the commitfile created in self.commit() back into + summary and header lines. + """ + f = codecs.open(commitfile, "r", self.encoding) + summary = f.readline() + body = f.read() + body.lstrip('\n') + if len(body) == 0: + body = None + f.close() + return (summary, body) + + +def setup_rcs_test_fixtures(testcase): + """Set up test fixtures for RCS test case.""" + testcase.rcs = testcase.Class() + testcase.dir = Dir() + testcase.dirname = testcase.dir.path + + rcs_not_supporting_uninitialized_user_id = [] + rcs_not_supporting_set_user_id = ["None", "hg"] + testcase.rcs_supports_uninitialized_user_id = ( + testcase.rcs.name not in rcs_not_supporting_uninitialized_user_id) + testcase.rcs_supports_set_user_id = ( + testcase.rcs.name not in rcs_not_supporting_set_user_id) + + if not testcase.rcs.installed(): + testcase.fail( + "%(name)s RCS not found" % vars(testcase.Class)) + + if testcase.Class.name != "None": + testcase.failIf( + testcase.rcs.detect(testcase.dirname), + "Detected %(name)s RCS before initialising" + % vars(testcase.Class)) + + testcase.rcs.init(testcase.dirname) + + +class RCSTestCase(unittest.TestCase): + """Test cases for base RCS class.""" + + Class = RCS + + def __init__(self, *args, **kwargs): + super(RCSTestCase, self).__init__(*args, **kwargs) + self.dirname = None + + def setUp(self): + super(RCSTestCase, self).setUp() + setup_rcs_test_fixtures(self) + + def tearDown(self): + del(self.rcs) + super(RCSTestCase, self).tearDown() + + def full_path(self, rel_path): + return os.path.join(self.dirname, rel_path) + + +class RCS_init_TestCase(RCSTestCase): + """Test cases for RCS.init method.""" + + def test_detect_should_succeed_after_init(self): + """Should detect RCS in directory after initialization.""" + self.failUnless( + self.rcs.detect(self.dirname), + "Did not detect %(name)s RCS after initialising" + % vars(self.Class)) + + def test_rcs_rootdir_in_specified_root_path(self): + """RCS root directory should be in specified root path.""" + rp = os.path.realpath(self.rcs.rootdir) + dp = os.path.realpath(self.dirname) + rcs_name = self.Class.name + self.failUnless( + dp == rp or rp == None, + "%(rcs_name)s RCS root in wrong dir (%(dp)s %(rp)s)" % vars()) + + +class RCS_get_user_id_TestCase(RCSTestCase): + """Test cases for RCS.get_user_id method.""" + + def test_gets_existing_user_id(self): + """Should get the existing user ID.""" + if not self.rcs_supports_uninitialized_user_id: + return + + user_id = self.rcs.get_user_id() + self.failUnless( + user_id is not None, + "unable to get a user id") + + +class RCS_set_user_id_TestCase(RCSTestCase): + """Test cases for RCS.set_user_id method.""" + + def setUp(self): + super(RCS_set_user_id_TestCase, self).setUp() + + if self.rcs_supports_uninitialized_user_id: + self.prev_user_id = self.rcs.get_user_id() + else: + self.prev_user_id = "Uninitialized identity " + + if self.rcs_supports_set_user_id: + self.test_new_user_id = "John Doe " + self.rcs.set_user_id(self.test_new_user_id) + + def tearDown(self): + if self.rcs_supports_set_user_id: + self.rcs.set_user_id(self.prev_user_id) + super(RCS_set_user_id_TestCase, self).tearDown() + + def test_raises_error_in_unsupported_vcs(self): + """Should raise an error in a VCS that doesn't support it.""" + if self.rcs_supports_set_user_id: + return + self.assertRaises( + SettingIDnotSupported, + self.rcs.set_user_id, "foo") + + def test_updates_user_id_in_supporting_rcs(self): + """Should update the user ID in an RCS that supports it.""" + if not self.rcs_supports_set_user_id: + return + user_id = self.rcs.get_user_id() + self.failUnlessEqual( + self.test_new_user_id, user_id, + "user id not set correctly (expected %s, got %s)" + % (self.test_new_user_id, user_id)) + + +def setup_rcs_revision_test_fixtures(testcase): + """Set up revision test fixtures for RCS test case.""" + testcase.test_dirs = ['a', 'a/b', 'c'] + for path in testcase.test_dirs: + testcase.rcs.mkdir(testcase.full_path(path)) + + testcase.test_files = ['a/text', 'a/b/text'] + + testcase.test_contents = { + 'rev_1': "Lorem ipsum", + 'uncommitted': "dolor sit amet", + } + + +class RCS_mkdir_TestCase(RCSTestCase): + """Test cases for RCS.mkdir method.""" + + def setUp(self): + super(RCS_mkdir_TestCase, self).setUp() + setup_rcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.rcs.recursive_remove(self.full_path(path)) + super(RCS_mkdir_TestCase, self).tearDown() + + def test_mkdir_creates_directory(self): + """Should create specified directory in filesystem.""" + for path in self.test_dirs: + full_path = self.full_path(path) + self.failUnless( + os.path.exists(full_path), + "path %(full_path)s does not exist" % vars()) + + +class RCS_commit_TestCase(RCSTestCase): + """Test cases for RCS.commit method.""" + + def setUp(self): + super(RCS_commit_TestCase, self).setUp() + setup_rcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.rcs.recursive_remove(self.full_path(path)) + super(RCS_commit_TestCase, self).tearDown() + + def test_file_contents_as_specified(self): + """Should set file contents as specified.""" + test_contents = self.test_contents['rev_1'] + for path in self.test_files: + full_path = self.full_path(path) + self.rcs.set_file_contents(full_path, test_contents) + current_contents = self.rcs.get_file_contents(full_path) + self.failUnlessEqual(test_contents, current_contents) + + def test_file_contents_as_committed(self): + """Should have file contents as specified after commit.""" + test_contents = self.test_contents['rev_1'] + for path in self.test_files: + full_path = self.full_path(path) + self.rcs.set_file_contents(full_path, test_contents) + revision = self.rcs.commit("Initial file contents.") + current_contents = self.rcs.get_file_contents(full_path) + self.failUnlessEqual(test_contents, current_contents) + + def test_file_contents_as_set_when_uncommitted(self): + """Should set file contents as specified after commit.""" + if not self.rcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.rcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.rcs.commit("Initial file contents.") + self.rcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + current_contents = self.rcs.get_file_contents(full_path) + self.failUnlessEqual( + self.test_contents['uncommitted'], current_contents) + + def test_revision_file_contents_as_committed(self): + """Should get file contents as committed to specified revision.""" + if not self.rcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.rcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.rcs.commit("Initial file contents.") + self.rcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + committed_contents = self.rcs.get_file_contents( + full_path, revision) + self.failUnlessEqual( + self.test_contents['rev_1'], committed_contents) + + +class RCS_duplicate_repo_TestCase(RCSTestCase): + """Test cases for RCS.duplicate_repo method.""" + + def setUp(self): + super(RCS_duplicate_repo_TestCase, self).setUp() + setup_rcs_revision_test_fixtures(self) + + def tearDown(self): + self.rcs.remove_duplicate_repo() + for path in reversed(sorted(self.test_dirs)): + self.rcs.recursive_remove(self.full_path(path)) + super(RCS_duplicate_repo_TestCase, self).tearDown() + + def test_revision_file_contents_as_committed(self): + """Should match file contents as committed to specified revision.""" + if not self.rcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.rcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.rcs.commit("Commit current status") + self.rcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + dup_repo_path = self.rcs.duplicate_repo(revision) + dup_file_path = os.path.join(dup_repo_path, path) + dup_file_contents = file(dup_file_path, 'rb').read() + self.failUnlessEqual( + self.test_contents['rev_1'], dup_file_contents) + self.rcs.remove_duplicate_repo() + + +def make_rcs_testcase_subclasses(rcs_class, namespace): + """Make RCSTestCase subclasses for rcs_class in the namespace.""" + rcs_testcase_classes = [ + c for c in ( + ob for ob in globals().values() if isinstance(ob, type)) + if issubclass(c, RCSTestCase)] + + for base_class in rcs_testcase_classes: + testcase_class_name = rcs_class.__name__ + base_class.__name__ + testcase_class_bases = (base_class,) + testcase_class_dict = dict(base_class.__dict__) + testcase_class_dict['Class'] = rcs_class + testcase_class = type( + testcase_class_name, testcase_class_bases, testcase_class_dict) + setattr(namespace, testcase_class_name, testcase_class) + + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/settings_object.py b/interfaces/web/Bugs-Everywhere-Web/libbe/settings_object.py new file mode 100644 index 0000000..dde247f --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/settings_object.py @@ -0,0 +1,417 @@ +# Bugs Everywhere - a distributed bugtracker +# Copyright (C) 2008-2009 W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +This module provides a base class implementing settings-dict based +property storage useful for BE objects with saved properties +(e.g. BugDir, Bug, Comment). For example usage, consider the +unittests at the end of the module. +""" + +import doctest +import unittest + +from properties import Property, doc_property, local_property, \ + defaulting_property, checked_property, fn_checked_property, \ + cached_property, primed_property, change_hook_property, \ + settings_property + + +class _Token (object): + """ + `Control' value class for properties. We want values that only + mean something to the settings_object module. + """ + pass + +class UNPRIMED (_Token): + "Property has not been primed." + pass + +class EMPTY (_Token): + """ + Property has been primed but has no user-set value, so use + default/generator value. + """ + pass + + +def prop_save_settings(self, old, new): + """ + The default action undertaken when a property changes. + """ + if self.sync_with_disk==True: + self.save_settings() + +def prop_load_settings(self): + """ + The default action undertaken when an UNPRIMED property is accessed. + """ + if self.sync_with_disk==True and self._settings_loaded==False: + self.load_settings() + else: + self._setup_saved_settings(flag_as_loaded=False) + +# Some name-mangling routines for pretty printing setting names +def setting_name_to_attr_name(self, name): + """ + Convert keys to the .settings dict into their associated + SavedSettingsObject attribute names. + >>> print setting_name_to_attr_name(None,"User-id") + user_id + """ + return name.lower().replace('-', '_') + +def attr_name_to_setting_name(self, name): + """ + The inverse of setting_name_to_attr_name. + >>> print attr_name_to_setting_name(None, "user_id") + User-id + """ + return name.capitalize().replace('_', '-') + + +def versioned_property(name, doc, + default=None, generator=None, + change_hook=prop_save_settings, + mutable=False, + primer=prop_load_settings, + allowed=None, check_fn=None, + settings_properties=[], + required_saved_properties=[], + require_save=False): + """ + Combine the common decorators in a single function. + + Use zero or one (but not both) of default or generator, since a + working default will keep the generator from functioning. Use the + default if you know what you want the default value to be at + 'coding time'. Use the generator if you can write a function to + determine a valid default at run time. If both default and + generator are None, then the property will be a defaulting + property which defaults to None. + + allowed and check_fn have a similar relationship, although you can + use both of these if you want. allowed compares the proposed + value against a list determined at 'coding time' and check_fn + allows more flexible comparisons to take place at run time. + + Set require_save to True if you want to save the default/generated + value for a property, to protect against future changes. E.g., we + currently expect all comments to be 'text/plain' but in the future + we may want to default to 'text/html'. If we don't want the old + comments to be interpreted as 'text/html', we would require that + the content type be saved. + + change_hook, primer, settings_properties, and + required_saved_properties are only options to get their defaults + into our local scope. Don't mess with them. + + Set mutable=True if: + * default is a mutable + * your generator function may return mutables + * you set change_hook and might have mutable property values + See the docstrings in libbe.properties for details on how each of + these cases are handled. + """ + settings_properties.append(name) + if require_save == True: + required_saved_properties.append(name) + def decorator(funcs): + fulldoc = doc + if default != None or generator == None: + defaulting = defaulting_property(default=default, null=EMPTY, + mutable_default=mutable) + fulldoc += "\n\nThis property defaults to %s." % default + if generator != None: + cached = cached_property(generator=generator, initVal=EMPTY, + mutable=mutable) + fulldoc += "\n\nThis property is generated with %s." % generator + if check_fn != None: + fn_checked = fn_checked_property(value_allowed_fn=check_fn) + fulldoc += "\n\nThis property is checked with %s." % check_fn + if allowed != None: + checked = checked_property(allowed=allowed) + fulldoc += "\n\nThe allowed values for this property are: %s." \ + % (', '.join(allowed)) + hooked = change_hook_property(hook=change_hook, mutable=mutable) + primed = primed_property(primer=primer, initVal=UNPRIMED) + settings = settings_property(name=name, null=UNPRIMED) + docp = doc_property(doc=fulldoc) + deco = hooked(primed(settings(docp(funcs)))) + if default != None or generator == None: + deco = defaulting(deco) + if generator != None: + deco = cached(deco) + if check_fn != None: + deco = fn_checked(deco) + if allowed != None: + deco = checked(deco) + return Property(deco) + return decorator + +class SavedSettingsObject(object): + + # Keep a list of properties that may be stored in the .settings dict. + #settings_properties = [] + + # A list of properties that we save to disk, even if they were + # never set (in which case we save the default value). This + # protects against future changes in default values. + #required_saved_properties = [] + + _setting_name_to_attr_name = setting_name_to_attr_name + _attr_name_to_setting_name = attr_name_to_setting_name + + def __init__(self): + self._settings_loaded = False + self.sync_with_disk = False + self.settings = {} + + def load_settings(self): + """Load the settings from disk.""" + # Override. Must call ._setup_saved_settings() after loading. + self.settings = {} + self._setup_saved_settings() + + def _setup_saved_settings(self, flag_as_loaded=True): + """ + To be run after setting self.settings up from disk. Marks all + settings as primed. + """ + for property in self.settings_properties: + if property not in self.settings: + self.settings[property] = EMPTY + elif self.settings[property] == UNPRIMED: + self.settings[property] = EMPTY + if flag_as_loaded == True: + self._settings_loaded = True + + def save_settings(self): + """Load the settings from disk.""" + # Override. Should save the dict output of ._get_saved_settings() + settings = self._get_saved_settings() + pass # write settings to disk.... + + def _get_saved_settings(self): + settings = {} + for k,v in self.settings.items(): + if v != None and v != EMPTY: + settings[k] = v + for k in self.required_saved_properties: + settings[k] = getattr(self, self._setting_name_to_attr_name(k)) + return settings + + def clear_cached_setting(self, setting=None): + "If setting=None, clear *all* cached settings" + if setting != None: + if hasattr(self, "_%s_cached_value" % setting): + delattr(self, "_%s_cached_value" % setting) + else: + for setting in settings_properties: + self.clear_cached_setting(setting) + + +class SavedSettingsObjectTests(unittest.TestCase): + def testSimpleProperty(self): + """Testing a minimal versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + # access missing setting + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(len(t.settings) == 0, len(t.settings)) + self.failUnless(t.content_type == None, t.content_type) + # accessing t.content_type triggers the priming, which runs + # t._setup_saved_settings, which fills out t.settings with + # EMPTY data. t._settings_loaded is still false though, since + # the default priming does not do any of the `official' loading + # that occurs in t.load_settings. + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + # load settings creates an EMPTY value in the settings array + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + # now we set a value + t.content_type = 5 + self.failUnless(t.settings["Content-type"] == 5, + t.settings["Content-type"]) + self.failUnless(t.content_type == 5, t.content_type) + self.failUnless(t.settings["Content-type"] == 5, + t.settings["Content-type"]) + # now we set another value + t.content_type = "text/plain" + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == "text/plain", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, + t._get_saved_settings()) + # now we clear to the post-primed value + t.content_type = EMPTY + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + def testDefaultingProperty(self): + """Testing a defaulting versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == {}, t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t.content_type == "text/html", + t.content_type) + self.failUnless(t.settings["Content-type"] == "text/html", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, + t._get_saved_settings()) + def testRequiredDefaultingProperty(self): + """Testing a required defaulting versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, + t._get_saved_settings()) + def testClassVersionedPropertyDefinition(self): + """Testing a class-specific _versioned property decorator""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + def _versioned_property(settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"]=required_saved_properties + return versioned_property(**kwargs) + @_versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, + t._get_saved_settings()) + def testMutableChangeHookedProperty(self): + """Testing a mutable change-hooked property""" + SAVES = [] + def prop_log_save_settings(self, old, new, saves=SAVES): + saves.append("'%s' -> '%s'" % (str(old), str(new))) + prop_save_settings(self, old, new) + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="List-type", + doc="A test property", + mutable=True, + change_hook=prop_log_save_settings, + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def list_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._settings_loaded == False, t._settings_loaded) + t.load_settings() + self.failUnless(SAVES == [], SAVES) + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.list_type == None, t.list_type) + self.failUnless(SAVES == [ + "'None' -> ''" + ], SAVES) + self.failUnless(t.settings["List-type"]==EMPTY,t.settings["List-type"]) + t.list_type = [] + self.failUnless(t.settings["List-type"] == [], t.settings["List-type"]) + self.failUnless(SAVES == [ + "'None' -> ''", + "'' -> '[]'" + ], SAVES) + t.list_type.append(5) + self.failUnless(SAVES == [ + "'None' -> ''", + "'' -> '[]'", + ], SAVES) + self.failUnless(t.settings["List-type"] == [5],t.settings["List-type"]) + self.failUnless(SAVES == [ # the append(5) has not yet been saved + "'None' -> ''", + "'' -> '[]'", + ], SAVES) + self.failUnless(t.list_type == [5], t.list_type) # <-get triggers saved + + self.failUnless(SAVES == [ # now the append(5) has been saved. + "'None' -> ''", + "'' -> '[]'", + "'[]' -> '[5]'" + ], SAVES) + +unitsuite=unittest.TestLoader().loadTestsFromTestCase(SavedSettingsObjectTests) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/tree.py b/interfaces/web/Bugs-Everywhere-Web/libbe/tree.py new file mode 100644 index 0000000..45ae085 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/tree.py @@ -0,0 +1,179 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2008-2009 W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import doctest + +class Tree(list): + """ + Construct + +-b---d-g + a-+ +-e + +-c-+-f-h-i + with + >>> i = Tree(); i.n = "i" + >>> h = Tree([i]); h.n = "h" + >>> f = Tree([h]); f.n = "f" + >>> e = Tree(); e.n = "e" + >>> c = Tree([f,e]); c.n = "c" + >>> g = Tree(); g.n = "g" + >>> d = Tree([g]); d.n = "d" + >>> b = Tree([d]); b.n = "b" + >>> a = Tree(); a.n = "a" + >>> a.append(c) + >>> a.append(b) + + >>> a.branch_len() + 5 + >>> a.sort(key=lambda node : -node.branch_len()) + >>> "".join([node.n for node in a.traverse()]) + 'acfhiebdg' + >>> a.sort(key=lambda node : node.branch_len()) + >>> "".join([node.n for node in a.traverse()]) + 'abdgcefhi' + >>> "".join([node.n for node in a.traverse(depth_first=False)]) + 'abcdefghi' + >>> for depth,node in a.thread(): + ... print "%*s" % (2*depth+1, node.n) + a + b + d + g + c + e + f + h + i + >>> for depth,node in a.thread(flatten=True): + ... print "%*s" % (2*depth+1, node.n) + a + b + d + g + c + e + f + h + i + >>> a.has_descendant(g) + True + >>> c.has_descendant(g) + False + >>> a.has_descendant(a) + False + >>> a.has_descendant(a, match_self=True) + True + """ + def __eq__(self, other): + return id(self) == id(other) + + def branch_len(self): + """ + Exhaustive search every time == SLOW. + + Use only on small trees, or reimplement by overriding + child-addition methods to allow accurate caching. + + For the tree + +-b---d-g + a-+ +-e + +-c-+-f-h-i + this method returns 5. + """ + if len(self) == 0: + return 1 + else: + return 1 + max([child.branch_len() for child in self]) + + def sort(self, *args, **kwargs): + """ + This method can be slow, e.g. on a branch_len() sort, since a + node at depth N from the root has it's branch_len() method + called N times. + """ + list.sort(self, *args, **kwargs) + for child in self: + child.sort(*args, **kwargs) + + def traverse(self, depth_first=True): + """ + Note: you might want to sort() your tree first. + """ + if depth_first == True: + yield self + for child in self: + for descendant in child.traverse(): + yield descendant + else: # breadth first, Wikipedia algorithm + # http://en.wikipedia.org/wiki/Breadth-first_search + queue = [self] + while len(queue) > 0: + node = queue.pop(0) + yield node + queue.extend(node) + + def thread(self, flatten=False): + """ + When flatten==False, the depth of any node is one greater than + the depth of its parent. That way the inheritance is + explicit, but you can end up with highly indented threads. + + When flatten==True, the depth of any node is only greater than + the depth of its parent when there is a branch, and the node + is not the last child. This can lead to ancestry ambiguity, + but keeps the total indentation down. E.g. + +-b +-b-c + a-+-c and a-+ + +-d-e-f +-d-e-f + would both produce (after sorting by branch_len()) + (0, a) + (1, b) + (1, c) + (0, d) + (0, e) + (0, f) + """ + stack = [] # ancestry of the current node + if flatten == True: + depthDict = {} + + for node in self.traverse(depth_first=True): + while len(stack) > 0 \ + and id(node) not in [id(c) for c in stack[-1]]: + stack.pop(-1) + if flatten == False: + depth = len(stack) + else: + if len(stack) == 0: + depth = 0 + else: + parent = stack[-1] + depth = depthDict[id(parent)] + if len(parent) > 1 and node != parent[-1]: + depth += 1 + depthDict[id(node)] = depth + yield (depth,node) + stack.append(node) + + def has_descendant(self, descendant, depth_first=True, match_self=False): + if descendant == self: + return match_self + for d in self.traverse(depth_first): + if descendant == d: + return True + return False + +suite = doctest.DocTestSuite() diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/utility.py b/interfaces/web/Bugs-Everywhere-Web/libbe/utility.py new file mode 100644 index 0000000..3df06b4 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/utility.py @@ -0,0 +1,129 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import calendar +import codecs +import os +import shutil +import tempfile +import time +import types +import doctest + +def search_parent_directories(path, filename): + """ + Find the file (or directory) named filename in path or in any + of path's parents. + + e.g. + search_parent_directories("/a/b/c", ".be") + will return the path to the first existing file from + /a/b/c/.be + /a/b/.be + /a/.be + /.be + or None if none of those files exist. + """ + path = os.path.realpath(path) + assert os.path.exists(path) + old_path = None + while True: + check_path = os.path.join(path, filename) + if os.path.exists(check_path): + return check_path + if path == old_path: + return None + old_path = path + path = os.path.dirname(path) + +class Dir (object): + "A temporary directory for testing use" + def __init__(self): + self.path = tempfile.mkdtemp(prefix="BEtest") + self.rmtree = shutil.rmtree # save local reference for __del__ + self.removed = False + def __del__(self): + self.cleanup() + def cleanup(self): + if self.removed == False: + self.rmtree(self.path) + self.removed = True + def __call__(self): + return self.path + +RFC_2822_TIME_FMT = "%a, %d %b %Y %H:%M:%S +0000" + + +def time_to_str(time_val): + """Convert a time value into an RFC 2822-formatted string. This format + lacks sub-second data. + >>> time_to_str(0) + 'Thu, 01 Jan 1970 00:00:00 +0000' + """ + return time.strftime(RFC_2822_TIME_FMT, time.gmtime(time_val)) + +def str_to_time(str_time): + """Convert an RFC 2822-fomatted string into a time value. + >>> str_to_time("Thu, 01 Jan 1970 00:00:00 +0000") + 0 + >>> q = time.time() + >>> str_to_time(time_to_str(q)) == int(q) + True + >>> str_to_time("Thu, 01 Jan 1970 00:00:00 -1000") + 36000 + """ + timezone_str = str_time[-5:] + if timezone_str != "+0000": + str_time = str_time.replace(timezone_str, "+0000") + time_val = calendar.timegm(time.strptime(str_time, RFC_2822_TIME_FMT)) + timesign = -int(timezone_str[0]+"1") # "+" -> time_val ahead of GMT + timezone_tuple = time.strptime(timezone_str[1:], "%H%M") + timezone = timezone_tuple.tm_hour*3600 + timezone_tuple.tm_min*60 + return time_val + timesign*timezone + +def handy_time(time_val): + return time.strftime("%a, %d %b %Y %H:%M", time.localtime(time_val)) + +def time_to_gmtime(str_time): + """Convert an RFC 2822-fomatted string to a GMT string. + >>> time_to_gmtime("Thu, 01 Jan 1970 00:00:00 -1000") + 'Thu, 01 Jan 1970 10:00:00 +0000' + """ + time_val = str_to_time(str_time) + return time_to_str(time_val) + +def iterable_full_of_strings(value, alternative=None): + """ + Require an iterable full of strings. + >>> iterable_full_of_strings([]) + True + >>> iterable_full_of_strings(["abc", "def", u"hij"]) + True + >>> iterable_full_of_strings(["abc", None, u"hij"]) + False + >>> iterable_full_of_strings(None, alternative=None) + True + """ + if value == alternative: + return True + elif not hasattr(value, "__iter__"): + return False + for x in value: + if type(x) not in types.StringTypes: + return False + return True + +suite = doctest.DocTestSuite() diff --git a/Bugs-Everywhere-Web/prod.cfg b/interfaces/web/Bugs-Everywhere-Web/prod.cfg similarity index 100% rename from Bugs-Everywhere-Web/prod.cfg rename to interfaces/web/Bugs-Everywhere-Web/prod.cfg diff --git a/Bugs-Everywhere-Web/sample-prod.cfg b/interfaces/web/Bugs-Everywhere-Web/sample-prod.cfg similarity index 100% rename from Bugs-Everywhere-Web/sample-prod.cfg rename to interfaces/web/Bugs-Everywhere-Web/sample-prod.cfg diff --git a/Bugs-Everywhere-Web/server.log b/interfaces/web/Bugs-Everywhere-Web/server.log similarity index 100% rename from Bugs-Everywhere-Web/server.log rename to interfaces/web/Bugs-Everywhere-Web/server.log diff --git a/Bugs-Everywhere-Web/setup-tables.py b/interfaces/web/Bugs-Everywhere-Web/setup-tables.py similarity index 100% rename from Bugs-Everywhere-Web/setup-tables.py rename to interfaces/web/Bugs-Everywhere-Web/setup-tables.py diff --git a/Bugs-Everywhere-Web/setup.py b/interfaces/web/Bugs-Everywhere-Web/setup.py similarity index 100% rename from Bugs-Everywhere-Web/setup.py rename to interfaces/web/Bugs-Everywhere-Web/setup.py diff --git a/Bugs-Everywhere-Web/start-beweb.py b/interfaces/web/Bugs-Everywhere-Web/start-beweb.py old mode 100755 new mode 100644 similarity index 100% rename from Bugs-Everywhere-Web/start-beweb.py rename to interfaces/web/Bugs-Everywhere-Web/start-beweb.py diff --git a/interfaces/xml/be-mbox-to-xml b/interfaces/xml/be-mbox-to-xml new file mode 100644 index 0000000..335f92f --- /dev/null +++ b/interfaces/xml/be-mbox-to-xml @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# Copyright (C) 2009 W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Convert an mbox into xml suitable for imput into be. + $ cat mbox | be-mbox-to-xml | be comment --xml - +mbox is a flat-file format, consisting of a series of messages. +Messages begin with a a From_ line, followed by RFC 822 email, +followed by a blank line. +""" + +import base64 +import email.utils +from libbe.encoding import get_encoding, set_IO_stream_encodings +from mailbox import mbox, Message # the mailbox people really want an on-disk copy +from time import asctime, gmtime +import types +from xml.sax import make_parser +from xml.sax.handler import ContentHandler +from xml.sax.saxutils import escape + +DEFAULT_ENCODING = get_encoding() +set_IO_stream_encodings(DEFAULT_ENCODING) + +KNOWN_IDS = [] + +def comment_message_to_xml(message, fields=None): + if fields == None: + fields = {} + new_fields = {} + new_fields[u'alt-id'] = message[u'message-id'] + new_fields[u'in-reply-to'] = message[u'in-reply-to'] + new_fields[u'from'] = message[u'from'] + new_fields[u'date'] = message[u'date'] + new_fields[u'content-type'] = message.get_content_type() + for k,v in new_fields.items(): + if v != None and type(v) != types.UnicodeType: + fields[k] = unicode(v, encoding=DEFAULT_ENCODING) + elif v == None and k in fields: + new_fields[k] = fields[k] + for k,v in fields.items(): + if k not in new_fields: + new_fields.k = fields[k] + fields = new_fields + + if fields[u'in-reply-to'] == None: + if message[u'references'] != None: + refs = message[u'references'].split() + for ref in refs: # search for a known reference id. + if ref in KNOWN_IDS: + fields[u'in-reply-to'] = ref + break + if fields[u'in-reply-to'] == None and len(refs) > 0: + fields[u'in-reply-to'] = refs[0] # default to the first + else: # check for mutliple in-reply-to references. + refs = fields[u'in-reply-to'].split() + for ref in refs: # search for a known reference id. + if ref in KNOWN_IDS: + fields[u'in-reply-to'] = ref + break + if fields[u'in-reply-to'] == None and len(refs) > 0: + fields[u'in-reply-to'] = refs[0] # default to the first + + if fields['alt-id'] != None: + KNOWN_IDS.append(fields['alt-id']) + + if message.is_multipart(): + ret = [] + alt_id = fields[u'alt-id'] + from_str = fields[u'from'] + date = fields[u'date'] + for m in message.walk(): + if m == message: + continue + fields[u'from'] = from_str + fields[u'date'] = date + if len(ret) > 0: # we've added one part already + fields.pop(u'alt-id') # don't pass alt-id to other parts + fields[u'in-reply-to'] = alt_id # others respond to first + ret.append(comment_message_to_xml(m, fields)) + return u'\n'.join(ret) + + charset = message.get_content_charset(DEFAULT_ENCODING).lower() + #assert charset == DEFAULT_ENCODING.lower(), \ + # u"Unknown charset: %s" % charset + + if message[u'content-transfer-encoding'] == None: + encoding = DEFAULT_ENCODING + else: + encoding = message[u'content-transfer-encoding'].lower() + body = message.get_payload(decode=True) # attempt to decode + assert body != None, "Unable to decode?" + if fields[u'content-type'].startswith(u"text/"): + body = unicode(body, encoding=charset).rstrip(u'\n') + else: + body = base64.encode(body) + fields[u'body'] = body + lines = [u""] + for tag,body in fields.items(): + if body != None: + ebody = escape(body) + lines.append(u" <%s>%s" % (tag, ebody, tag)) + lines.append(u"") + return u'\n'.join(lines) + +def main(mbox_filename): + mb = mbox(mbox_filename) + print u'' % DEFAULT_ENCODING + print u"" + for message in mb: + print comment_message_to_xml(message) + print u"" + + +if __name__ == "__main__": + import sys + main(sys.argv[1]) diff --git a/xml/be-xml-to-mbox b/interfaces/xml/be-xml-to-mbox old mode 100755 new mode 100644 similarity index 51% rename from xml/be-xml-to-mbox rename to interfaces/xml/be-xml-to-mbox index 80db634..ea77c34 --- a/xml/be-xml-to-mbox +++ b/interfaces/xml/be-xml-to-mbox @@ -2,19 +2,19 @@ # Copyright (C) 2009 Chris Ball # W. Trevor King # -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. # -# 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. +# 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. # -# 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 +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Convert xml output of `be list --xml` into mbox format for browsing with a mail reader. For example @@ -26,14 +26,16 @@ followed by a blank line. """ #from mailbox import mbox, Message # the mailbox people really want an on-disk copy +import codecs import email.utils -import types - from libbe.encoding import get_encoding, set_IO_stream_encodings from libbe.utility import str_to_time as rfc2822_to_gmtime_integer from time import asctime, gmtime -from xml.sax import make_parser -from xml.sax.handler import ContentHandler +import types +try: # import core module, Python >= 2.5 + from xml.etree import ElementTree +except ImportError: # look for non-core module + from elementtree import ElementTree from xml.sax.saxutils import unescape @@ -83,7 +85,7 @@ class Bug (LimitedAttrDict): u"created", u"summary", u"comments", - u"extra_strings"] + u"extra-strings"] def print_to_mbox(self): name,addr = email.utils.parseaddr(self["creator"]) print "From %s %s" % (addr, rfc2822_to_asctime(self["created"])) @@ -96,28 +98,62 @@ class Bug (LimitedAttrDict): print "" print self["summary"] print "" - if len(self["extra_strings"]) > 0: + if "extra-strings" in self: print "extra strings:\n ", print '\n '.join(self["extra_strings"]) print "" - for comment in self["comments"]: - comment.print_to_mbox(self) + if "comments" in self: + for comment in self["comments"]: + comment.print_to_mbox(self) + def init_from_etree(self, element): + assert element.tag == "bug", element.tag + for field in element.getchildren(): + text = unescape(unicode(field.text).decode("unicode_escape").strip()) + if field.tag == "comment": + comm = Comment() + comm.init_from_etree(field) + if "comments" in self: + self["comments"].append(comm) + else: + self["comments"] = [comm] + elif field.tag == "extra-string": + if "extra-strings" in self: + self["extra-strings"].append(text) + else: + self["extra-strings"] = [text] + else: + self[field.tag] = text class Comment (LimitedAttrDict): _attrs = [u"uuid", + u"alt-id", u"short-name", u"in-reply-to", u"from", u"date", u"content-type", u"body"] - def print_to_mbox(self, bug): + def print_to_mbox(self, bug=None): + if bug == None: + bug = Bug() + bug[u"uuid"] = u"no-uuid" name,addr = email.utils.parseaddr(self["from"]) print "From %s %s" % (addr, rfc2822_to_asctime(self["date"])) - print "Message-ID: <%s@%s>" % (self["uuid"], DEFAULT_DOMAIN) + if "uuid" in self: id = self["uuid"] + elif "alt-id" in self: id = self["alt-id"] + else: id = None + if id != None: + print "Message-ID: <%s@%s>" % (id, DEFAULT_DOMAIN) print "Date: %s" % self["date"] print "From: %s" % self["from"] - print "Subject: %s: %s" % (self["short-name"], bug["summary"]) + subject = "" + if "short-name" in self: + subject += self["short-name"]+u": " + if "summary" in bug: + subject += bug["summary"] + else: + subject += u"no-subject" + print "Subject: %s" % subject if "in-reply-to" not in self.keys(): self["in-reply-to"] = bug["uuid"] print "In-Reply-To: <%s@%s>" % (self["in-reply-to"], DEFAULT_DOMAIN) @@ -129,72 +165,41 @@ class Comment (LimitedAttrDict): else: # content type and transfer encoding already in XML MIME output print self["body"] print "" + def init_from_etree(self, element): + assert element.tag == "comment", element.tag + for field in element.getchildren(): + text = unescape(unicode(field.text).decode("unicode_escape").strip()) + if field.tag == "body": + text+="\n" + self[field.tag] = text -class BE_list_handler (ContentHandler): - def __init__(self): - self.reset() - - def reset(self): - self.bug = None - self.comment = None - self.extra_strings = None - self.text_field = None - - def startElement(self, name, attributes): - if name == "bug": - assert self.bug == None, "Nested bugs?!" - assert self.comment == None - assert self.text_field == None - self.bug = Bug(comments=[], extra_strings=[]) - elif name == "comment": - assert self.bug != None, " not in ?" - assert self.comment == None, "Nested comments?!" - assert self.text_field == None, " in text field %s?" % self.text_field - self.comment = Comment() - elif self.bug != None and self.comment == None: - # parse bug text field - self.text_field = name - self.text_data = "" - elif self.bug != None and self.comment != None: - # parse comment text field - self.text_field = name - self.text_data = "" - - def endElement(self, name): - if name == "bug": - assert self.bug != None, "Invalid XML?" - assert self.comment == None, "Invalid XML?" - assert self.text_field == None, "Invalid XML?" - self.bug.print_to_mbox() - self.bug = None - elif name == "comment": - assert self.bug != None, " not in ?" - assert self.comment != None, "Invalid XML?" - assert self.text_field == None, " in text field %s?" % self.text_field - self.bug["comments"].append(self.comment) - # comments printed by bug.print_to_mbox() - self.comment = None - elif self.bug != None and self.comment == None: - # parse bug text field - if self.text_field == "extra-string": - self.bug["extra_strings"].append(unescape(self.text_data.strip())) - else: - self.bug[self.text_field] = unescape(self.text_data.strip()) - self.text_field = None - self.text_data = None - elif self.bug != None and self.comment != None: - # parse comment text field - self.comment[self.text_field] = unescape(self.text_data.strip()) - self.text_field = None - self.text_data = None - - def characters(self, data): - if self.text_field != None: - self.text_data += data +def print_to_mbox(element): + if element.tag == "bug": + b = Bug() + b.init_from_etree(element) + b.print_to_mbox() + elif element.tag == "comment": + c = Comment() + c.init_from_etree(element) + c.print_to_mbox() + elif element.tag in ["bugs", "bug-list"]: + for b_elt in element.getchildren(): + b = Bug() + b.init_from_etree(b_elt) + b.print_to_mbox() + elif element.tag in ["comments", "comment-list"]: + for c_elt in element.getchildren(): + c = Comment() + c.init_from_etree(c_elt) + c.print_to_mbox() if __name__ == "__main__": import sys - - parser = make_parser() - parser.setContentHandler(BE_list_handler()) - parser.parse(sys.stdin) + + if len(sys.argv) == 1: # no filename given, use stdin + xml_unicode = sys.stdin.read() + else: + xml_unicode = codecs.open(sys.argv[1], "r", DEFAULT_ENCODING).read() + xml_str = xml_unicode.encode("unicode_escape").replace(r"\n", "\n") + elist = ElementTree.XML(xml_str) + print_to_mbox(elist) diff --git a/libbe/arch.py b/libbe/arch.py index 3051b34..e69de29 100644 --- a/libbe/arch.py +++ b/libbe/arch.py @@ -1,295 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Ben Finney -# James Rowe -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 codecs -import os -import re -import shutil -import sys -import time -import unittest -import doctest - -import config -from beuuid import uuid_gen -import rcs -from rcs import RCS - -DEFAULT_CLIENT = "tla" - -client = config.get_val("arch_client", default=DEFAULT_CLIENT) - -def new(): - return Arch() - -class Arch(RCS): - name = "Arch" - client = client - versioned = True - _archive_name = None - _archive_dir = None - _tmp_archive = False - _project_name = None - _tmp_project = False - _arch_paramdir = os.path.expanduser("~/.arch-params") - def _rcs_help(self): - status,output,error = self._u_invoke_client("--help") - return output - def _rcs_detect(self, path): - """Detect whether a directory is revision-controlled using Arch""" - if self._u_search_parent_directories(path, "{arch}") != None : - config.set_val("arch_client", client) - return True - return False - def _rcs_init(self, path): - self._create_archive(path) - self._create_project(path) - self._add_project_code(path) - def _create_archive(self, path): - # Create a new archive - # http://regexps.srparish.net/tutorial-tla/new-archive.html#Creating_a_New_Archive - assert self._archive_name == None - id = self.get_user_id() - name, email = self._u_parse_id(id) - if email == None: - email = "%s@example.com" % name - trailer = "%s-%s" % ("bugs-everywhere-auto", uuid_gen()[0:8]) - self._archive_name = "%s--%s" % (email, trailer) - self._archive_dir = "/tmp/%s" % trailer - self._tmp_archive = True - self._u_invoke_client("make-archive", self._archive_name, - self._archive_dir, directory=path) - def _invoke_client(self, *args, **kwargs): - """ - Invoke the client on our archive. - """ - assert self._archive_name != None - command = args[0] - if len(args) > 1: - tailargs = args[1:] - else: - tailargs = [] - arglist = [command, "-A", self._archive_name] - arglist.extend(tailargs) - args = tuple(arglist) - return self._u_invoke_client(*args, **kwargs) - def _remove_archive(self): - assert self._tmp_archive == True - assert self._archive_dir != None - assert self._archive_name != None - os.remove(os.path.join(self._arch_paramdir, - "=locations", self._archive_name)) - shutil.rmtree(self._archive_dir) - self._tmp_archive = False - self._archive_dir = False - self._archive_name = False - def _create_project(self, path): - """ - Create a temporary Arch project in the directory PATH. This - project will be removed by - __del__->cleanup->_rcs_cleanup->_remove_project - """ - # http://mwolson.org/projects/GettingStartedWithArch.html - # http://regexps.srparish.net/tutorial-tla/new-project.html#Starting_a_New_Project - category = "bugs-everywhere" - branch = "mainline" - version = "0.1" - self._project_name = "%s--%s--%s" % (category, branch, version) - self._invoke_client("archive-setup", self._project_name, - directory=path) - self._tmp_project = True - def _remove_project(self): - assert self._tmp_project == True - assert self._project_name != None - assert self._archive_dir != None - shutil.rmtree(os.path.join(self._archive_dir, self._project_name)) - self._tmp_project = False - self._project_name = False - def _archive_project_name(self): - assert self._archive_name != None - assert self._project_name != None - return "%s/%s" % (self._archive_name, self._project_name) - def _adjust_naming_conventions(self, path): - """ - By default, Arch restricts source code filenames to - ^[_=a-zA-Z0-9].*$ - See - http://regexps.srparish.net/tutorial-tla/naming-conventions.html - Since our bug directory '.be' doesn't satisfy these conventions, - we need to adjust them. - - The conventions are specified in - project-root/{arch}/=tagging-method - """ - tagpath = os.path.join(path, "{arch}", "=tagging-method") - lines_out = [] - f = codecs.open(tagpath, "r", self.encoding) - for line in f: - if line.startswith("source "): - lines_out.append("source ^[._=a-zA-X0-9].*$\n") - else: - lines_out.append(line) - f.close() - f = codecs.open(tagpath, "w", self.encoding) - f.write("".join(lines_out)) - f.close() - - def _add_project_code(self, path): - # http://mwolson.org/projects/GettingStartedWithArch.html - # http://regexps.srparish.net/tutorial-tla/new-source.html - # http://regexps.srparish.net/tutorial-tla/importing-first.html - self._invoke_client("init-tree", self._project_name, - directory=path) - self._adjust_naming_conventions(path) - self._invoke_client("import", "--summary", "Began versioning", - directory=path) - def _rcs_cleanup(self): - if self._tmp_project == True: - self._remove_project() - if self._tmp_archive == True: - self._remove_archive() - - def _rcs_root(self, path): - if not os.path.isdir(path): - dirname = os.path.dirname(path) - else: - dirname = path - status,output,error = self._u_invoke_client("tree-root", dirname) - root = output.rstrip('\n') - - self._get_archive_project_name(root) - - return root - - def _get_archive_name(self, root): - status,output,error = self._u_invoke_client("archives") - lines = output.split('\n') - # e.g. output: - # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52 - # /tmp/BEtestXXXXXX/rootdir - # (+ repeats) - for archive,location in zip(lines[::2], lines[1::2]): - if os.path.realpath(location) == os.path.realpath(root): - self._archive_name = archive - assert self._archive_name != None - - def _get_archive_project_name(self, root): - # get project names - status,output,error = self._u_invoke_client("tree-version", directory=root) - # e.g output - # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52/be--mainline--0.1 - archive_name,project_name = output.rstrip('\n').split('/') - self._archive_name = archive_name - self._project_name = project_name - def _rcs_get_user_id(self): - try: - status,output,error = self._u_invoke_client('my-id') - return output.rstrip('\n') - except Exception, e: - if 'no arch user id set' in e.args[0]: - return None - else: - raise - def _rcs_set_user_id(self, value): - self._u_invoke_client('my-id', value) - def _rcs_add(self, path): - self._u_invoke_client("add-id", path) - realpath = os.path.realpath(self._u_abspath(path)) - pathAdded = realpath in self._list_added(self.rootdir) - if self.paranoid and not pathAdded: - self._force_source(path) - def _list_added(self, root): - assert os.path.exists(root) - assert os.access(root, os.X_OK) - root = os.path.realpath(root) - status,output,error = self._u_invoke_client("inventory", "--source", - "--both", "--all", root) - inv_str = output.rstrip('\n') - return [os.path.join(root, p) for p in inv_str.split('\n')] - def _add_dir_rule(self, rule, dirname, root): - inv_path = os.path.join(dirname, '.arch-inventory') - f = codecs.open(inv_path, "a", self.encoding) - f.write(rule) - f.close() - if os.path.realpath(inv_path) not in self._list_added(root): - paranoid = self.paranoid - self.paranoid = False - self.add(inv_path) - self.paranoid = paranoid - def _force_source(self, path): - rule = "source %s\n" % self._u_rel_path(path) - self._add_dir_rule(rule, os.path.dirname(path), self.rootdir) - if os.path.realpath(path) not in self._list_added(self.rootdir): - raise CantAddFile(path) - def _rcs_remove(self, path): - if not '.arch-ids' in path: - self._u_invoke_client("delete-id", path) - def _rcs_update(self, path): - pass - def _rcs_get_file_contents(self, path, revision=None, binary=False): - if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) - else: - status,output,error = \ - self._invoke_client("file-find", path, revision) - relpath = output.rstrip('\n') - abspath = os.path.join(self.rootdir, relpath) - f = codecs.open(abspath, "r", self.encoding) - contents = f.read() - f.close() - return contents - def _rcs_duplicate_repo(self, directory, revision=None): - if revision == None: - RCS._rcs_duplicate_repo(self, directory, revision) - else: - status,output,error = \ - self._u_invoke_client("get", revision,directory) - def _rcs_commit(self, commitfile): - summary,body = self._u_parse_commitfile(commitfile) - #status,output,error = self._invoke_client("make-log") - if body == None: - status,output,error \ - = self._u_invoke_client("commit","--summary",summary) - else: - status,output,error \ - = self._u_invoke_client("commit","--summary",summary, - "--log-message",body) - revision = None - revline = re.compile("[*] committed (.*)") - match = revline.search(output) - assert match != None, output+error - assert len(match.groups()) == 1 - revpath = match.groups()[0] - assert not " " in revpath, revpath - assert revpath.startswith(self._archive_project_name()+'--') - revision = revpath[len(self._archive_project_name()+'--'):] - return revpath - -class CantAddFile(Exception): - def __init__(self, file): - self.file = file - Exception.__init__(self, "Can't automatically add file %s" % file) - - - -rcs.make_rcs_testcase_subclasses(Arch, sys.modules[__name__]) - -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/beuuid.py b/libbe/beuuid.py index de67cb7..e69de29 100644 --- a/libbe/beuuid.py +++ b/libbe/beuuid.py @@ -1,62 +0,0 @@ -# Copyright (C) 2008-2009 W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 -""" -Backwards compatibility support for Python 2.4. Once people give up -on 2.4 ;), the uuid call should be merged into bugdir.py -""" - -import unittest - -try: - from uuid import uuid4 # Python >= 2.5 - def uuid_gen(): - id = uuid4() - idstr = id.urn - start = "urn:uuid:" - assert idstr.startswith(start) - return idstr[len(start):] -except ImportError: - import os - import sys - from subprocess import Popen, PIPE - - def uuid_gen(): - # Shell-out to system uuidgen - args = ['uuidgen', 'r'] - try: - if sys.platform != "win32": - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) - else: - # win32 don't have os.execvp() so have to run command in a shell - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, - shell=True, cwd=cwd) - except OSError, e : - strerror = "%s\nwhile executing %s" % (e.args[1], args) - raise OSError, strerror - output, error = q.communicate() - status = q.wait() - if status != 0: - strerror = "%s\nwhile executing %s" % (status, args) - raise Exception, strerror - return output.rstrip('\n') - -class UUIDtestCase(unittest.TestCase): - def testUUID_gen(self): - id = uuid_gen() - self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) - -suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) diff --git a/libbe/bug.py b/libbe/bug.py index dfa49f2..e69de29 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -1,527 +0,0 @@ -# Copyright (C) 2008-2009 Chris Ball -# Thomas Habets -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 -import os.path -import errno -import time -import types -import xml.sax.saxutils -import doctest - -from beuuid import uuid_gen -from properties import Property, doc_property, local_property, \ - defaulting_property, checked_property, cached_property, \ - primed_property, change_hook_property, settings_property -import settings_object -import mapfile -import comment -import utility - - -### Define and describe valid bug categories -# Use a tuple of (category, description) tuples since we don't have -# ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/ - -# in order of increasing severity. (name, description) pairs -severity_def = ( - ("wishlist","A feature that could improve usefulness, but not a bug."), - ("minor","The standard bug level."), - ("serious","A bug that requires workarounds."), - ("critical","A bug that prevents some features from working at all."), - ("fatal","A bug that makes the package unusable.")) - -# in order of increasing resolution -# roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html -active_status_def = ( - ("unconfirmed","A possible bug which lacks independent existance confirmation."), - ("open","A working bug that has not been assigned to a developer."), - ("assigned","A working bug that has been assigned to a developer."), - ("test","The code has been adjusted, but the fix is still being tested.")) -inactive_status_def = ( - ("closed", "The bug is no longer relevant."), - ("fixed", "The bug should no longer occur."), - ("wontfix","It's not a bug, it's a feature.")) - - -### Convert the description tuples to more useful formats - -severity_values = () -severity_description = {} -severity_index = {} -def load_severities(severity_def): - global severity_values - global severity_description - global severity_index - if severity_def == settings_object.EMPTY: - return - severity_values = tuple([val for val,description in severity_def]) - severity_description = dict(severity_def) - severity_index = {} - for i,severity in enumerate(severity_values): - severity_index[severity] = i -load_severities(severity_def) - -active_status_values = [] -inactive_status_values = [] -status_values = [] -status_description = {} -status_index = {} -def load_status(active_status_def, inactive_status_def): - global active_status_values - global inactive_status_values - global status_values - global status_description - global status_index - if active_status_def == settings_object.EMPTY: - active_status_def = globals()["active_status_def"] - if inactive_status_def == settings_object.EMPTY: - inactive_status_def = globals()["inactive_status_def"] - active_status_values = tuple([val for val,description in active_status_def]) - inactive_status_values = tuple([val for val,description in inactive_status_def]) - status_values = active_status_values + inactive_status_values - status_description = dict(tuple(active_status_def) + tuple(inactive_status_def)) - status_index = {} - for i,status in enumerate(status_values): - status_index[status] = i -load_status(active_status_def, inactive_status_def) - - -class Bug(settings_object.SavedSettingsObject): - """ - >>> b = Bug() - >>> print b.status - open - >>> print b.severity - minor - - There are two formats for time, int and string. Setting either - one will adjust the other appropriately. The string form is the - one stored in the bug's settings file on disk. - >>> print type(b.time) - - >>> print type(b.time_string) - - >>> b.time = 0 - >>> print b.time_string - Thu, 01 Jan 1970 00:00:00 +0000 - >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000" - >>> b.time - 60 - >>> print b.settings["time"] - Thu, 01 Jan 1970 00:01:00 +0000 - """ - settings_properties = [] - required_saved_properties = [] - _prop_save_settings = settings_object.prop_save_settings - _prop_load_settings = settings_object.prop_load_settings - def _versioned_property(settings_properties=settings_properties, - required_saved_properties=required_saved_properties, - **kwargs): - if "settings_properties" not in kwargs: - kwargs["settings_properties"] = settings_properties - if "required_saved_properties" not in kwargs: - kwargs["required_saved_properties"]=required_saved_properties - return settings_object.versioned_property(**kwargs) - - @_versioned_property(name="severity", - doc="A measure of the bug's importance", - default="minor", - check_fn=lambda s: s in severity_values, - require_save=True) - def severity(): return {} - - @_versioned_property(name="status", - doc="The bug's current status", - default="open", - check_fn=lambda s: s in status_values, - require_save=True) - def status(): return {} - - @property - def active(self): - return self.status in active_status_values - - @_versioned_property(name="target", - doc="The deadline for fixing this bug") - def target(): return {} - - @_versioned_property(name="creator", - doc="The user who entered the bug into the system") - def creator(): return {} - - @_versioned_property(name="reporter", - doc="The user who reported the bug") - def reporter(): return {} - - @_versioned_property(name="assigned", - doc="The developer in charge of the bug") - def assigned(): return {} - - @_versioned_property(name="time", - doc="An RFC 2822 timestamp for bug creation") - def time_string(): return {} - - def _get_time(self): - if self.time_string == None or self.time_string == settings_object.EMPTY: - return None - return utility.str_to_time(self.time_string) - def _set_time(self, value): - self.time_string = utility.time_to_str(value) - time = property(fget=_get_time, - fset=_set_time, - doc="An integer version of .time_string") - - def _extra_strings_check_fn(value): - "Require an iterable full of strings" - if value == settings_object.EMPTY: - return True - elif not hasattr(value, "__iter__"): - return False - for x in value: - if type(x) not in types.StringTypes: - return False - return True - def _extra_strings_change_hook(self, old, new): - self.extra_strings.sort() # to make merging easier - self._prop_save_settings(old, new) - @_versioned_property(name="extra_strings", - doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/.py.", - default=[], - check_fn=_extra_strings_check_fn, - change_hook=_extra_strings_change_hook, - mutable=True) - def extra_strings(): return {} - - @_versioned_property(name="summary", - doc="A one-line bug description") - def summary(): return {} - - def _get_comment_root(self, load_full=False): - if self.sync_with_disk: - return comment.loadComments(self, load_full=load_full) - else: - return comment.Comment(self, uuid=comment.INVALID_UUID) - - @Property - @cached_property(generator=_get_comment_root) - @local_property("comment_root") - @doc_property(doc="The trunk of the comment tree") - def comment_root(): return {} - - def _get_rcs(self): - if hasattr(self.bugdir, "rcs"): - return self.bugdir.rcs - - @Property - @cached_property(generator=_get_rcs) - @local_property("rcs") - @doc_property(doc="A revision control system instance.") - def rcs(): return {} - - def __init__(self, bugdir=None, uuid=None, from_disk=False, - load_comments=False, summary=None): - settings_object.SavedSettingsObject.__init__(self) - self.bugdir = bugdir - self.uuid = uuid - if from_disk == True: - self.sync_with_disk = True - else: - self.sync_with_disk = False - if uuid == None: - self.uuid = uuid_gen() - self.time = int(time.time()) # only save to second precision - if self.rcs != None: - self.creator = self.rcs.get_user_id() - self.summary = summary - - def __repr__(self): - return "Bug(uuid=%r)" % self.uuid - - def _setting_attr_string(self, setting): - value = getattr(self, setting) - if value == settings_object.EMPTY: - return "" - else: - return str(value) - - def xml(self, show_comments=False): - if self.bugdir == None: - shortname = self.uuid - else: - shortname = self.bugdir.bug_shortname(self) - - if self.time == None: - timestring = "" - else: - timestring = utility.time_to_str(self.time) - - info = [("uuid", self.uuid), - ("short-name", shortname), - ("severity", self.severity), - ("status", self.status), - ("assigned", self.assigned), - ("target", self.target), - ("reporter", self.reporter), - ("creator", self.creator), - ("created", timestring), - ("summary", self.summary)] - ret = '\n' - for (k,v) in info: - if v is not settings_object.EMPTY: - ret += ' <%s>%s\n' % (k,xml.sax.saxutils.escape(v),k) - for estr in self.extra_strings: - ret += ' %s\n' % estr - if show_comments == True: - comout = self.comment_root.xml_thread(auto_name_map=True, - bug_shortname=shortname) - if len(comout) > 0: - ret += comout+'\n' - ret += '' - return ret - - def string(self, shortlist=False, show_comments=False): - if self.bugdir == None: - shortname = self.uuid - else: - shortname = self.bugdir.bug_shortname(self) - if shortlist == False: - if self.time == None: - timestring = "" - else: - htime = utility.handy_time(self.time) - timestring = "%s (%s)" % (htime, self.time_string) - info = [("ID", self.uuid), - ("Short name", shortname), - ("Severity", self.severity), - ("Status", self.status), - ("Assigned", self._setting_attr_string("assigned")), - ("Target", self._setting_attr_string("target")), - ("Reporter", self._setting_attr_string("reporter")), - ("Creator", self._setting_attr_string("creator")), - ("Created", timestring)] - longest_key_len = max([len(k) for k,v in info]) - infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info] - bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n') - else: - statuschar = self.status[0] - severitychar = self.severity[0] - chars = "%c%c" % (statuschar, severitychar) - bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n')) - - if show_comments == True: - # take advantage of the string_thread(auto_name_map=True) - # SIDE-EFFECT of sorting by comment time. - comout = self.comment_root.string_thread(flatten=False, - auto_name_map=True, - bug_shortname=shortname) - output = bugout + '\n' + comout.rstrip('\n') - else : - output = bugout - return output - - def __str__(self): - return self.string(shortlist=True) - - def __cmp__(self, other): - return cmp_full(self, other) - - def get_path(self, name=None): - my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid) - if name is None: - return my_dir - assert name in ["values", "comments"] - return os.path.join(my_dir, name) - - def load_settings(self): - self.settings = mapfile.map_load(self.rcs, self.get_path("values")) - self._setup_saved_settings() - - def load_comments(self, load_full=True): - if load_full == True: - # Force a complete load of the whole comment tree - self.comment_root = self._get_comment_root(load_full=True) - else: - # Setup for fresh lazy-loading. Clear _comment_root, so - # _get_comment_root returns a fresh version. Turn of - # syncing temporarily so we don't write our blank comment - # tree to disk. - self.sync_with_disk = False - self.comment_root = None - self.sync_with_disk = True - - def save_settings(self): - assert self.summary != None, "Can't save blank bug" - - self.rcs.mkdir(self.get_path()) - path = self.get_path("values") - mapfile.map_save(self.rcs, path, self._get_saved_settings()) - - def save(self): - self.save_settings() - - if len(self.comment_root) > 0: - self.rcs.mkdir(self.get_path("comments")) - comment.saveComments(self) - - def remove(self): - self.comment_root.remove() - path = self.get_path() - self.rcs.recursive_remove(path) - - def comments(self): - for comment in self.comment_root.traverse(): - yield comment - - def new_comment(self, body=None): - comm = self.comment_root.new_reply(body=body) - return comm - - def comment_from_shortname(self, shortname, *args, **kwargs): - return self.comment_root.comment_from_shortname(shortname, - *args, **kwargs) - - def comment_from_uuid(self, uuid): - return self.comment_root.comment_from_uuid(uuid) - - def comment_shortnames(self, shortname=None): - """ - SIDE-EFFECT : Comment.comment_shortnames will sort the comment - tree by comment.time - """ - for id, comment in self.comment_root.comment_shortnames(shortname): - yield (id, comment) - - -# The general rule for bug sorting is that "more important" bugs are -# less than "less important" bugs. This way sorting a list of bugs -# will put the most important bugs first in the list. When relative -# importance is unclear, the sorting follows some arbitrary convention -# (i.e. dictionary order). - -def cmp_severity(bug_1, bug_2): - """ - Compare the severity levels of two bugs, with more severe bugs - comparing as less. - >>> bugA = Bug() - >>> bugB = Bug() - >>> bugA.severity = bugB.severity = "wishlist" - >>> cmp_severity(bugA, bugB) == 0 - True - >>> bugB.severity = "minor" - >>> cmp_severity(bugA, bugB) > 0 - True - >>> bugA.severity = "critical" - >>> cmp_severity(bugA, bugB) < 0 - True - """ - if not hasattr(bug_2, "severity") : - return 1 - return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity]) - -def cmp_status(bug_1, bug_2): - """ - Compare the status levels of two bugs, with more 'open' bugs - comparing as less. - >>> bugA = Bug() - >>> bugB = Bug() - >>> bugA.status = bugB.status = "open" - >>> cmp_status(bugA, bugB) == 0 - True - >>> bugB.status = "closed" - >>> cmp_status(bugA, bugB) < 0 - True - >>> bugA.status = "fixed" - >>> cmp_status(bugA, bugB) > 0 - True - """ - if not hasattr(bug_2, "status") : - return 1 - val_2 = status_index[bug_2.status] - return cmp(status_index[bug_1.status], status_index[bug_2.status]) - -def cmp_attr(bug_1, bug_2, attr, invert=False): - """ - Compare a general attribute between two bugs using the conventional - comparison rule for that attribute type. If invert == True, sort - *against* that convention. - >>> attr="severity" - >>> bugA = Bug() - >>> bugB = Bug() - >>> bugA.severity = "critical" - >>> bugB.severity = "wishlist" - >>> cmp_attr(bugA, bugB, attr) < 0 - True - >>> cmp_attr(bugA, bugB, attr, invert=True) > 0 - True - >>> bugB.severity = "critical" - >>> cmp_attr(bugA, bugB, attr) == 0 - True - """ - if not hasattr(bug_2, attr) : - return 1 - val_1 = getattr(bug_1, attr) - val_2 = getattr(bug_2, attr) - if val_1 == settings_object.EMPTY: val_1 = None - if val_2 == settings_object.EMPTY: val_2 = None - - if invert == True : - return -cmp(val_1, val_2) - else : - return cmp(val_1, val_2) - -# alphabetical rankings (a < z) -cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator") -cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned") -# chronological rankings (newer < older) -cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True) - -DEFAULT_CMP_FULL_CMP_LIST = \ - (cmp_status,cmp_severity,cmp_assigned,cmp_time,cmp_creator) - -class BugCompoundComparator (object): - def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): - self.cmp_list = cmp_list - def __call__(self, bug_1, bug_2): - for comparison in self.cmp_list : - val = comparison(bug_1, bug_2) - if val != 0 : - return val - return 0 - -cmp_full = BugCompoundComparator() - - -# define some bonus cmp_* functions -def cmp_last_modified(bug_1, bug_2): - """ - Like cmp_time(), but use most recent comment instead of bug - creation for the timestamp. - """ - def last_modified(bug): - time = bug.time - for comment in bug.comment_root.traverse(): - if comment.time > time: - time = comment.time - return time - val_1 = last_modified(bug_1) - val_2 = last_modified(bug_2) - return -cmp(val_1, val_2) - - -suite = doctest.DocTestSuite() diff --git a/libbe/bugdir.py b/libbe/bugdir.py index d4a39cb..e69de29 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -1,628 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Alexander Belchenko -# Chris Ball -# Oleg Romanyshyn -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 -import os.path -import errno -import time -import copy -import unittest -import doctest - -from properties import Property, doc_property, local_property, \ - defaulting_property, checked_property, fn_checked_property, \ - cached_property, primed_property, change_hook_property, \ - settings_property -import settings_object -import mapfile -import bug -import rcs -import encoding -import utility - - -class NoBugDir(Exception): - def __init__(self, path): - msg = "The directory \"%s\" has no bug directory." % path - Exception.__init__(self, msg) - self.path = path - -class NoRootEntry(Exception): - def __init__(self, path): - self.path = path - Exception.__init__(self, "Specified root does not exist: %s" % path) - -class AlreadyInitialized(Exception): - def __init__(self, path): - self.path = path - Exception.__init__(self, - "Specified root is already initialized: %s" % path) - -class MultipleBugMatches(ValueError): - def __init__(self, shortname, matches): - msg = ("More than one bug matches %s. " - "Please be more specific.\n%s" % (shortname, matches)) - ValueError.__init__(self, msg) - self.shortname = shortname - self.matches = matches - - -TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n" - - -class BugDir (list, settings_object.SavedSettingsObject): - """ - Sink to existing root - ====================== - - Consider the following usage case: - You have a bug directory rooted in - /path/to/source - by which I mean the '.be' directory is at - /path/to/source/.be - However, you're of in some subdirectory like - /path/to/source/GUI/testing - and you want to comment on a bug. Setting sink_to_root=True wen - you initialize your BugDir will cause it to search for the '.be' - file in the ancestors of the path you passed in as 'root'. - /path/to/source/GUI/testing/.be miss - /path/to/source/GUI/.be miss - /path/to/source/.be hit! - So it still roots itself appropriately without much work for you. - - File-system access - ================== - - When rooted in non-bugdir directory, BugDirs live completely in - memory until the first call to .save(). This creates a '.be' - sub-directory containing configurations options, bugs, comments, - etc. Once this sub-directory has been created (possibly by - another BugDir instance) any changes to the BugDir in memory will - be flushed to the file system automatically. However, the BugDir - will only load information from the file system when it loads new - bugs/comments that it doesn't already have in memory, or when it - explicitly asked to do so (e.g. .load() or __init__(from_disk=True)). - - Allow RCS initialization - ======================== - - This one is for testing purposes. Setting it to True allows the - BugDir to search for an installed RCS backend and initialize it in - the root directory. This is a convenience option for supporting - tests of versioning functionality (e.g. .duplicate_bugdir). - - Disable encoding manipulation - ============================= - - This one is for testing purposed. You might have non-ASCII - Unicode in your bugs, comments, files, etc. BugDir instances try - and support your preferred encoding scheme (e.g. "utf-8") when - dealing with stream and file input/output. For stream output, - this involves replacing sys.stdout and sys.stderr - (libbe.encode.set_IO_stream_encodings). However this messes up - doctest's output catching. In order to support doctest tests - using BugDirs, set manipulate_encodings=False, and stick to ASCII - in your tests. - """ - - settings_properties = [] - required_saved_properties = [] - _prop_save_settings = settings_object.prop_save_settings - _prop_load_settings = settings_object.prop_load_settings - def _versioned_property(settings_properties=settings_properties, - required_saved_properties=required_saved_properties, - **kwargs): - if "settings_properties" not in kwargs: - kwargs["settings_properties"] = settings_properties - if "required_saved_properties" not in kwargs: - kwargs["required_saved_properties"]=required_saved_properties - return settings_object.versioned_property(**kwargs) - - @_versioned_property(name="target", - doc="The current project development target") - def target(): return {} - - def _guess_encoding(self): - return encoding.get_encoding() - def _check_encoding(value): - if value != None and value != settings_object.EMPTY: - return encoding.known_encoding(value) - def _setup_encoding(self, new_encoding): - if new_encoding != None and new_encoding != settings_object.EMPTY: - if self._manipulate_encodings == True: - encoding.set_IO_stream_encodings(new_encoding) - def _set_encoding(self, old_encoding, new_encoding): - self._setup_encoding(new_encoding) - self._prop_save_settings(old_encoding, new_encoding) - - @_versioned_property(name="encoding", - doc="""The default input/output encoding to use (e.g. "utf-8").""", - change_hook=_set_encoding, - generator=_guess_encoding, - check_fn=_check_encoding) - def encoding(): return {} - - def _setup_user_id(self, user_id): - self.rcs.user_id = user_id - def _guess_user_id(self): - return self.rcs.get_user_id() - def _set_user_id(self, old_user_id, new_user_id): - self._setup_user_id(new_user_id) - self._prop_save_settings(old_user_id, new_user_id) - - @_versioned_property(name="user_id", - doc= -"""The user's prefered name, e.g. 'John Doe '. Note -that the Arch RCS backend *enforces* ids with this format.""", - change_hook=_set_user_id, - generator=_guess_user_id) - def user_id(): return {} - - @_versioned_property(name="default_assignee", - doc= -"""The default assignee for new bugs e.g. 'John Doe '.""") - def default_assignee(): return {} - - @_versioned_property(name="rcs_name", - doc="""The name of the current RCS. Kept seperate to make saving/loading -settings easy. Don't set this attribute. Set .rcs instead, and -.rcs_name will be automatically adjusted.""", - default="None", - allowed=["None", "Arch", "bzr", "darcs", "git", "hg"]) - def rcs_name(): return {} - - def _get_rcs(self, rcs_name=None): - """Get and root a new revision control system""" - if rcs_name == None: - rcs_name = self.rcs_name - new_rcs = rcs.rcs_by_name(rcs_name) - self._change_rcs(None, new_rcs) - return new_rcs - def _change_rcs(self, old_rcs, new_rcs): - new_rcs.encoding = self.encoding - new_rcs.root(self.root) - self.rcs_name = new_rcs.name - - @Property - @change_hook_property(hook=_change_rcs) - @cached_property(generator=_get_rcs) - @local_property("rcs") - @doc_property(doc="A revision control system instance.") - def rcs(): return {} - - def _bug_map_gen(self): - map = {} - for bug in self: - map[bug.uuid] = bug - for uuid in self.list_uuids(): - if uuid not in map: - map[uuid] = None - self._bug_map_value = map # ._bug_map_value used by @local_property - - @Property - @primed_property(primer=_bug_map_gen) - @local_property("bug_map") - @doc_property(doc="A dict of (bug-uuid, bug-instance) pairs.") - def _bug_map(): return {} - - def _setup_severities(self, severities): - if severities != None and severities != settings_object.EMPTY: - bug.load_severities(severities) - def _set_severities(self, old_severities, new_severities): - self._setup_severities(new_severities) - self._prop_save_settings(old_severities, new_severities) - @_versioned_property(name="severities", - doc="The allowed bug severities and their descriptions.", - change_hook=_set_severities) - def severities(): return {} - - def _setup_status(self, active_status, inactive_status): - bug.load_status(active_status, inactive_status) - def _set_active_status(self, old_active_status, new_active_status): - self._setup_status(new_active_status, self.inactive_status) - self._prop_save_settings(old_active_status, new_active_status) - @_versioned_property(name="active_status", - doc="The allowed active bug states and their descriptions.", - change_hook=_set_active_status) - def active_status(): return {} - - def _set_inactive_status(self, old_inactive_status, new_inactive_status): - self._setup_status(self.active_status, new_inactive_status) - self._prop_save_settings(old_inactive_status, new_inactive_status) - @_versioned_property(name="inactive_status", - doc="The allowed inactive bug states and their descriptions.", - change_hook=_set_inactive_status) - def inactive_status(): return {} - - - def __init__(self, root=None, sink_to_existing_root=True, - assert_new_BugDir=False, allow_rcs_init=False, - manipulate_encodings=True, - from_disk=False, rcs=None): - list.__init__(self) - settings_object.SavedSettingsObject.__init__(self) - self._manipulate_encodings = manipulate_encodings - if root == None: - root = os.getcwd() - if sink_to_existing_root == True: - self.root = self._find_root(root) - else: - if not os.path.exists(root): - raise NoRootEntry(root) - self.root = root - # get a temporary rcs until we've loaded settings - self.sync_with_disk = False - self.rcs = self._guess_rcs() - - if from_disk == True: - self.sync_with_disk = True - self.load() - else: - self.sync_with_disk = False - if assert_new_BugDir == True: - if os.path.exists(self.get_path()): - raise AlreadyInitialized, self.get_path() - if rcs == None: - rcs = self._guess_rcs(allow_rcs_init) - self.rcs = rcs - self._setup_user_id(self.user_id) - - def _find_root(self, path): - """ - Search for an existing bug database dir and it's ancestors and - return a BugDir rooted there. - """ - if not os.path.exists(path): - raise NoRootEntry(path) - versionfile=utility.search_parent_directories(path, - os.path.join(".be", "version")) - if versionfile != None: - beroot = os.path.dirname(versionfile) - root = os.path.dirname(beroot) - return root - else: - beroot = utility.search_parent_directories(path, ".be") - if beroot == None: - raise NoBugDir(path) - return beroot - - def get_version(self, path=None, use_none_rcs=False): - if use_none_rcs == True: - RCS = rcs.rcs_by_name("None") - RCS.root(self.root) - RCS.encoding = encoding.get_encoding() - else: - RCS = self.rcs - - if path == None: - path = self.get_path("version") - tree_version = RCS.get_file_contents(path) - return tree_version - - def set_version(self): - self.rcs.set_file_contents(self.get_path("version"), - TREE_VERSION_STRING) - - def get_path(self, *args): - my_dir = os.path.join(self.root, ".be") - if len(args) == 0: - return my_dir - assert args[0] in ["version", "settings", "bugs"], str(args) - return os.path.join(my_dir, *args) - - def _guess_rcs(self, allow_rcs_init=False): - deepdir = self.get_path() - if not os.path.exists(deepdir): - deepdir = os.path.dirname(deepdir) - new_rcs = rcs.detect_rcs(deepdir) - install = False - if new_rcs.name == "None": - if allow_rcs_init == True: - new_rcs = rcs.installed_rcs() - new_rcs.init(self.root) - return new_rcs - - def load(self): - version = self.get_version(use_none_rcs=True) - if version != TREE_VERSION_STRING: - raise NotImplementedError, \ - "BugDir cannot handle version '%s' yet." % version - else: - if not os.path.exists(self.get_path()): - raise NoBugDir(self.get_path()) - self.load_settings() - - self.rcs = rcs.rcs_by_name(self.rcs_name) - self._setup_user_id(self.user_id) - - def load_all_bugs(self): - "Warning: this could take a while." - self._clear_bugs() - for uuid in self.list_uuids(): - self._load_bug(uuid) - - def save(self): - self.rcs.mkdir(self.get_path()) - self.set_version() - self.save_settings() - self.rcs.mkdir(self.get_path("bugs")) - for bug in self: - bug.save() - - def load_settings(self): - self.settings = self._get_settings(self.get_path("settings")) - self._setup_saved_settings() - self._setup_user_id(self.user_id) - self._setup_encoding(self.encoding) - self._setup_severities(self.severities) - self._setup_status(self.active_status, self.inactive_status) - - def _get_settings(self, settings_path): - allow_no_rcs = not self.rcs.path_in_root(settings_path) - # allow_no_rcs=True should only be for the special case of - # configuring duplicate bugdir settings - - try: - settings = mapfile.map_load(self.rcs, settings_path, allow_no_rcs) - except rcs.NoSuchFile: - settings = {"rcs_name": "None"} - return settings - - def save_settings(self): - settings = self._get_saved_settings() - self._save_settings(self.get_path("settings"), settings) - - def _save_settings(self, settings_path, settings): - allow_no_rcs = not self.rcs.path_in_root(settings_path) - # allow_no_rcs=True should only be for the special case of - # configuring duplicate bugdir settings - mapfile.map_save(self.rcs, settings_path, settings, allow_no_rcs) - - def duplicate_bugdir(self, revision): - duplicate_path = self.rcs.duplicate_repo(revision) - - # setup revision RCS as None, since the duplicate may not be - # initialized for versioning - duplicate_settings_path = os.path.join(duplicate_path, - ".be", "settings") - duplicate_settings = self._get_settings(duplicate_settings_path) - if "rcs_name" in duplicate_settings: - duplicate_settings["rcs_name"] = "None" - duplicate_settings["user_id"] = self.user_id - if "disabled" in bug.status_values: - # Hack to support old versions of BE bugs - duplicate_settings["inactive_status"] = self.inactive_status - self._save_settings(duplicate_settings_path, duplicate_settings) - - return BugDir(duplicate_path, from_disk=True, manipulate_encodings=self._manipulate_encodings) - - def remove_duplicate_bugdir(self): - self.rcs.remove_duplicate_repo() - - def list_uuids(self): - uuids = [] - if os.path.exists(self.get_path()): - # list the uuids on disk - for uuid in os.listdir(self.get_path("bugs")): - if not (uuid.startswith('.')): - uuids.append(uuid) - yield uuid - # and the ones that are still just in memory - for bug in self: - if bug.uuid not in uuids: - uuids.append(bug.uuid) - yield bug.uuid - - def _clear_bugs(self): - while len(self) > 0: - self.pop() - self._bug_map_gen() - - def _load_bug(self, uuid): - bg = bug.Bug(bugdir=self, uuid=uuid, from_disk=True) - self.append(bg) - self._bug_map_gen() - return bg - - def new_bug(self, uuid=None, summary=None): - bg = bug.Bug(bugdir=self, uuid=uuid, summary=summary) - self.append(bg) - self._bug_map_gen() - return bg - - def remove_bug(self, bug): - self.remove(bug) - bug.remove() - - def bug_shortname(self, bug): - """ - Generate short names from uuids. Picks the minimum number of - characters (>=3) from the beginning of the uuid such that the - short names are unique. - - Obviously, as the number of bugs in the database grows, these - short names will cease to be unique. The complete uuid should be - used for long term reference. - """ - chars = 3 - for uuid in self._bug_map.keys(): - if bug.uuid == uuid: - continue - while (bug.uuid[:chars] == uuid[:chars]): - chars+=1 - return bug.uuid[:chars] - - def bug_from_shortname(self, shortname): - """ - >>> bd = simple_bug_dir() - >>> bug_a = bd.bug_from_shortname('a') - >>> print type(bug_a) - - >>> print bug_a - a:om: Bug A - """ - matches = [] - self._bug_map_gen() - for uuid in self._bug_map.keys(): - if uuid.startswith(shortname): - matches.append(uuid) - if len(matches) > 1: - raise MultipleBugMatches(shortname, matches) - if len(matches) == 1: - return self.bug_from_uuid(matches[0]) - raise KeyError("No bug matches %s" % shortname) - - def bug_from_uuid(self, uuid): - if not self.has_bug(uuid): - raise KeyError("No bug matches %s\n bug map: %s\n root: %s" \ - % (uuid, self._bug_map, self.root)) - if self._bug_map[uuid] == None: - self._load_bug(uuid) - return self._bug_map[uuid] - - def has_bug(self, bug_uuid): - if bug_uuid not in self._bug_map: - self._bug_map_gen() - if bug_uuid not in self._bug_map: - return False - return True - - -def simple_bug_dir(): - """ - For testing - >>> bugdir = simple_bug_dir() - >>> ls = list(bugdir.list_uuids()) - >>> ls.sort() - >>> print ls - ['a', 'b'] - """ - dir = utility.Dir() - assert os.path.exists(dir.path) - bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True, - manipulate_encodings=False) - bugdir._dir_ref = dir # postpone cleanup since dir.__del__() removes dir. - bug_a = bugdir.new_bug("a", summary="Bug A") - bug_a.creator = "John Doe " - bug_a.time = 0 - bug_b = bugdir.new_bug("b", summary="Bug B") - bug_b.creator = "Jane Doe " - bug_b.time = 0 - bug_b.status = "closed" - bugdir.save() - return bugdir - - -class BugDirTestCase(unittest.TestCase): - def __init__(self, *args, **kwargs): - unittest.TestCase.__init__(self, *args, **kwargs) - def setUp(self): - self.dir = utility.Dir() - self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, - allow_rcs_init=True) - self.rcs = self.bugdir.rcs - def tearDown(self): - self.rcs.cleanup() - self.dir.cleanup() - def fullPath(self, path): - return os.path.join(self.dir.path, path) - def assertPathExists(self, path): - fullpath = self.fullPath(path) - self.failUnless(os.path.exists(fullpath)==True, - "path %s does not exist" % fullpath) - self.assertRaises(AlreadyInitialized, BugDir, - self.dir.path, assertNewBugDir=True) - def versionTest(self): - if self.rcs.versioned == False: - return - original = self.bugdir.rcs.commit("Began versioning") - bugA = self.bugdir.bug_from_uuid("a") - bugA.status = "fixed" - self.bugdir.save() - new = self.rcs.commit("Fixed bug a") - dupdir = self.bugdir.duplicate_bugdir(original) - self.failUnless(dupdir.root != self.bugdir.root, - "%s, %s" % (dupdir.root, self.bugdir.root)) - bugAorig = dupdir.bug_from_uuid("a") - self.failUnless(bugA != bugAorig, - "\n%s\n%s" % (bugA.string(), bugAorig.string())) - bugAorig.status = "fixed" - self.failUnless(bug.cmp_status(bugA, bugAorig)==0, - "%s, %s" % (bugA.status, bugAorig.status)) - self.failUnless(bug.cmp_severity(bugA, bugAorig)==0, - "%s, %s" % (bugA.severity, bugAorig.severity)) - self.failUnless(bug.cmp_assigned(bugA, bugAorig)==0, - "%s, %s" % (bugA.assigned, bugAorig.assigned)) - self.failUnless(bug.cmp_time(bugA, bugAorig)==0, - "%s, %s" % (bugA.time, bugAorig.time)) - self.failUnless(bug.cmp_creator(bugA, bugAorig)==0, - "%s, %s" % (bugA.creator, bugAorig.creator)) - self.failUnless(bugA == bugAorig, - "\n%s\n%s" % (bugA.string(), bugAorig.string())) - self.bugdir.remove_duplicate_bugdir() - self.failUnless(os.path.exists(dupdir.root)==False, str(dupdir.root)) - def testRun(self): - self.bugdir.new_bug(uuid="a", summary="Ant") - self.bugdir.new_bug(uuid="b", summary="Cockroach") - self.bugdir.new_bug(uuid="c", summary="Praying mantis") - length = len(self.bugdir) - self.failUnless(length == 3, "%d != 3 bugs" % length) - uuids = list(self.bugdir.list_uuids()) - self.failUnless(len(uuids) == 3, "%d != 3 uuids" % len(uuids)) - self.failUnless(uuids == ["a","b","c"], str(uuids)) - bugA = self.bugdir.bug_from_uuid("a") - bugAprime = self.bugdir.bug_from_shortname("a") - self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime)) - self.bugdir.save() - self.versionTest() - def testComments(self): - self.bugdir.new_bug(uuid="a", summary="Ant") - bug = self.bugdir.bug_from_uuid("a") - comm = bug.comment_root - rep = comm.new_reply("Ants are small.") - rep.new_reply("And they have six legs.") - self.bugdir.save() - self.bugdir._clear_bugs() - bug = self.bugdir.bug_from_uuid("a") - bug.load_comments() - self.failUnless(len(bug.comment_root)==1, len(bug.comment_root)) - for index,comment in enumerate(bug.comments()): - if index == 0: - repLoaded = comment - self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid) - self.failUnless(comment.sync_with_disk == True, - comment.sync_with_disk) - #load_settings() - self.failUnless(comment.content_type == "text/plain", - comment.content_type) - self.failUnless(repLoaded.settings["Content-type"]=="text/plain", - repLoaded.settings) - self.failUnless(repLoaded.body == "Ants are small.", - repLoaded.body) - elif index == 1: - self.failUnless(comment.in_reply_to == repLoaded.uuid, - repLoaded.uuid) - self.failUnless(comment.body == "And they have six legs.", - comment.body) - else: - self.failIf(True, "Invalid comment: %d\n%s" % (index, comment)) - -unitsuite = unittest.TestLoader().loadTestsFromTestCase(BugDirTestCase) -suite = unittest.TestSuite([unitsuite])#, doctest.DocTestSuite()]) diff --git a/libbe/bzr.py b/libbe/bzr.py index d73392a..e69de29 100644 --- a/libbe/bzr.py +++ b/libbe/bzr.py @@ -1,104 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Ben Finney -# Marien Zwart -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 -import re -import sys -import unittest -import doctest - -import rcs -from rcs import RCS - -def new(): - return Bzr() - -class Bzr(RCS): - name = "bzr" - client = "bzr" - versioned = True - def _rcs_help(self): - status,output,error = self._u_invoke_client("--help") - return output - def _rcs_detect(self, path): - if self._u_search_parent_directories(path, ".bzr") != None : - return True - return False - def _rcs_root(self, path): - """Find the root of the deepest repository containing path.""" - status,output,error = self._u_invoke_client("root", path) - return output.rstrip('\n') - def _rcs_init(self, path): - self._u_invoke_client("init", directory=path) - def _rcs_get_user_id(self): - status,output,error = self._u_invoke_client("whoami") - return output.rstrip('\n') - def _rcs_set_user_id(self, value): - self._u_invoke_client("whoami", value) - def _rcs_add(self, path): - self._u_invoke_client("add", path) - def _rcs_remove(self, path): - # --force to also remove unversioned files. - self._u_invoke_client("remove", "--force", path) - def _rcs_update(self, path): - pass - def _rcs_get_file_contents(self, path, revision=None, binary=False): - if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) - else: - status,output,error = \ - self._u_invoke_client("cat","-r",revision,path) - return output - def _rcs_duplicate_repo(self, directory, revision=None): - if revision == None: - RCS._rcs_duplicate_repo(self, directory, revision) - else: - self._u_invoke_client("branch", "--revision", revision, - ".", directory) - def _rcs_commit(self, commitfile): - status,output,error = self._u_invoke_client("commit", "--unchanged", - "--file", commitfile) - revision = None - revline = re.compile("Committed revision (.*)[.]") - match = revline.search(error) - assert match != None, output+error - assert len(match.groups()) == 1 - revision = match.groups()[0] - return revision - def postcommit(self): - try: - self._u_invoke_client('merge') - except rcs.CommandError, e: - if ('No merge branch known or specified' in e.err_str or - 'No merge location known or specified' in e.err_str): - pass - else: - self._u_invoke_client('revert', '--no-backup', - directory=directory) - self._u_invoke_client('resolve', '--all', directory=directory) - raise - if len(self._u_invoke_client('status', directory=directory)[1]) > 0: - self.commit('Merge from upstream') - - -rcs.make_rcs_testcase_subclasses(Bzr, sys.modules[__name__]) - -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index edc6442..e69de29 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -1,191 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Oleg Romanyshyn -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 optparse -import os -from textwrap import TextWrapper -from StringIO import StringIO -import sys -import doctest - -import bugdir -import plugin -import encoding - - -class UserError(Exception): - def __init__(self, msg): - Exception.__init__(self, msg) - -class UserErrorWrap(UserError): - def __init__(self, exception): - UserError.__init__(self, str(exception)) - self.exception = exception - -class UsageError(Exception): - pass - -class GetHelp(Exception): - pass - -class GetCompletions(Exception): - def __init__(self, completions=[]): - msg = "Get allowed completions" - Exception.__init__(self, msg) - self.completions = completions - -def iter_commands(): - for name, module in plugin.iter_plugins("becommands"): - yield name.replace("_", "-"), module - -def get_command(command_name): - """Retrieves the module for a user command - - >>> get_command("asdf") - Traceback (most recent call last): - UserError: Unknown command asdf - >>> repr(get_command("list")).startswith(" -# Thomas Habets -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA -import email.mime.base, email.encoders -import os -import os.path -import time -import xml.sax.saxutils -import textwrap -import doctest - -from beuuid import uuid_gen -from properties import Property, doc_property, local_property, \ - defaulting_property, checked_property, cached_property, \ - primed_property, change_hook_property, settings_property -import settings_object -import mapfile -from tree import Tree -import utility - - -class InvalidShortname(KeyError): - def __init__(self, shortname, shortnames): - msg = "Invalid shortname %s\n%s" % (shortname, shortnames) - KeyError.__init__(self, msg) - self.shortname = shortname - self.shortnames = shortnames - - -INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!" - -def _list_to_root(comments, bug): - """ - Convert a raw list of comments to single (dummy) root comment. We - use a dummy root comment, because there can be several comment - threads rooted on the same parent bug. To simplify comment - interaction, we condense these threads into a single thread with a - Comment dummy root. - - No Comment method should use the dummy comment. - """ - root_comments = [] - uuid_map = {} - for comment in comments: - assert comment.uuid != None - uuid_map[comment.uuid] = comment - for comm in comments: - rep = comm.in_reply_to - if rep == None or rep == settings_object.EMPTY or rep == bug.uuid: - root_comments.append(comm) - else: - parentUUID = comm.in_reply_to - parent = uuid_map[parentUUID] - parent.add_reply(comm) - dummy_root = Comment(bug, uuid=INVALID_UUID) - dummy_root.extend(root_comments) - return dummy_root - -def loadComments(bug, load_full=False): - """ - Set load_full=True when you want to load the comment completely - from disk *now*, rather than waiting and lazy loading as required. - """ - path = bug.get_path("comments") - if not os.path.isdir(path): - return Comment(bug, uuid=INVALID_UUID) - comments = [] - for uuid in os.listdir(path): - if uuid.startswith('.'): - continue - comm = Comment(bug, uuid, from_disk=True) - if load_full == True: - comm.load_settings() - dummy = comm.body # force the body to load - comments.append(comm) - return _list_to_root(comments, bug) - -def saveComments(bug): - path = bug.get_path("comments") - bug.rcs.mkdir(path) - for comment in bug.comment_root.traverse(): - comment.save() - - -class Comment(Tree, settings_object.SavedSettingsObject): - """ - >>> c = Comment() - >>> c.uuid != None - True - >>> c.uuid = "some-UUID" - >>> print c.content_type - text/plain - """ - - settings_properties = [] - required_saved_properties = [] - _prop_save_settings = settings_object.prop_save_settings - _prop_load_settings = settings_object.prop_load_settings - def _versioned_property(settings_properties=settings_properties, - required_saved_properties=required_saved_properties, - **kwargs): - if "settings_properties" not in kwargs: - kwargs["settings_properties"] = settings_properties - if "required_saved_properties" not in kwargs: - kwargs["required_saved_properties"]=required_saved_properties - return settings_object.versioned_property(**kwargs) - - @_versioned_property(name="From", - doc="The author of the comment") - def From(): return {} - - @_versioned_property(name="In-reply-to", - doc="UUID for parent comment or bug") - def in_reply_to(): return {} - - @_versioned_property(name="Content-type", - doc="Mime type for comment body", - default="text/plain", - require_save=True) - def content_type(): return {} - - @_versioned_property(name="Date", - doc="An RFC 2822 timestamp for comment creation") - def time_string(): return {} - - def _get_time(self): - if self.time_string == None: - return None - return utility.str_to_time(self.time_string) - def _set_time(self, value): - self.time_string = utility.time_to_str(value) - time = property(fget=_get_time, - fset=_set_time, - doc="An integer version of .time_string") - - def _get_comment_body(self): - if self.rcs != None and self.sync_with_disk == True: - import rcs - binary = not self.content_type.startswith("text/") - return self.rcs.get_file_contents(self.get_path("body"), binary=binary) - def _set_comment_body(self, old=None, new=None, force=False): - if (self.rcs != None and self.sync_with_disk == True) or force==True: - assert new != None, "Can't save empty comment" - binary = not self.content_type.startswith("text/") - self.rcs.set_file_contents(self.get_path("body"), new, binary=binary) - - @Property - @change_hook_property(hook=_set_comment_body) - @cached_property(generator=_get_comment_body) - @local_property("body") - @doc_property(doc="The meat of the comment") - def body(): return {} - - def _get_rcs(self): - if hasattr(self.bug, "rcs"): - return self.bug.rcs - - @Property - @cached_property(generator=_get_rcs) - @local_property("rcs") - @doc_property(doc="A revision control system instance.") - def rcs(): return {} - - def __init__(self, bug=None, uuid=None, from_disk=False, - in_reply_to=None, body=None): - """ - Set from_disk=True to load an old comment. - Set from_disk=False to create a new comment. - - The uuid option is required when from_disk==True. - - The in_reply_to and body options are only used if - from_disk==False (the default). When from_disk==True, they are - loaded from the bug database. - - in_reply_to should be the uuid string of the parent comment. - """ - Tree.__init__(self) - settings_object.SavedSettingsObject.__init__(self) - self.bug = bug - self.uuid = uuid - if from_disk == True: - self.sync_with_disk = True - else: - self.sync_with_disk = False - if uuid == None: - self.uuid = uuid_gen() - self.time = int(time.time()) # only save to second precision - if self.rcs != None: - self.From = self.rcs.get_user_id() - self.in_reply_to = in_reply_to - self.body = body - - def traverse(self, *args, **kwargs): - """Avoid working with the possible dummy root comment""" - for comment in Tree.traverse(self, *args, **kwargs): - if comment.uuid == INVALID_UUID: - continue - yield comment - - def _setting_attr_string(self, setting): - value = getattr(self, setting) - if value == settings_object.EMPTY: - return "" - else: - return str(value) - - def xml(self, indent=0, shortname=None): - """ - >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") - >>> comm.uuid = "0123" - >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" - >>> print comm.xml(indent=2, shortname="com-1") - - 0123 - com-1 - - Thu, 01 Jan 1970 00:00:00 +0000 - text/plain - Some - insightful - remarks - - """ - if shortname == None: - shortname = self.uuid - if self.content_type.startswith("text/"): - body = (self.body or "").rstrip('\n') - else: - maintype,subtype = self.content_type.split('/',1) - msg = email.mime.base.MIMEBase(maintype, subtype) - msg.set_payload(self.body or "") - email.encoders.encode_base64(msg) - body = msg.as_string() - info = [("uuid", self.uuid), - ("short-name", shortname), - ("in-reply-to", self.in_reply_to), - ("from", self._setting_attr_string("From")), - ("date", self.time_string), - ("content-type", self.content_type), - ("body", body)] - lines = [""] - for (k,v) in info: - if v not in [settings_object.EMPTY, None]: - lines.append(' <%s>%s' % (k,xml.sax.saxutils.escape(v),k)) - lines.append("") - istring = ' '*indent - sep = '\n' + istring - return istring + sep.join(lines).rstrip('\n') - - def string(self, indent=0, shortname=None): - """ - >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") - >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" - >>> print comm.string(indent=2, shortname="com-1") - --------- Comment --------- - Name: com-1 - From: - Date: Thu, 01 Jan 1970 00:00:00 +0000 - - Some - insightful - remarks - """ - if shortname == None: - shortname = self.uuid - lines = [] - lines.append("--------- Comment ---------") - lines.append("Name: %s" % shortname) - lines.append("From: %s" % (self._setting_attr_string("From"))) - lines.append("Date: %s" % self.time_string) - lines.append("") - #lines.append(textwrap.fill(self.body or "", - # width=(79-indent))) - if self.content_type.startswith("text/"): - lines.extend((self.body or "").splitlines()) - else: - lines.append("Content type %s not printable. Try XML output instead" % self.content_type) - # some comments shouldn't be wrapped... - - istring = ' '*indent - sep = '\n' + istring - return istring + sep.join(lines).rstrip('\n') - - def __str__(self): - """ - >>> comm = Comment(bug=None, body="Some insightful remarks") - >>> comm.uuid = "com-1" - >>> comm.time_string = "Thu, 20 Nov 2008 15:55:11 +0000" - >>> comm.From = "Jane Doe " - >>> print comm - --------- Comment --------- - Name: com-1 - From: Jane Doe - Date: Thu, 20 Nov 2008 15:55:11 +0000 - - Some insightful remarks - """ - return self.string() - - def get_path(self, name=None): - my_dir = os.path.join(self.bug.get_path("comments"), self.uuid) - if name is None: - return my_dir - assert name in ["values", "body"] - return os.path.join(my_dir, name) - - def load_settings(self): - self.settings = mapfile.map_load(self.rcs, self.get_path("values")) - self._setup_saved_settings() - - def save_settings(self): - parent_dir = os.path.dirname(self.get_path()) - self.rcs.mkdir(parent_dir) - self.rcs.mkdir(self.get_path()) - path = self.get_path("values") - mapfile.map_save(self.rcs, path, self._get_saved_settings()) - - def save(self): - assert self.body != None, "Can't save blank comment" - #if self.in_reply_to == None: - # raise Exception, str(self)+'\n'+str(self.settings)+'\n'+str(self._settings_loaded) - #assert self.in_reply_to != None, "Comment must be a reply to something" - self.save_settings() - self._set_comment_body(new=self.body, force=True) - - def remove(self): - for comment in self.traverse(): - path = comment.get_path() - self.rcs.recursive_remove(path) - - def add_reply(self, reply, allow_time_inversion=False): - if self.uuid != INVALID_UUID: - reply.in_reply_to = self.uuid - self.append(reply) - #raise Exception, "adding reply \n%s\n%s" % (self, reply) - - def new_reply(self, body=None): - """ - >>> comm = Comment(bug=None, body="Some insightful remarks") - >>> repA = comm.new_reply("Critique original comment") - >>> repB = repA.new_reply("Begin flamewar :p") - >>> repB.in_reply_to == repA.uuid - True - """ - reply = Comment(self.bug, body=body) - self.add_reply(reply) - #raise Exception, "new reply added (%s),\n%s\n%s\n\t--%s--" % (body, self, reply, reply.in_reply_to) - return reply - - def string_thread(self, string_method_name="string", name_map={}, - indent=0, flatten=True, - auto_name_map=False, bug_shortname=None): - """ - Return a string displaying a thread of comments. - bug_shortname is only used if auto_name_map == True. - - string_method_name (defaults to "string") is the name of the - Comment method used to generate the output string for each - Comment in the thread. The method must take the arguments - indent and shortname. - - SIDE-EFFECT: if auto_name_map==True, calls comment_shortnames() - which will sort the tree by comment.time. Avoid by calling - name_map = {} - for shortname,comment in comm.comment_shortnames(bug_shortname): - name_map[comment.uuid] = shortname - comm.sort(key=lambda c : c.From) # your sort - comm.string_thread(name_map=name_map) - - >>> a = Comment(bug=None, uuid="a", body="Insightful remarks") - >>> a.time = utility.str_to_time("Thu, 20 Nov 2008 01:00:00 +0000") - >>> b = a.new_reply("Critique original comment") - >>> b.uuid = "b" - >>> b.time = utility.str_to_time("Thu, 20 Nov 2008 02:00:00 +0000") - >>> c = b.new_reply("Begin flamewar :p") - >>> c.uuid = "c" - >>> c.time = utility.str_to_time("Thu, 20 Nov 2008 03:00:00 +0000") - >>> d = a.new_reply("Useful examples") - >>> d.uuid = "d" - >>> d.time = utility.str_to_time("Thu, 20 Nov 2008 04:00:00 +0000") - >>> a.sort(key=lambda comm : comm.time) - >>> print a.string_thread(flatten=True) - --------- Comment --------- - Name: a - From: - Date: Thu, 20 Nov 2008 01:00:00 +0000 - - Insightful remarks - --------- Comment --------- - Name: b - From: - Date: Thu, 20 Nov 2008 02:00:00 +0000 - - Critique original comment - --------- Comment --------- - Name: c - From: - Date: Thu, 20 Nov 2008 03:00:00 +0000 - - Begin flamewar :p - --------- Comment --------- - Name: d - From: - Date: Thu, 20 Nov 2008 04:00:00 +0000 - - Useful examples - >>> print a.string_thread(auto_name_map=True, bug_shortname="bug-1") - --------- Comment --------- - Name: bug-1:1 - From: - Date: Thu, 20 Nov 2008 01:00:00 +0000 - - Insightful remarks - --------- Comment --------- - Name: bug-1:2 - From: - Date: Thu, 20 Nov 2008 02:00:00 +0000 - - Critique original comment - --------- Comment --------- - Name: bug-1:3 - From: - Date: Thu, 20 Nov 2008 03:00:00 +0000 - - Begin flamewar :p - --------- Comment --------- - Name: bug-1:4 - From: - Date: Thu, 20 Nov 2008 04:00:00 +0000 - - Useful examples - """ - if auto_name_map == True: - name_map = {} - for shortname,comment in self.comment_shortnames(bug_shortname): - name_map[comment.uuid] = shortname - stringlist = [] - for depth,comment in self.thread(flatten=flatten): - ind = 2*depth+indent - if comment.uuid in name_map: - sname = name_map[comment.uuid] - else: - sname = None - string_fn = getattr(comment, string_method_name) - stringlist.append(string_fn(indent=ind, shortname=sname)) - return '\n'.join(stringlist) - - def xml_thread(self, name_map={}, indent=0, - auto_name_map=False, bug_shortname=None): - return self.string_thread(string_method_name="xml", name_map=name_map, - indent=indent, auto_name_map=auto_name_map, - bug_shortname=bug_shortname) - - def comment_shortnames(self, bug_shortname=None): - """ - Iterate through (id, comment) pairs, in time order. - (This is a user-friendly id, not the comment uuid). - - SIDE-EFFECT : will sort the comment tree by comment.time - - >>> a = Comment(bug=None, uuid="a") - >>> b = a.new_reply() - >>> b.uuid = "b" - >>> c = b.new_reply() - >>> c.uuid = "c" - >>> d = a.new_reply() - >>> d.uuid = "d" - >>> for id,name in a.comment_shortnames("bug-1"): - ... print id, name.uuid - bug-1:1 a - bug-1:2 b - bug-1:3 c - bug-1:4 d - """ - if bug_shortname == None: - bug_shortname = "" - self.sort(key=lambda comm : comm.time) - for num,comment in enumerate(self.traverse()): - yield ("%s:%d" % (bug_shortname, num+1), comment) - - def comment_from_shortname(self, comment_shortname, *args, **kwargs): - """ - Use a comment shortname to look up a comment. - >>> a = Comment(bug=None, uuid="a") - >>> b = a.new_reply() - >>> b.uuid = "b" - >>> c = b.new_reply() - >>> c.uuid = "c" - >>> d = a.new_reply() - >>> d.uuid = "d" - >>> comm = a.comment_from_shortname("bug-1:3", bug_shortname="bug-1") - >>> id(comm) == id(c) - True - """ - for cur_name, comment in self.comment_shortnames(*args, **kwargs): - if comment_shortname == cur_name: - return comment - raise InvalidShortname(comment_shortname, - list(self.comment_shortnames(*args, **kwargs))) - - def comment_from_uuid(self, uuid): - """ - Use a comment shortname to look up a comment. - >>> a = Comment(bug=None, uuid="a") - >>> b = a.new_reply() - >>> b.uuid = "b" - >>> c = b.new_reply() - >>> c.uuid = "c" - >>> d = a.new_reply() - >>> d.uuid = "d" - >>> comm = a.comment_from_uuid("d") - >>> id(comm) == id(d) - True - """ - for comment in self.traverse(): - if comment.uuid == uuid: - return comment - raise KeyError(uuid) - -suite = doctest.DocTestSuite() diff --git a/libbe/config.py b/libbe/config.py index 7f600a5..e69de29 100644 --- a/libbe/config.py +++ b/libbe/config.py @@ -1,84 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 ConfigParser -import codecs -import locale -import os.path -import sys -import doctest - -default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() - -def path(): - """Return the path to the per-user config file""" - return os.path.expanduser("~/.bugs_everywhere") - -def set_val(name, value, section="DEFAULT", encoding=None): - """Set a value in the per-user config file - - :param name: The name of the value to set - :param value: The new value to set (or None to delete the value) - :param section: The section to store the name/value in - """ - if encoding == None: - encoding = default_encoding - config = ConfigParser.ConfigParser() - if os.path.exists(path()) == False: # touch file or config - open(path(), "w").close() # read chokes on missing file - f = codecs.open(path(), "r", encoding) - config.readfp(f, path()) - f.close() - if value is not None: - config.set(section, name, value) - else: - config.remove_option(section, name) - f = codecs.open(path(), "w", encoding) - config.write(f) - f.close() - -def get_val(name, section="DEFAULT", default=None, encoding=None): - """ - Get a value from the per-user config file - - :param name: The name of the value to get - :section: The section that the name is in - :return: The value, or None - >>> get_val("junk") is None - True - >>> set_val("junk", "random") - >>> get_val("junk") - u'random' - >>> set_val("junk", None) - >>> get_val("junk") is None - True - """ - if os.path.exists(path()): - if encoding == None: - encoding = default_encoding - config = ConfigParser.ConfigParser() - f = codecs.open(path(), "r", encoding) - config.readfp(f, path()) - f.close() - try: - return config.get(section, name) - except ConfigParser.NoOptionError: - return default - else: - return default - -suite = doctest.DocTestSuite() diff --git a/libbe/darcs.py b/libbe/darcs.py index 43af99a..e69de29 100644 --- a/libbe/darcs.py +++ b/libbe/darcs.py @@ -1,162 +0,0 @@ -# Copyright (C) 2009 W. Trevor King -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 codecs -import os -import re -import sys -import unittest -import doctest - -import rcs -from rcs import RCS - -def new(): - return Darcs() - -class Darcs(RCS): - name="darcs" - client="darcs" - versioned=True - def _rcs_help(self): - status,output,error = self._u_invoke_client("--help") - return output - def _rcs_detect(self, path): - if self._u_search_parent_directories(path, "_darcs") != None : - return True - return False - def _rcs_root(self, path): - """Find the root of the deepest repository containing path.""" - # Assume that nothing funny is going on; in particular, that we aren't - # dealing with a bare repo. - if os.path.isdir(path) != True: - path = os.path.dirname(path) - darcs_dir = self._u_search_parent_directories(path, "_darcs") - if darcs_dir == None: - return None - return os.path.dirname(darcs_dir) - def _rcs_init(self, path): - self._u_invoke_client("init", directory=path) - def _rcs_get_user_id(self): - # following http://darcs.net/manual/node4.html#SECTION00410030000000000000 - # as of June 29th, 2009 - if self.rootdir == None: - return None - darcs_dir = os.path.join(self.rootdir, "_darcs") - if darcs_dir != None: - for pref_file in ["author", "email"]: - pref_path = os.path.join(darcs_dir, "prefs", pref_file) - if os.path.exists(pref_path): - return self.get_file_contents(pref_path) - for env_variable in ["DARCS_EMAIL", "EMAIL"]: - if env_variable in os.environ: - return os.environ[env_variable] - return None - def _rcs_set_user_id(self, value): - if self.rootdir == None: - self.root(".") - if self.rootdir == None: - raise rcs.SettingIDnotSupported - author_path = os.path.join(self.rootdir, "_darcs", "prefs", "author") - f = codecs.open(author_path, "w", self.encoding) - f.write(value) - f.close() - def _rcs_add(self, path): - if os.path.isdir(path): - return - self._u_invoke_client("add", path) - def _rcs_remove(self, path): - if not os.path.isdir(self._u_abspath(path)): - os.remove(os.path.join(self.rootdir, path)) # darcs notices removal - def _rcs_update(self, path): - pass # darcs notices changes - def _rcs_get_file_contents(self, path, revision=None, binary=False): - if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, - binary=binary) - else: - try: - return self._u_invoke_client("show", "contents", "--patch", revision, path) - except rcs.CommandError: - # Darcs versions < 2.0.0pre2 lack the "show contents" command - - status,output,error = self._u_invoke_client("diff", "--unified", - "--from-patch", - revision, path) - major_patch = output - status,output,error = self._u_invoke_client("diff", "--unified", - "--patch", - revision, path) - target_patch = output - - # "--output -" to be supported in GNU patch > 2.5.9 - # but that hasn't been released as of June 30th, 2009. - - # Rewrite path to status before the patch we want - args=["patch", "--reverse", path] - status,output,error = self._u_invoke(args, stdin=major_patch) - # Now apply the patch we want - args=["patch", path] - status,output,error = self._u_invoke(args, stdin=target_patch) - - if os.path.exists(os.path.join(self.rootdir, path)) == True: - contents = RCS._rcs_get_file_contents(self, path, - binary=binary) - else: - contents = "" - - # Now restore path to it's current incarnation - args=["patch", "--reverse", path] - status,output,error = self._u_invoke(args, stdin=target_patch) - args=["patch", path] - status,output,error = self._u_invoke(args, stdin=major_patch) - current_contents = RCS._rcs_get_file_contents(self, path, - binary=binary) - return contents - def _rcs_duplicate_repo(self, directory, revision=None): - if revision==None: - RCS._rcs_duplicate_repo(self, directory, revision) - else: - self._u_invoke_client("put", "--to-patch", revision, directory) - def _rcs_commit(self, commitfile): - id = self.get_user_id() - if '@' not in id: - id = "%s <%s@invalid.com>" % (id, id) - # Darcs doesn't like commitfiles without trailing endlines. - f = codecs.open(commitfile, 'r', self.encoding) - contents = f.read() - f.close() - if contents[-1] != '\n': - f = codecs.open(commitfile, 'a', self.encoding) - f.write('\n') - f.close() - status,output,error = self._u_invoke_client('record', '--all', - '--author', id, - '--logfile', commitfile) - revision = None - - revline = re.compile("Finished recording patch '(.*)'") - match = revline.search(output) - assert match != None, output+error - assert len(match.groups()) == 1 - revision = match.groups()[0] - return revision - - -rcs.make_rcs_testcase_subclasses(Darcs, sys.modules[__name__]) - -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/diff.py b/libbe/diff.py index a349e14..e69de29 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -1,117 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 -"""Compare two bug trees""" -from libbe import cmdutil, bugdir, bug -from libbe.utility import time_to_str -import doctest - -def diff(old_bugdir, new_bugdir): - added = [] - removed = [] - modified = [] - for uuid in old_bugdir.list_uuids(): - old_bug = old_bugdir.bug_from_uuid(uuid) - try: - new_bug = new_bugdir.bug_from_uuid(uuid) - if old_bug != new_bug: - modified.append((old_bug, new_bug)) - except KeyError: - removed.append(old_bug) - for uuid in new_bugdir.list_uuids(): - if not old_bugdir.has_bug(uuid): - new_bug = new_bugdir.bug_from_uuid(uuid) - added.append(new_bug) - return (removed, modified, added) - -def diff_report(diff_data, bug_dir): - (removed, modified, added) = diff_data - def modified_cmp(left, right): - return bug.cmp_severity(left[1], right[1]) - - added.sort(bug.cmp_severity) - removed.sort(bug.cmp_severity) - modified.sort(modified_cmp) - lines = [] - - if len(added) > 0: - lines.append("New bug reports:") - for bg in added: - lines.extend(bg.string(shortlist=True).splitlines()) - lines.append("") - - if len(modified) > 0: - printed = False - for old_bug, new_bug in modified: - change_str = bug_changes(old_bug, new_bug, bug_dir) - if change_str is None: - continue - if not printed: - printed = True - lines.append("Modified bug reports:") - lines.extend(change_str.splitlines()) - if printed == True: - lines.append("") - - if len(removed) > 0: - lines.append("Removed bug reports:") - for bg in removed: - lines.extend(bg.string(shortlist=True).splitlines()) - lines.append("") - - return '\n'.join(lines) - -def change_lines(old, new, attributes): - change_list = [] - for attr in attributes: - old_attr = getattr(old, attr) - new_attr = getattr(new, attr) - if old_attr != new_attr: - change_list.append((attr, old_attr, new_attr)) - if len(change_list) >= 0: - return change_list - else: - return None - -def bug_changes(old, new, bugs): - change_list = change_lines(old, new, ("time", "creator", "severity", - "target", "summary", "status", "assigned")) - - old_comment_ids = [c.uuid for c in old.comments()] - new_comment_ids = [c.uuid for c in new.comments()] - change_strings = ["%s: %s -> %s" % f for f in change_list] - for comment_id in new_comment_ids: - if comment_id not in old_comment_ids: - summary = comment_summary(new.comment_from_uuid(comment_id), "new") - change_strings.append(summary) - for comment_id in old_comment_ids: - if comment_id not in new_comment_ids: - summary = comment_summary(new.comment_from_uuid(comment_id), - "removed") - change_strings.append(summary) - - if len(change_strings) == 0: - return None - return "%s\n %s" % (new.string(shortlist=True), - " \n".join(change_strings)) - - -def comment_summary(comment, status): - return "%8s comment from %s on %s" % (status, comment.From, - time_to_str(comment.time)) - -suite = doctest.DocTestSuite() diff --git a/libbe/editor.py b/libbe/editor.py index 6638ed9..e69de29 100644 --- a/libbe/editor.py +++ b/libbe/editor.py @@ -1,103 +0,0 @@ -# Bugs Everywhere, a distributed bugtracker -# Copyright (C) 2008-2009 W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA - -import codecs -import locale -import os -import sys -import tempfile -import doctest - -default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() - -comment_marker = u"== Anything below this line will be ignored\n" - -class CantFindEditor(Exception): - def __init__(self): - Exception.__init__(self, "Can't find editor to get string from") - -def editor_string(comment=None, encoding=None): - """Invokes the editor, and returns the user_produced text as a string - - >>> if "EDITOR" in os.environ: - ... del os.environ["EDITOR"] - >>> if "VISUAL" in os.environ: - ... del os.environ["VISUAL"] - >>> editor_string() - Traceback (most recent call last): - CantFindEditor: Can't find editor to get string from - >>> os.environ["EDITOR"] = "echo bar > " - >>> editor_string() - u'bar\\n' - >>> os.environ["VISUAL"] = "echo baz > " - >>> editor_string() - u'baz\\n' - >>> del os.environ["EDITOR"] - >>> del os.environ["VISUAL"] - """ - if encoding == None: - encoding = default_encoding - for name in ('VISUAL', 'EDITOR'): - try: - editor = os.environ[name] - break - except KeyError: - pass - else: - raise CantFindEditor() - fhandle, fname = tempfile.mkstemp() - try: - if comment is not None: - os.write(fhandle, '\n'+comment_string(comment)) - os.close(fhandle) - oldmtime = os.path.getmtime(fname) - os.system("%s %s" % (editor, fname)) - f = codecs.open(fname, "r", encoding) - output = trimmed_string(f.read()) - f.close() - if output.rstrip('\n') == "": - output = None - finally: - os.unlink(fname) - return output - - -def comment_string(comment): - """ - >>> comment_string('hello') == comment_marker+"hello" - True - """ - return comment_marker + comment - - -def trimmed_string(instring): - """ - >>> trimmed_string("hello\\n"+comment_marker) - u'hello\\n' - >>> trimmed_string("hi!\\n" + comment_string('Booga')) - u'hi!\\n' - """ - out = [] - for line in instring.splitlines(True): - if line.startswith(comment_marker): - break - out.append(line) - return ''.join(out) - -suite = doctest.DocTestSuite() diff --git a/libbe/encoding.py b/libbe/encoding.py index bdac98e..e69de29 100644 --- a/libbe/encoding.py +++ b/libbe/encoding.py @@ -1,53 +0,0 @@ -# Bugs Everywhere, a distributed bugtracker -# Copyright (C) 2008-2009 W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA -import codecs -import locale -import sys -import doctest - -def get_encoding(): - """ - Guess a useful input/output/filesystem encoding... Maybe we need - seperate encodings for input/output and filesystem? Hmm... - """ - encoding = locale.getpreferredencoding() or sys.getdefaultencoding() - if sys.platform != 'win32' or sys.version_info[:2] > (2, 3): - encoding = locale.getlocale(locale.LC_TIME)[1] or encoding - # Python 2.3 on windows doesn't know about 'XYZ' alias for 'cpXYZ' - return encoding - -def known_encoding(encoding): - """ - >>> known_encoding("highly-unlikely-encoding") - False - >>> known_encoding(get_encoding()) - True - """ - try: - codecs.lookup(encoding) - return True - except LookupError: - return False - -def set_IO_stream_encodings(encoding): - sys.stdin = codecs.getreader(encoding)(sys.__stdin__) - sys.stdout = codecs.getwriter(encoding)(sys.__stdout__) - sys.stderr = codecs.getwriter(encoding)(sys.__stderr__) - -suite = doctest.DocTestSuite() diff --git a/libbe/git.py b/libbe/git.py index 31bbe32..e69de29 100644 --- a/libbe/git.py +++ b/libbe/git.py @@ -1,111 +0,0 @@ -# Copyright (C) 2008-2009 Ben Finney -# Chris Ball -# W. Trevor King -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 -import re -import sys -import unittest -import doctest - -import rcs -from rcs import RCS - -def new(): - return Git() - -class Git(RCS): - name="git" - client="git" - versioned=True - def _rcs_help(self): - status,output,error = self._u_invoke_client("--help") - return output - def _rcs_detect(self, path): - if self._u_search_parent_directories(path, ".git") != None : - return True - return False - def _rcs_root(self, path): - """Find the root of the deepest repository containing path.""" - # Assume that nothing funny is going on; in particular, that we aren't - # dealing with a bare repo. - if os.path.isdir(path) != True: - path = os.path.dirname(path) - status,output,error = self._u_invoke_client("rev-parse", "--git-dir", - directory=path) - gitdir = os.path.join(path, output.rstrip('\n')) - dirname = os.path.abspath(os.path.dirname(gitdir)) - return dirname - def _rcs_init(self, path): - self._u_invoke_client("init", directory=path) - def _rcs_get_user_id(self): - status,output,error = self._u_invoke_client("config", "user.name") - name = output.rstrip('\n') - status,output,error = self._u_invoke_client("config", "user.email") - email = output.rstrip('\n') - if name != "" or email != "": # got something! - # guess missing info, if necessary - if name == "": - name = self._u_get_fallback_username() - if email == "": - email = self._u_get_fallback_email() - return self._u_create_id(name, email) - return None # Git has no infomation - def _rcs_set_user_id(self, value): - name,email = self._u_parse_id(value) - if email != None: - self._u_invoke_client("config", "user.email", email) - self._u_invoke_client("config", "user.name", name) - def _rcs_add(self, path): - if os.path.isdir(path): - return - self._u_invoke_client("add", path) - def _rcs_remove(self, path): - if not os.path.isdir(self._u_abspath(path)): - self._u_invoke_client("rm", "-f", path) - def _rcs_update(self, path): - self._rcs_add(path) - def _rcs_get_file_contents(self, path, revision=None, binary=False): - if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) - else: - arg = "%s:%s" % (revision,path) - status,output,error = self._u_invoke_client("show", arg) - return output - def _rcs_duplicate_repo(self, directory, revision=None): - if revision==None: - RCS._rcs_duplicate_repo(self, directory, revision) - else: - #self._u_invoke_client("archive", revision, directory) # makes tarball - self._u_invoke_client("clone", "--no-checkout",".",directory) - self._u_invoke_client("checkout", revision, directory=directory) - def _rcs_commit(self, commitfile): - status,output,error = self._u_invoke_client('commit', '-a', - '-F', commitfile) - revision = None - revline = re.compile("(.*) (.*)[:\]] (.*)") - match = revline.search(output) - assert match != None, output+error - assert len(match.groups()) == 3 - revision = match.groups()[1] - return revision - - -rcs.make_rcs_testcase_subclasses(Git, sys.modules[__name__]) - -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/hg.py b/libbe/hg.py index 30b0470..e69de29 100644 --- a/libbe/hg.py +++ b/libbe/hg.py @@ -1,91 +0,0 @@ -# Copyright (C) 2007-2009 Aaron Bentley and Panometrics, Inc. -# Ben Finney -# W. Trevor King -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 -import re -import sys -import unittest -import doctest - -import rcs -from rcs import RCS - -def new(): - return Hg() - -class Hg(RCS): - name="hg" - client="hg" - versioned=True - def _rcs_help(self): - status,output,error = self._u_invoke_client("--help") - return output - def _rcs_detect(self, path): - """Detect whether a directory is revision-controlled using Mercurial""" - if self._u_search_parent_directories(path, ".hg") != None: - return True - return False - def _rcs_root(self, path): - status,output,error = self._u_invoke_client("root", directory=path) - return output.rstrip('\n') - def _rcs_init(self, path): - self._u_invoke_client("init", directory=path) - def _rcs_get_user_id(self): - status,output,error = self._u_invoke_client("showconfig","ui.username") - return output.rstrip('\n') - def _rcs_set_user_id(self, value): - """ - Supported by the Config Extension, but that is not part of - standard Mercurial. - http://www.selenic.com/mercurial/wiki/index.cgi/ConfigExtension - """ - raise rcs.SettingIDnotSupported - def _rcs_add(self, path): - self._u_invoke_client("add", path) - def _rcs_remove(self, path): - self._u_invoke_client("rm", path) - def _rcs_update(self, path): - pass - def _rcs_get_file_contents(self, path, revision=None, binary=False): - if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) - else: - status,output,error = \ - self._u_invoke_client("cat","-r",revision,path) - return output - def _rcs_duplicate_repo(self, directory, revision=None): - if revision == None: - return RCS._rcs_duplicate_repo(self, directory, revision) - else: - self._u_invoke_client("archive", "--rev", revision, directory) - def _rcs_commit(self, commitfile): - self._u_invoke_client('commit', '--logfile', commitfile) - status,output,error = self._u_invoke_client('identify') - revision = None - revline = re.compile("(.*) tip") - match = revline.search(output) - assert match != None, output+error - assert len(match.groups()) == 1 - revision = match.groups()[0] - return revision - - -rcs.make_rcs_testcase_subclasses(Hg, sys.modules[__name__]) - -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/mapfile.py b/libbe/mapfile.py index 9ff2215..e69de29 100644 --- a/libbe/mapfile.py +++ b/libbe/mapfile.py @@ -1,128 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 yaml -import os.path -import errno -import utility -import doctest - -class IllegalKey(Exception): - def __init__(self, key): - Exception.__init__(self, 'Illegal key "%s"' % key) - self.key = key - -class IllegalValue(Exception): - def __init__(self, value): - Exception.__init__(self, 'Illegal value "%s"' % value) - self.value = value - -def generate(map): - """Generate a YAML mapfile content string. - >>> generate({"q":"p"}) - 'q: p\\n\\n' - >>> generate({"q":u"Fran\u00e7ais"}) - 'q: Fran\\xc3\\xa7ais\\n\\n' - >>> generate({"q":u"hello"}) - 'q: hello\\n\\n' - >>> generate({"q=":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "q=" - >>> generate({"q:":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "q:" - >>> generate({"q\\n":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "q\\n" - >>> generate({"":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "" - >>> generate({">q":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key ">q" - >>> generate({"q":"p\\n"}) - Traceback (most recent call last): - IllegalValue: Illegal value "p\\n" - """ - keys = map.keys() - keys.sort() - for key in keys: - try: - assert not key.startswith('>') - assert('\n' not in key) - assert('=' not in key) - assert(':' not in key) - assert(len(key) > 0) - except AssertionError: - raise IllegalKey(key.encode('string_escape')) - if "\n" in map[key]: - raise IllegalValue(map[key].encode('string_escape')) - - lines = [] - for key in keys: - lines.append(yaml.safe_dump({key: map[key]}, - default_flow_style=False, - allow_unicode=True)) - lines.append("") - return '\n'.join(lines) - -def parse(contents): - """ - Parse a YAML mapfile string. - >>> parse('q: p\\n\\n')['q'] - 'p' - >>> parse('q: \\'p\\'\\n\\n')['q'] - 'p' - >>> contents = generate({"a":"b", "c":"d", "e":"f"}) - >>> dict = parse(contents) - >>> dict["a"] - 'b' - >>> dict["c"] - 'd' - >>> dict["e"] - 'f' - """ - old_format = False - for line in contents.splitlines(): - if len(line.split("=")) == 2: - old_format = True - break - if old_format: # translate to YAML. Hack to deal with old BE bugs. - newlines = [] - for line in contents.splitlines(): - line = line.rstrip('\n') - if len(line) == 0: - continue - fields = line.split("=") - if len(fields) == 2: - key,value = fields - newlines.append('%s: "%s"' % (key, value.replace('"','\\"'))) - else: - newlines.append(line) - contents = '\n'.join(newlines) - return yaml.load(contents) or {} - -def map_save(rcs, path, map, allow_no_rcs=False): - """Save the map as a mapfile to the specified path""" - contents = generate(map) - rcs.set_file_contents(path, contents, allow_no_rcs) - -def map_load(rcs, path, allow_no_rcs=False): - contents = rcs.get_file_contents(path, allow_no_rcs=allow_no_rcs) - return parse(contents) - -suite = doctest.DocTestSuite() diff --git a/libbe/plugin.py b/libbe/plugin.py index 3c7c753..e69de29 100644 --- a/libbe/plugin.py +++ b/libbe/plugin.py @@ -1,72 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Marien Zwart -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 -import os.path -import sys -import doctest - -def my_import(mod_name): - module = __import__(mod_name) - components = mod_name.split('.') - for comp in components[1:]: - module = getattr(module, comp) - return module - -def iter_plugins(prefix): - """ - >>> "list" in [n for n,m in iter_plugins("becommands")] - True - >>> "plugin" in [n for n,m in iter_plugins("libbe")] - True - """ - modfiles = os.listdir(os.path.join(plugin_path, prefix)) - modfiles.sort() - for modfile in modfiles: - if modfile.startswith('.'): - continue # the occasional emacs temporary file - if modfile.endswith(".py") and modfile != "__init__.py": - yield modfile[:-3], my_import(prefix+"."+modfile[:-3]) - - -def get_plugin(prefix, name): - """ - >>> get_plugin("becommands", "asdf") is None - True - >>> q = repr(get_plugin("becommands", "list")) - >>> q.startswith(" -# -# 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 -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# 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. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -This module provides a series of useful decorators for defining -various types of properties. For example usage, consider the -unittests at the end of the module. - -See - http://www.python.org/dev/peps/pep-0318/ -and - http://www.phyast.pitt.edu/~micheles/python/documentation.html -for more information on decorators. -""" - -import copy -import types -import unittest - - -class ValueCheckError (ValueError): - def __init__(self, name, value, allowed): - action = "in" # some list of allowed values - if type(allowed) == types.FunctionType: - action = "allowed by" # some allowed-value check function - msg = "%s not %s %s for %s" % (value, action, allowed, name) - ValueError.__init__(self, msg) - self.name = name - self.value = value - self.allowed = allowed - -def Property(funcs): - """ - End a chain of property decorators, returning a property. - """ - args = {} - args["fget"] = funcs.get("fget", None) - args["fset"] = funcs.get("fset", None) - args["fdel"] = funcs.get("fdel", None) - args["doc"] = funcs.get("doc", None) - - #print "Creating a property with" - #for key, val in args.items(): print key, value - return property(**args) - -def doc_property(doc=None): - """ - Add a docstring to a chain of property decorators. - """ - def decorator(funcs=None): - """ - Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...} - or a function fn() returning such a dict. - """ - if hasattr(funcs, "__call__"): - funcs = funcs() # convert from function-arg to dict - funcs["doc"] = doc - return funcs - return decorator - -def local_property(name, null=None, mutable_null=False): - """ - Define get/set access to per-parent-instance local storage. Uses - .__value to store the value for a particular owner instance. - If the .__value attribute does not exist, returns null. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget", None) - fset = funcs.get("fset", None) - def _fget(self): - if fget is not None: - fget(self) - if mutable_null == True: - ret_null = copy.deepcopy(null) - else: - ret_null = null - value = getattr(self, "_%s_value" % name, ret_null) - return value - def _fset(self, value): - setattr(self, "_%s_value" % name, value) - if fset is not None: - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - funcs["name"] = name - return funcs - return decorator - -def settings_property(name, null=None): - """ - Similar to local_property, except where local_property stores the - value in instance.__value, settings_property stores the - value in instance.settings[name]. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget", None) - fset = funcs.get("fset", None) - def _fget(self): - if fget is not None: - fget(self) - value = self.settings.get(name, null) - return value - def _fset(self, value): - self.settings[name] = value - if fset is not None: - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - funcs["name"] = name - return funcs - return decorator - - -# Allow comparison and caching with _original_ values for mutables, -# since -# -# >>> a = [] -# >>> b = a -# >>> b.append(1) -# >>> a -# [1] -# >>> a==b -# True -def _hash_mutable_value(value): - return repr(value) -def _init_mutable_property_cache(self): - if not hasattr(self, "_mutable_property_cache_hash"): - # first call to _fget for any mutable property - self._mutable_property_cache_hash = {} - self._mutable_property_cache_copy = {} -def _set_cached_mutable_property(self, cacher_name, property_name, value): - _init_mutable_property_cache(self) - self._mutable_property_cache_hash[(cacher_name, property_name)] = \ - _hash_mutable_value(value) - self._mutable_property_cache_copy[(cacher_name, property_name)] = \ - copy.deepcopy(value) -def _get_cached_mutable_property(self, cacher_name, property_name, default=None): - _init_mutable_property_cache(self) - if (cacher_name, property_name) not in self._mutable_property_cache_copy: - return default - return self._mutable_property_cache_copy[(cacher_name, property_name)] -def _cmp_cached_mutable_property(self, cacher_name, property_name, value): - _init_mutable_property_cache(self) - if (cacher_name, property_name) not in self._mutable_property_cache_hash: - return 1 # any value > non-existant old hash - old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)] - return cmp(_hash_mutable_value(value), old_hash) - - -def defaulting_property(default=None, null=None, - default_mutable=False, - null_mutable=False): - """ - Define a default value for get access to a property. - If the stored value is null, then default is returned. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self): - value = fget(self) - if value == null: - if default_mutable == True: - return copy.deepcopy(default) - else: - return default - return value - def _fset(self, value): - if value == default: - if null_mutable == True: - value = copy.deepcopy(null) - else: - value = null - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - -def fn_checked_property(value_allowed_fn): - """ - Define allowed values for get/set access to a property. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self): - value = fget(self) - if value_allowed_fn(value) != True: - raise ValueCheckError(name, value, value_allowed_fn) - return value - def _fset(self, value): - if value_allowed_fn(value) != True: - raise ValueCheckError(name, value, value_allowed_fn) - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - -def checked_property(allowed=[]): - """ - Define allowed values for get/set access to a property. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self): - value = fget(self) - if value not in allowed: - raise ValueCheckError(name, value, allowed) - return value - def _fset(self, value): - if value not in allowed: - raise ValueCheckError(name, value, allowed) - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - -def cached_property(generator, initVal=None, mutable=False): - """ - Allow caching of values generated by generator(instance), where - instance is the instance to which this property belongs. Uses - .__cache to store a cache flag for a particular owner - instance. - - When the cache flag is True or missing and the stored value is - initVal, the first fget call triggers the generator function, - whose output is stored in __cached_value. That and - subsequent calls to fget will return this cached value. - - If the input value is no longer initVal (e.g. a value has been - loaded from disk or set with fset), that value overrides any - cached value, and this property has no effect. - - When the cache flag is False and the stored value is initVal, the - generator is not cached, but is called on every fget. - - The cache flag is missing on initialization. Particular instances - may override by setting their own flag. - - In the case that mutable == True, all caching is disabled and the - generator is called whenever the cached value would otherwise be - used. This avoids uncertainties in the value of stored mutables. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - name = funcs.get("name", "") - def _fget(self): - cache = getattr(self, "_%s_cache" % name, True) - value = fget(self) - if value == initVal: - if cache == True and mutable == False: - if hasattr(self, "_%s_cached_value" % name): - value = getattr(self, "_%s_cached_value" % name) - else: - value = generator(self) - setattr(self, "_%s_cached_value" % name, value) - else: - value = generator(self) - return value - funcs["fget"] = _fget - return funcs - return decorator - -def primed_property(primer, initVal=None): - """ - Just like a generator_property, except that instead of returning a - new value and running fset to cache it, the primer performs some - background manipulation (e.g. loads data into instance.settings) - such that a _second_ pass through fget succeeds. - - The 'cache' flag becomes a 'prime' flag, with priming taking place - whenever .__prime is True, or is False or missing and - value == initVal. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - name = funcs.get("name", "") - def _fget(self): - prime = getattr(self, "_%s_prime" % name, False) - if prime == False: - value = fget(self) - if prime == True or (prime == False and value == initVal): - primer(self) - value = fget(self) - return value - funcs["fget"] = _fget - return funcs - return decorator - -def change_hook_property(hook, mutable=False): - """ - Call the function hook(instance, old_value, new_value) whenever a - value different from the current value is set (instance is a a - reference to the class instance to which this property belongs). - This is useful for saving changes to disk, etc. This function is - called _after_ the new value has been stored, allowing you to - change the stored value if you want. - - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self, new_value=None, from_fset=False): # only used if mutable == True - value = fget(self) - if _cmp_cached_mutable_property(self, "change hook property", name, value) != 0: - # there has been a change, cache new value - old_value = _get_cached_mutable_property(self, "change hook property", name) - _set_cached_mutable_property(self, "change hook property", name, value) - if from_fset == True: # return previously cached value - value = old_value - else: # the value changed while we weren't looking - hook(self, old_value, value) - return value - def _fset(self, value): - if mutable == True: # get cached previous value - old_value = _fget(self, new_value=value, from_fset=True) - else: - old_value = fget(self) - fset(self, value) - if value != old_value: - hook(self, old_value, value) - if mutable == True: - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - - -class DecoratorTests(unittest.TestCase): - def testLocalDoc(self): - class Test(object): - @Property - @doc_property("A fancy property") - def x(): - return {} - self.failUnless(Test.x.__doc__ == "A fancy property", - Test.x.__doc__) - def testLocalProperty(self): - class Test(object): - @Property - @local_property(name="LOCAL") - def x(): - return {} - t = Test() - self.failUnless(t.x == None, str(t.x)) - t.x = 'z' # the first set initializes ._LOCAL_value - self.failUnless(t.x == 'z', str(t.x)) - self.failUnless("_LOCAL_value" in dir(t), dir(t)) - self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value) - def testSettingsProperty(self): - class Test(object): - @Property - @settings_property(name="attr") - def x(): - return {} - def __init__(self): - self.settings = {} - t = Test() - self.failUnless(t.x == None, str(t.x)) - t.x = 'z' # the first set initializes ._LOCAL_value - self.failUnless(t.x == 'z', str(t.x)) - self.failUnless("attr" in t.settings, t.settings) - self.failUnless(t.settings["attr"] == 'z', t.settings["attr"]) - def testDefaultingLocalProperty(self): - class Test(object): - @Property - @defaulting_property(default='y', null='x') - @local_property(name="DEFAULT", null=5) - def x(): return {} - t = Test() - self.failUnless(t.x == 5, str(t.x)) - t.x = 'x' - self.failUnless(t.x == 'y', str(t.x)) - t.x = 'y' - self.failUnless(t.x == 'y', str(t.x)) - t.x = 'z' - self.failUnless(t.x == 'z', str(t.x)) - t.x = 5 - self.failUnless(t.x == 5, str(t.x)) - def testCheckedLocalProperty(self): - class Test(object): - @Property - @checked_property(allowed=['x', 'y', 'z']) - @local_property(name="CHECKED") - def x(): return {} - def __init__(self): - self._CHECKED_value = 'x' - t = Test() - self.failUnless(t.x == 'x', str(t.x)) - try: - t.x = None - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - def testTwoCheckedLocalProperties(self): - class Test(object): - @Property - @checked_property(allowed=['x', 'y', 'z']) - @local_property(name="X") - def x(): return {} - - @Property - @checked_property(allowed=['a', 'b', 'c']) - @local_property(name="A") - def a(): return {} - def __init__(self): - self._A_value = 'a' - self._X_value = 'x' - t = Test() - try: - t.x = 'a' - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - t.x = 'x' - t.x = 'y' - t.x = 'z' - try: - t.a = 'x' - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - t.a = 'a' - t.a = 'b' - t.a = 'c' - def testFnCheckedLocalProperty(self): - class Test(object): - @Property - @fn_checked_property(lambda v : v in ['x', 'y', 'z']) - @local_property(name="CHECKED") - def x(): return {} - def __init__(self): - self._CHECKED_value = 'x' - t = Test() - self.failUnless(t.x == 'x', str(t.x)) - try: - t.x = None - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - def testCachedLocalProperty(self): - class Gen(object): - def __init__(self): - self.i = 0 - def __call__(self, owner): - self.i += 1 - return self.i - class Test(object): - @Property - @cached_property(generator=Gen(), initVal=None) - @local_property(name="CACHED") - def x(): return {} - t = Test() - self.failIf("_CACHED_cache" in dir(t), getattr(t, "_CACHED_cache", None)) - self.failUnless(t.x == 1, t.x) - self.failUnless(t.x == 1, t.x) - self.failUnless(t.x == 1, t.x) - t.x = 8 - self.failUnless(t.x == 8, t.x) - self.failUnless(t.x == 8, t.x) - t._CACHED_cache = False # Caching is off, but the stored value - val = t.x # is 8, not the initVal (None), so we - self.failUnless(val == 8, val) # get 8. - t._CACHED_value = None # Now we've set the stored value to None - val = t.x # so future calls to fget (like this) - self.failUnless(val == 2, val) # will call the generator every time... - val = t.x - self.failUnless(val == 3, val) - val = t.x - self.failUnless(val == 4, val) - t._CACHED_cache = True # We turn caching back on, and get - self.failUnless(t.x == 1, str(t.x)) # the original cached value. - del t._CACHED_cached_value # Removing that value forces a - self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call - self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which - self.failUnless(t.x == 5, str(t.x)) # we get the new cached value. - def testPrimedLocalProperty(self): - class Test(object): - def prime(self): - self.settings["PRIMED"] = "initialized" - @Property - @primed_property(primer=prime, initVal=None) - @settings_property(name="PRIMED") - def x(): return {} - def __init__(self): - self.settings={} - t = Test() - self.failIf("_PRIMED_prime" in dir(t), getattr(t, "_PRIMED_prime", None)) - self.failUnless(t.x == "initialized", t.x) - t.x = 1 - self.failUnless(t.x == 1, t.x) - t.x = None - self.failUnless(t.x == "initialized", t.x) - t._PRIMED_prime = True - t.x = 3 - self.failUnless(t.x == "initialized", t.x) - t._PRIMED_prime = False - t.x = 3 - self.failUnless(t.x == 3, t.x) - def testChangeHookLocalProperty(self): - class Test(object): - def _hook(self, old, new): - self.old = old - self.new = new - - @Property - @change_hook_property(_hook) - @local_property(name="HOOKED") - def x(): return {} - t = Test() - t.x = 1 - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == 1, t.new) - t.x = 1 - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == 1, t.new) - t.x = 2 - self.failUnless(t.old == 1, t.old) - self.failUnless(t.new == 2, t.new) - def testChangeHookMutableProperty(self): - class Test(object): - def _hook(self, old, new): - self.old = old - self.new = new - self.hook_calls += 1 - - @Property - @change_hook_property(_hook, mutable=True) - @local_property(name="HOOKED") - def x(): return {} - t = Test() - t.hook_calls = 0 - t.x = [] - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == [], t.new) - a = t.x - a.append(5) - t.x = a - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5], t.new) - t.x = [] - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [], t.new) - # now append without reassigning. this doesn't trigger the - # change, since we don't ever set t.x, only get it and mess - # with it. It does, however, update our t.new, since t.new = - # t.x and is not a static copy. - t.x.append(5) - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [5], t.new) - # however, the next t.x get _will_ notice the change... - a = t.x - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5], t.new) - self.failUnless(t.hook_calls == 6, t.hook_calls) - t.x.append(6) # this append(6) is not noticed yet - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5,6], t.new) - self.failUnless(t.hook_calls == 6, t.hook_calls) - # this append(7) is not noticed, but the t.x get causes the - # append(6) to be noticed - t.x.append(7) - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [5,6,7], t.new) - self.failUnless(t.hook_calls == 7, t.hook_calls) - a = t.x # now the append(7) is noticed - self.failUnless(t.old == [5,6], t.old) - self.failUnless(t.new == [5,6,7], t.new) - self.failUnless(t.hook_calls == 8, t.hook_calls) - - -suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) - diff --git a/libbe/rcs.py b/libbe/rcs.py index 2c416f4..e69de29 100644 --- a/libbe/rcs.py +++ b/libbe/rcs.py @@ -1,838 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Alexander Belchenko -# Ben Finney -# Chris Ball -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 - -from subprocess import Popen, PIPE -import codecs -import os -import os.path -import re -from socket import gethostname -import shutil -import sys -import tempfile -import unittest -import doctest - -from utility import Dir, search_parent_directories - - -def _get_matching_rcs(matchfn): - """Return the first module for which matchfn(RCS_instance) is true""" - import arch - import bzr - import darcs - import git - import hg - for module in [arch, bzr, darcs, git, hg]: - rcs = module.new() - if matchfn(rcs) == True: - return rcs - del(rcs) - return RCS() - -def rcs_by_name(rcs_name): - """Return the module for the RCS with the given name""" - return _get_matching_rcs(lambda rcs: rcs.name == rcs_name) - -def detect_rcs(dir): - """Return an RCS instance for the rcs being used in this directory""" - return _get_matching_rcs(lambda rcs: rcs.detect(dir)) - -def installed_rcs(): - """Return an instance of an installed RCS""" - return _get_matching_rcs(lambda rcs: rcs.installed()) - - -class CommandError(Exception): - def __init__(self, err_str, status): - Exception.__init__(self, "Command failed (%d): %s" % (status, err_str)) - self.err_str = err_str - self.status = status - -class SettingIDnotSupported(NotImplementedError): - pass - -class RCSnotRooted(Exception): - def __init__(self): - msg = "RCS not rooted" - Exception.__init__(self, msg) - -class PathNotInRoot(Exception): - def __init__(self, path, root): - msg = "Path '%s' not in root '%s'" % (path, root) - Exception.__init__(self, msg) - self.path = path - self.root = root - -class NoSuchFile(Exception): - def __init__(self, pathname, root="."): - path = os.path.abspath(os.path.join(root, pathname)) - Exception.__init__(self, "No such file: %s" % path) - - -def new(): - return RCS() - -class RCS(object): - """ - This class implements a 'no-rcs' interface. - - Support for other RCSs can be added by subclassing this class, and - overriding methods _rcs_*() with code appropriate for your RCS. - - The methods _u_*() are utility methods available to the _rcs_*() - methods. - """ - name = "None" - client = "" # command-line tool for _u_invoke_client - versioned = False - def __init__(self, paranoid=False, encoding=sys.getdefaultencoding()): - self.paranoid = paranoid - self.verboseInvoke = False - self.rootdir = None - self._duplicateBasedir = None - self._duplicateDirname = None - self.encoding = encoding - def __del__(self): - self.cleanup() - - def _rcs_help(self): - """ - Return the command help string. - (Allows a simple test to see if the client is installed.) - """ - pass - def _rcs_detect(self, path=None): - """ - Detect whether a directory is revision controlled with this RCS. - """ - return True - def _rcs_root(self, path): - """ - Get the RCS root. This is the default working directory for - future invocations. You would normally set this to the root - directory for your RCS. - """ - if os.path.isdir(path)==False: - path = os.path.dirname(path) - if path == "": - path = os.path.abspath(".") - return path - def _rcs_init(self, path): - """ - Begin versioning the tree based at path. - """ - pass - def _rcs_cleanup(self): - """ - Remove any cruft that _rcs_init() created outside of the - versioned tree. - """ - pass - def _rcs_get_user_id(self): - """ - Get the RCS's suggested user id (e.g. "John Doe "). - If the RCS has not been configured with a username, return None. - """ - return None - def _rcs_set_user_id(self, value): - """ - Set the RCS's suggested user id (e.g "John Doe "). - This is run if the RCS has not been configured with a usename, so - that commits will have a reasonable FROM value. - """ - raise SettingIDnotSupported - def _rcs_add(self, path): - """ - Add the already created file at path to version control. - """ - pass - def _rcs_remove(self, path): - """ - Remove the file at path from version control. Optionally - remove the file from the filesystem as well. - """ - pass - def _rcs_update(self, path): - """ - Notify the versioning system of changes to the versioned file - at path. - """ - pass - def _rcs_get_file_contents(self, path, revision=None, binary=False): - """ - Get the file contents as they were in a given revision. - Revision==None specifies the current revision. - """ - assert revision == None, \ - "The %s RCS does not support revision specifiers" % self.name - if binary == False: - f = codecs.open(os.path.join(self.rootdir, path), "r", self.encoding) - else: - f = open(path, "rb") - contents = f.read() - f.close() - return contents - def _rcs_duplicate_repo(self, directory, revision=None): - """ - Get the repository as it was in a given revision. - revision==None specifies the current revision. - dir specifies a directory to create the duplicate in. - """ - shutil.copytree(self.rootdir, directory, True) - def _rcs_commit(self, commitfile): - """ - Commit the current working directory, using the contents of - commitfile as the comment. Return the name of the old - revision. - """ - return None - def installed(self): - try: - self._rcs_help() - return True - except OSError, e: - if e.errno == errno.ENOENT: - return False - except CommandError: - return False - def detect(self, path="."): - """ - Detect whether a directory is revision controlled with this RCS. - """ - return self._rcs_detect(path) - def root(self, path): - """ - Set the root directory to the path's RCS root. This is the - default working directory for future invocations. - """ - self.rootdir = self._rcs_root(path) - def init(self, path): - """ - Begin versioning the tree based at path. - Also roots the rcs at path. - """ - if os.path.isdir(path)==False: - path = os.path.dirname(path) - self._rcs_init(path) - self.root(path) - def cleanup(self): - self._rcs_cleanup() - def get_user_id(self): - """ - Get the RCS's suggested user id (e.g. "John Doe "). - If the RCS has not been configured with a username, return the user's - id. You can override the automatic lookup procedure by setting the - RCS.user_id attribute to a string of your choice. - """ - if hasattr(self, "user_id"): - if self.user_id != None: - return self.user_id - id = self._rcs_get_user_id() - if id == None: - name = self._u_get_fallback_username() - email = self._u_get_fallback_email() - id = self._u_create_id(name, email) - print >> sys.stderr, "Guessing id '%s'" % id - try: - self.set_user_id(id) - except SettingIDnotSupported: - pass - return id - def set_user_id(self, value): - """ - Set the RCS's suggested user id (e.g "John Doe "). - This is run if the RCS has not been configured with a usename, so - that commits will have a reasonable FROM value. - """ - self._rcs_set_user_id(value) - def add(self, path): - """ - Add the already created file at path to version control. - """ - self._rcs_add(self._u_rel_path(path)) - def remove(self, path): - """ - Remove a file from both version control and the filesystem. - """ - self._rcs_remove(self._u_rel_path(path)) - if os.path.exists(path): - os.remove(path) - def recursive_remove(self, dirname): - """ - Remove a file/directory and all its decendents from both - version control and the filesystem. - """ - if not os.path.exists(dirname): - raise NoSuchFile(dirname) - for dirpath,dirnames,filenames in os.walk(dirname, topdown=False): - filenames.extend(dirnames) - for path in filenames: - fullpath = os.path.join(dirpath, path) - if os.path.exists(fullpath) == False: - continue - self._rcs_remove(self._u_rel_path(fullpath)) - if os.path.exists(dirname): - shutil.rmtree(dirname) - def update(self, path): - """ - Notify the versioning system of changes to the versioned file - at path. - """ - self._rcs_update(self._u_rel_path(path)) - def get_file_contents(self, path, revision=None, allow_no_rcs=False, binary=False): - """ - Get the file as it was in a given revision. - Revision==None specifies the current revision. - """ - if not os.path.exists(path): - raise NoSuchFile(path) - if self._use_rcs(path, allow_no_rcs): - relpath = self._u_rel_path(path) - contents = self._rcs_get_file_contents(relpath,revision,binary=binary) - else: - f = codecs.open(path, "r", self.encoding) - contents = f.read() - f.close() - return contents - def set_file_contents(self, path, contents, allow_no_rcs=False, binary=False): - """ - Set the file contents under version control. - """ - add = not os.path.exists(path) - if binary == False: - f = codecs.open(path, "w", self.encoding) - else: - f = open(path, "wb") - f.write(contents) - f.close() - - if self._use_rcs(path, allow_no_rcs): - if add: - self.add(path) - else: - self.update(path) - def mkdir(self, path, allow_no_rcs=False): - """ - Create (if neccessary) a directory at path under version - control. - """ - if not os.path.exists(path): - os.mkdir(path) - if self._use_rcs(path, allow_no_rcs): - self.add(path) - else: - assert os.path.isdir(path) - if self._use_rcs(path, allow_no_rcs): - self.update(path) - def duplicate_repo(self, revision=None): - """ - Get the repository as it was in a given revision. - revision==None specifies the current revision. - Return the path to the arbitrary directory at the base of the new repo. - """ - # Dirname in Baseir to protect against simlink attacks. - if self._duplicateBasedir == None: - self._duplicateBasedir = tempfile.mkdtemp(prefix='BErcs') - self._duplicateDirname = \ - os.path.join(self._duplicateBasedir, "duplicate") - self._rcs_duplicate_repo(directory=self._duplicateDirname, - revision=revision) - return self._duplicateDirname - def remove_duplicate_repo(self): - """ - Clean up a duplicate repo created with duplicate_repo(). - """ - if self._duplicateBasedir != None: - shutil.rmtree(self._duplicateBasedir) - self._duplicateBasedir = None - self._duplicateDirname = None - def commit(self, summary, body=None): - """ - Commit the current working directory, with a commit message - string summary and body. Return the name of the old revision - (or None if versioning is not supported). - """ - if body is not None: - summary += '\n' + body - descriptor, filename = tempfile.mkstemp() - revision = None - try: - temp_file = os.fdopen(descriptor, 'wb') - temp_file.write(summary) - temp_file.flush() - revision = self._rcs_commit(filename) - temp_file.close() - finally: - os.remove(filename) - return revision - def precommit(self, directory): - pass - def postcommit(self, directory): - pass - def _u_invoke(self, args, stdin=None, expect=(0,), cwd=None): - if cwd == None: - cwd = self.rootdir - if self.verboseInvoke == True: - print >> sys.stderr, "%s$ %s" % (cwd, " ".join(args)) - try : - if sys.platform != "win32": - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd) - else: - # win32 don't have os.execvp() so have to run command in a shell - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, - shell=True, cwd=cwd) - except OSError, e : - strerror = "%s\nwhile executing %s" % (e.args[1], args) - raise CommandError(strerror, e.args[0]) - output, error = q.communicate(input=stdin) - status = q.wait() - if self.verboseInvoke == True: - print >> sys.stderr, "%d\n%s%s" % (status, output, error) - if status not in expect: - strerror = "%s\nwhile executing %s\n%s" % (args[1], args, error) - raise CommandError(strerror, status) - return status, output, error - def _u_invoke_client(self, *args, **kwargs): - directory = kwargs.get('directory',None) - expect = kwargs.get('expect', (0,)) - stdin = kwargs.get('stdin', None) - cl_args = [self.client] - cl_args.extend(args) - return self._u_invoke(cl_args, stdin=stdin,expect=expect,cwd=directory) - def _u_search_parent_directories(self, path, filename): - """ - Find the file (or directory) named filename in path or in any - of path's parents. - - e.g. - search_parent_directories("/a/b/c", ".be") - will return the path to the first existing file from - /a/b/c/.be - /a/b/.be - /a/.be - /.be - or None if none of those files exist. - """ - return search_parent_directories(path, filename) - def _use_rcs(self, path, allow_no_rcs): - """ - Try and decide if _rcs_add/update/mkdir/etc calls will - succeed. Returns True is we think the rcs_call would - succeeed, and False otherwise. - """ - use_rcs = True - exception = None - if self.rootdir != None: - if self.path_in_root(path) == False: - use_rcs = False - exception = PathNotInRoot(path, self.rootdir) - else: - use_rcs = False - exception = RCSnotRooted - if use_rcs == False and allow_no_rcs==False: - raise exception - return use_rcs - def path_in_root(self, path, root=None): - """ - Return the relative path to path from root. - >>> rcs = new() - >>> rcs.path_in_root("/a.b/c/.be", "/a.b/c") - True - >>> rcs.path_in_root("/a.b/.be", "/a.b/c") - False - """ - if root == None: - if self.rootdir == None: - raise RCSnotRooted - root = self.rootdir - path = os.path.abspath(path) - absRoot = os.path.abspath(root) - absRootSlashedDir = os.path.join(absRoot,"") - if not path.startswith(absRootSlashedDir): - return False - return True - def _u_rel_path(self, path, root=None): - """ - Return the relative path to path from root. - >>> rcs = new() - >>> rcs._u_rel_path("/a.b/c/.be", "/a.b/c") - '.be' - """ - if root == None: - if self.rootdir == None: - raise RCSnotRooted - root = self.rootdir - path = os.path.abspath(path) - absRoot = os.path.abspath(root) - absRootSlashedDir = os.path.join(absRoot,"") - if not path.startswith(absRootSlashedDir): - raise PathNotInRoot(path, absRootSlashedDir) - assert path != absRootSlashedDir, \ - "file %s == root directory %s" % (path, absRootSlashedDir) - relpath = path[len(absRootSlashedDir):] - return relpath - def _u_abspath(self, path, root=None): - """ - Return the absolute path from a path realtive to root. - >>> rcs = new() - >>> rcs._u_abspath(".be", "/a.b/c") - '/a.b/c/.be' - """ - if root == None: - assert self.rootdir != None, "RCS not rooted" - root = self.rootdir - return os.path.abspath(os.path.join(root, path)) - def _u_create_id(self, name, email=None): - """ - >>> rcs = new() - >>> rcs._u_create_id("John Doe", "jdoe@example.com") - 'John Doe ' - >>> rcs._u_create_id("John Doe") - 'John Doe' - """ - assert len(name) > 0 - if email == None or len(email) == 0: - return name - else: - return "%s <%s>" % (name, email) - def _u_parse_id(self, value): - """ - >>> rcs = new() - >>> rcs._u_parse_id("John Doe ") - ('John Doe', 'jdoe@example.com') - >>> rcs._u_parse_id("John Doe") - ('John Doe', None) - >>> try: - ... rcs._u_parse_id("John Doe ") - ... except AssertionError: - ... print "Invalid match" - Invalid match - """ - emailexp = re.compile("(.*) <([^>]*)>(.*)") - match = emailexp.search(value) - if match == None: - email = None - name = value - else: - assert len(match.groups()) == 3 - assert match.groups()[2] == "", match.groups() - email = match.groups()[1] - name = match.groups()[0] - assert name != None - assert len(name) > 0 - return (name, email) - def _u_get_fallback_username(self): - name = None - for envariable in ["LOGNAME", "USERNAME"]: - if os.environ.has_key(envariable): - name = os.environ[envariable] - break - assert name != None - return name - def _u_get_fallback_email(self): - hostname = gethostname() - name = self._u_get_fallback_username() - return "%s@%s" % (name, hostname) - def _u_parse_commitfile(self, commitfile): - """ - Split the commitfile created in self.commit() back into - summary and header lines. - """ - f = codecs.open(commitfile, "r", self.encoding) - summary = f.readline() - body = f.read() - body.lstrip('\n') - if len(body) == 0: - body = None - f.close() - return (summary, body) - - -def setup_rcs_test_fixtures(testcase): - """Set up test fixtures for RCS test case.""" - testcase.rcs = testcase.Class() - testcase.dir = Dir() - testcase.dirname = testcase.dir.path - - rcs_not_supporting_uninitialized_user_id = [] - rcs_not_supporting_set_user_id = ["None", "hg"] - testcase.rcs_supports_uninitialized_user_id = ( - testcase.rcs.name not in rcs_not_supporting_uninitialized_user_id) - testcase.rcs_supports_set_user_id = ( - testcase.rcs.name not in rcs_not_supporting_set_user_id) - - if not testcase.rcs.installed(): - testcase.fail( - "%(name)s RCS not found" % vars(testcase.Class)) - - if testcase.Class.name != "None": - testcase.failIf( - testcase.rcs.detect(testcase.dirname), - "Detected %(name)s RCS before initialising" - % vars(testcase.Class)) - - testcase.rcs.init(testcase.dirname) - - -class RCSTestCase(unittest.TestCase): - """Test cases for base RCS class.""" - - Class = RCS - - def __init__(self, *args, **kwargs): - super(RCSTestCase, self).__init__(*args, **kwargs) - self.dirname = None - - def setUp(self): - super(RCSTestCase, self).setUp() - setup_rcs_test_fixtures(self) - - def tearDown(self): - del(self.rcs) - super(RCSTestCase, self).tearDown() - - def full_path(self, rel_path): - return os.path.join(self.dirname, rel_path) - - -class RCS_init_TestCase(RCSTestCase): - """Test cases for RCS.init method.""" - - def test_detect_should_succeed_after_init(self): - """Should detect RCS in directory after initialization.""" - self.failUnless( - self.rcs.detect(self.dirname), - "Did not detect %(name)s RCS after initialising" - % vars(self.Class)) - - def test_rcs_rootdir_in_specified_root_path(self): - """RCS root directory should be in specified root path.""" - rp = os.path.realpath(self.rcs.rootdir) - dp = os.path.realpath(self.dirname) - rcs_name = self.Class.name - self.failUnless( - dp == rp or rp == None, - "%(rcs_name)s RCS root in wrong dir (%(dp)s %(rp)s)" % vars()) - - -class RCS_get_user_id_TestCase(RCSTestCase): - """Test cases for RCS.get_user_id method.""" - - def test_gets_existing_user_id(self): - """Should get the existing user ID.""" - if not self.rcs_supports_uninitialized_user_id: - return - - user_id = self.rcs.get_user_id() - self.failUnless( - user_id is not None, - "unable to get a user id") - - -class RCS_set_user_id_TestCase(RCSTestCase): - """Test cases for RCS.set_user_id method.""" - - def setUp(self): - super(RCS_set_user_id_TestCase, self).setUp() - - if self.rcs_supports_uninitialized_user_id: - self.prev_user_id = self.rcs.get_user_id() - else: - self.prev_user_id = "Uninitialized identity " - - if self.rcs_supports_set_user_id: - self.test_new_user_id = "John Doe " - self.rcs.set_user_id(self.test_new_user_id) - - def tearDown(self): - if self.rcs_supports_set_user_id: - self.rcs.set_user_id(self.prev_user_id) - super(RCS_set_user_id_TestCase, self).tearDown() - - def test_raises_error_in_unsupported_vcs(self): - """Should raise an error in a VCS that doesn't support it.""" - if self.rcs_supports_set_user_id: - return - self.assertRaises( - SettingIDnotSupported, - self.rcs.set_user_id, "foo") - - def test_updates_user_id_in_supporting_rcs(self): - """Should update the user ID in an RCS that supports it.""" - if not self.rcs_supports_set_user_id: - return - user_id = self.rcs.get_user_id() - self.failUnlessEqual( - self.test_new_user_id, user_id, - "user id not set correctly (expected %s, got %s)" - % (self.test_new_user_id, user_id)) - - -def setup_rcs_revision_test_fixtures(testcase): - """Set up revision test fixtures for RCS test case.""" - testcase.test_dirs = ['a', 'a/b', 'c'] - for path in testcase.test_dirs: - testcase.rcs.mkdir(testcase.full_path(path)) - - testcase.test_files = ['a/text', 'a/b/text'] - - testcase.test_contents = { - 'rev_1': "Lorem ipsum", - 'uncommitted': "dolor sit amet", - } - - -class RCS_mkdir_TestCase(RCSTestCase): - """Test cases for RCS.mkdir method.""" - - def setUp(self): - super(RCS_mkdir_TestCase, self).setUp() - setup_rcs_revision_test_fixtures(self) - - def tearDown(self): - for path in reversed(sorted(self.test_dirs)): - self.rcs.recursive_remove(self.full_path(path)) - super(RCS_mkdir_TestCase, self).tearDown() - - def test_mkdir_creates_directory(self): - """Should create specified directory in filesystem.""" - for path in self.test_dirs: - full_path = self.full_path(path) - self.failUnless( - os.path.exists(full_path), - "path %(full_path)s does not exist" % vars()) - - -class RCS_commit_TestCase(RCSTestCase): - """Test cases for RCS.commit method.""" - - def setUp(self): - super(RCS_commit_TestCase, self).setUp() - setup_rcs_revision_test_fixtures(self) - - def tearDown(self): - for path in reversed(sorted(self.test_dirs)): - self.rcs.recursive_remove(self.full_path(path)) - super(RCS_commit_TestCase, self).tearDown() - - def test_file_contents_as_specified(self): - """Should set file contents as specified.""" - test_contents = self.test_contents['rev_1'] - for path in self.test_files: - full_path = self.full_path(path) - self.rcs.set_file_contents(full_path, test_contents) - current_contents = self.rcs.get_file_contents(full_path) - self.failUnlessEqual(test_contents, current_contents) - - def test_file_contents_as_committed(self): - """Should have file contents as specified after commit.""" - test_contents = self.test_contents['rev_1'] - for path in self.test_files: - full_path = self.full_path(path) - self.rcs.set_file_contents(full_path, test_contents) - revision = self.rcs.commit("Initial file contents.") - current_contents = self.rcs.get_file_contents(full_path) - self.failUnlessEqual(test_contents, current_contents) - - def test_file_contents_as_set_when_uncommitted(self): - """Should set file contents as specified after commit.""" - if not self.rcs.versioned: - return - for path in self.test_files: - full_path = self.full_path(path) - self.rcs.set_file_contents( - full_path, self.test_contents['rev_1']) - revision = self.rcs.commit("Initial file contents.") - self.rcs.set_file_contents( - full_path, self.test_contents['uncommitted']) - current_contents = self.rcs.get_file_contents(full_path) - self.failUnlessEqual( - self.test_contents['uncommitted'], current_contents) - - def test_revision_file_contents_as_committed(self): - """Should get file contents as committed to specified revision.""" - if not self.rcs.versioned: - return - for path in self.test_files: - full_path = self.full_path(path) - self.rcs.set_file_contents( - full_path, self.test_contents['rev_1']) - revision = self.rcs.commit("Initial file contents.") - self.rcs.set_file_contents( - full_path, self.test_contents['uncommitted']) - committed_contents = self.rcs.get_file_contents( - full_path, revision) - self.failUnlessEqual( - self.test_contents['rev_1'], committed_contents) - - -class RCS_duplicate_repo_TestCase(RCSTestCase): - """Test cases for RCS.duplicate_repo method.""" - - def setUp(self): - super(RCS_duplicate_repo_TestCase, self).setUp() - setup_rcs_revision_test_fixtures(self) - - def tearDown(self): - self.rcs.remove_duplicate_repo() - for path in reversed(sorted(self.test_dirs)): - self.rcs.recursive_remove(self.full_path(path)) - super(RCS_duplicate_repo_TestCase, self).tearDown() - - def test_revision_file_contents_as_committed(self): - """Should match file contents as committed to specified revision.""" - if not self.rcs.versioned: - return - for path in self.test_files: - full_path = self.full_path(path) - self.rcs.set_file_contents( - full_path, self.test_contents['rev_1']) - revision = self.rcs.commit("Commit current status") - self.rcs.set_file_contents( - full_path, self.test_contents['uncommitted']) - dup_repo_path = self.rcs.duplicate_repo(revision) - dup_file_path = os.path.join(dup_repo_path, path) - dup_file_contents = file(dup_file_path, 'rb').read() - self.failUnlessEqual( - self.test_contents['rev_1'], dup_file_contents) - self.rcs.remove_duplicate_repo() - - -def make_rcs_testcase_subclasses(rcs_class, namespace): - """Make RCSTestCase subclasses for rcs_class in the namespace.""" - rcs_testcase_classes = [ - c for c in ( - ob for ob in globals().values() if isinstance(ob, type)) - if issubclass(c, RCSTestCase)] - - for base_class in rcs_testcase_classes: - testcase_class_name = rcs_class.__name__ + base_class.__name__ - testcase_class_bases = (base_class,) - testcase_class_dict = dict(base_class.__dict__) - testcase_class_dict['Class'] = rcs_class - testcase_class = type( - testcase_class_name, testcase_class_bases, testcase_class_dict) - setattr(namespace, testcase_class_name, testcase_class) - - -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/settings_object.py b/libbe/settings_object.py index 7326d1b..e69de29 100644 --- a/libbe/settings_object.py +++ b/libbe/settings_object.py @@ -1,411 +0,0 @@ -# Bugs Everywhere - a distributed bugtracker -# Copyright (C) 2008-2009 W. Trevor King -# -# 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 -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# 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. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -This module provides a base class implementing settings-dict based -property storage useful for BE objects with saved properties -(e.g. BugDir, Bug, Comment). For example usage, consider the -unittests at the end of the module. -""" - -import doctest -import unittest - -from properties import Property, doc_property, local_property, \ - defaulting_property, checked_property, fn_checked_property, \ - cached_property, primed_property, change_hook_property, \ - settings_property - - -class _Token (object): - """ - `Control' value class for properties. We want values that only - mean something to the settings_object module. - """ - pass - -class UNPRIMED (_Token): - "Property has not been primed." - pass - -class EMPTY (_Token): - """ - Property has been primed but has no user-set value, so use - default/generator value. - """ - pass - - -def prop_save_settings(self, old, new): - """ - The default action undertaken when a property changes. - """ - if self.sync_with_disk==True: - self.save_settings() - -def prop_load_settings(self): - """ - The default action undertaken when an UNPRIMED property is accessed. - """ - if self.sync_with_disk==True and self._settings_loaded==False: - self.load_settings() - else: - self._setup_saved_settings(flag_as_loaded=False) - -# Some name-mangling routines for pretty printing setting names -def setting_name_to_attr_name(self, name): - """ - Convert keys to the .settings dict into their associated - SavedSettingsObject attribute names. - >>> print setting_name_to_attr_name(None,"User-id") - user_id - """ - return name.lower().replace('-', '_') - -def attr_name_to_setting_name(self, name): - """ - The inverse of setting_name_to_attr_name. - >>> print attr_name_to_setting_name(None, "user_id") - User-id - """ - return name.capitalize().replace('_', '-') - - -def versioned_property(name, doc, - default=None, generator=None, - change_hook=prop_save_settings, - mutable=False, - primer=prop_load_settings, - allowed=None, check_fn=None, - settings_properties=[], - required_saved_properties=[], - require_save=False): - """ - Combine the common decorators in a single function. - - Use zero or one (but not both) of default or generator, since a - working default will keep the generator from functioning. Use the - default if you know what you want the default value to be at - 'coding time'. Use the generator if you can write a function to - determine a valid default at run time. If both default and - generator are None, then the property will be a defaulting - property which defaults to None. - - allowed and check_fn have a similar relationship, although you can - use both of these if you want. allowed compares the proposed - value against a list determined at 'coding time' and check_fn - allows more flexible comparisons to take place at run time. - - Set require_save to True if you want to save the default/generated - value for a property, to protect against future changes. E.g., we - currently expect all comments to be 'text/plain' but in the future - we may want to default to 'text/html'. If we don't want the old - comments to be interpreted as 'text/html', we would require that - the content type be saved. - - change_hook, primer, settings_properties, and - required_saved_properties are only options to get their defaults - into our local scope. Don't mess with them. - """ - settings_properties.append(name) - if require_save == True: - required_saved_properties.append(name) - def decorator(funcs): - fulldoc = doc - if default != None: - defaulting = defaulting_property(default=default, null=EMPTY, - default_mutable=mutable) - fulldoc += "\n\nThis property defaults to %s" % default - if generator != None: - cached = cached_property(generator=generator, initVal=EMPTY, - mutable=mutable) - fulldoc += "\n\nThis property is generated with %s" % generator - if check_fn != None: - fn_checked = fn_checked_property(value_allowed_fn=check_fn) - fulldoc += "\n\nThis property is checked with %s" % check_fn - if allowed != None: - checked = checked_property(allowed=allowed) - fulldoc += "\n\nThe allowed values for this property are: %s." \ - % (', '.join(allowed)) - hooked = change_hook_property(hook=change_hook, mutable=mutable) - primed = primed_property(primer=primer, initVal=UNPRIMED) - settings = settings_property(name=name, null=UNPRIMED) - docp = doc_property(doc=fulldoc) - deco = hooked(primed(settings(docp(funcs)))) - if default != None: - deco = defaulting(deco) - if generator != None: - deco = cached(deco) - if check_fn != None: - deco = fn_checked(deco) - if allowed != None: - deco = checked(deco) - return Property(deco) - return decorator - -class SavedSettingsObject(object): - - # Keep a list of properties that may be stored in the .settings dict. - #settings_properties = [] - - # A list of properties that we save to disk, even if they were - # never set (in which case we save the default value). This - # protects against future changes in default values. - #required_saved_properties = [] - - _setting_name_to_attr_name = setting_name_to_attr_name - _attr_name_to_setting_name = attr_name_to_setting_name - - def __init__(self): - self._settings_loaded = False - self.sync_with_disk = False - self.settings = {} - - def load_settings(self): - """Load the settings from disk.""" - # Override. Must call ._setup_saved_settings() after loading. - self.settings = {} - self._setup_saved_settings() - - def _setup_saved_settings(self, flag_as_loaded=True): - """ - To be run after setting self.settings up from disk. Marks all - settings as primed. - """ - for property in self.settings_properties: - if property not in self.settings: - self.settings[property] = EMPTY - elif self.settings[property] == UNPRIMED: - self.settings[property] = EMPTY - if flag_as_loaded == True: - self._settings_loaded = True - - def save_settings(self): - """Load the settings from disk.""" - # Override. Should save the dict output of ._get_saved_settings() - settings = self._get_saved_settings() - pass # write settings to disk.... - - def _get_saved_settings(self): - settings = {} - for k,v in self.settings.items(): - if v != None and v != EMPTY: - settings[k] = v - for k in self.required_saved_properties: - settings[k] = getattr(self, self._setting_name_to_attr_name(k)) - return settings - - def clear_cached_setting(self, setting=None): - "If setting=None, clear *all* cached settings" - if setting != None: - if hasattr(self, "_%s_cached_value" % setting): - delattr(self, "_%s_cached_value" % setting) - else: - for setting in settings_properties: - self.clear_cached_setting(setting) - - -class SavedSettingsObjectTests(unittest.TestCase): - def testSimpleProperty(self): - """Testing a minimal versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - settings_properties=settings_properties, - required_saved_properties=required_saved_properties) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - # access missing setting - self.failUnless(t._settings_loaded == False, t._settings_loaded) - self.failUnless(len(t.settings) == 0, len(t.settings)) - self.failUnless(t.content_type == EMPTY, t.content_type) - # accessing t.content_type triggers the priming, which runs - # t._setup_saved_settings, which fills out t.settings with - # EMPTY data. t._settings_loaded is still false though, since - # the default priming does not do any of the `official' loading - # that occurs in t.load_settings. - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - # load settings creates an EMPTY value in the settings array - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t.content_type == EMPTY, t.content_type) - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - # now we set a value - t.content_type = None - self.failUnless(t.settings["Content-type"] == None, - t.settings["Content-type"]) - self.failUnless(t.content_type == None, t.content_type) - self.failUnless(t.settings["Content-type"] == None, - t.settings["Content-type"]) - # now we set another value - t.content_type = "text/plain" - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t.settings["Content-type"] == "text/plain", - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, - t._get_saved_settings()) - # now we clear to the post-primed value - t.content_type = EMPTY - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t.content_type == EMPTY, t.content_type) - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - def testDefaultingProperty(self): - """Testing a defaulting versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties=required_saved_properties) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings() == {}, t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t.content_type == "text/html", - t.content_type) - self.failUnless(t.settings["Content-type"] == "text/html", - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, - t._get_saved_settings()) - def testRequiredDefaultingProperty(self): - """Testing a required defaulting versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties=required_saved_properties, - require_save=True) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, - t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, - t._get_saved_settings()) - def testClassVersionedPropertyDefinition(self): - """Testing a class-specific _versioned property decorator""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - def _versioned_property(settings_properties=settings_properties, - required_saved_properties=required_saved_properties, - **kwargs): - if "settings_properties" not in kwargs: - kwargs["settings_properties"] = settings_properties - if "required_saved_properties" not in kwargs: - kwargs["required_saved_properties"]=required_saved_properties - return versioned_property(**kwargs) - @_versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - require_save=True) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, - t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, - t._get_saved_settings()) - def testMutableChangeHookedProperty(self): - """Testing a mutable change-hooked property""" - SAVES = [] - def prop_log_save_settings(self, old, new, saves=SAVES): - saves.append("'%s' -> '%s'" % (str(old), str(new))) - prop_save_settings(self, old, new) - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="List-type", - doc="A test property", - mutable=True, - change_hook=prop_log_save_settings, - settings_properties=settings_properties, - required_saved_properties=required_saved_properties) - def list_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(SAVES == [], SAVES) - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.list_type == EMPTY, t.list_type) - self.failUnless(SAVES == [ - "'None' -> ''" - ], SAVES) - self.failUnless(t.settings["List-type"]==EMPTY,t.settings["List-type"]) - t.list_type = [] - self.failUnless(t.settings["List-type"] == [], t.settings["List-type"]) - self.failUnless(SAVES == [ - "'None' -> ''", - "'' -> '[]'" - ], SAVES) - t.list_type.append(5) - self.failUnless(SAVES == [ - "'None' -> ''", - "'' -> '[]'", - "'' -> '[]'" # <- TODO. Where did this come from? - ], SAVES) - self.failUnless(t.settings["List-type"] == [5],t.settings["List-type"]) - self.failUnless(SAVES == [ # the append(5) has not yet been saved - "'None' -> ''", - "'' -> '[]'", - "'' -> '[]'", - ], SAVES) - self.failUnless(t.list_type == [5], t.list_type) # <-get triggers saved - self.failUnless(SAVES == [ # now the append(5) has been saved. - "'None' -> ''", - "'' -> '[]'", - "'' -> '[]'", - "'[]' -> '[5]'" - ], SAVES) - -unitsuite=unittest.TestLoader().loadTestsFromTestCase(SavedSettingsObjectTests) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/tree.py b/libbe/tree.py index 54b927e..e69de29 100644 --- a/libbe/tree.py +++ b/libbe/tree.py @@ -1,161 +0,0 @@ -# Bugs Everywhere, a distributed bugtracker -# Copyright (C) 2008-2009 W. Trevor King -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA - -import doctest - -class Tree(list): - """ - Construct - +-b---d-g - a-+ +-e - +-c-+-f-h-i - with - >>> i = Tree(); i.n = "i" - >>> h = Tree([i]); h.n = "h" - >>> f = Tree([h]); f.n = "f" - >>> e = Tree(); e.n = "e" - >>> c = Tree([f,e]); c.n = "c" - >>> g = Tree(); g.n = "g" - >>> d = Tree([g]); d.n = "d" - >>> b = Tree([d]); b.n = "b" - >>> a = Tree(); a.n = "a" - >>> a.append(c) - >>> a.append(b) - - >>> a.branch_len() - 5 - >>> a.sort(key=lambda node : -node.branch_len()) - >>> "".join([node.n for node in a.traverse()]) - 'acfhiebdg' - >>> a.sort(key=lambda node : node.branch_len()) - >>> "".join([node.n for node in a.traverse()]) - 'abdgcefhi' - >>> "".join([node.n for node in a.traverse(depthFirst=False)]) - 'abcdefghi' - >>> for depth,node in a.thread(): - ... print "%*s" % (2*depth+1, node.n) - a - b - d - g - c - e - f - h - i - >>> for depth,node in a.thread(flatten=True): - ... print "%*s" % (2*depth+1, node.n) - a - b - d - g - c - e - f - h - i - """ - def branch_len(self): - """ - Exhaustive search every time == SLOW. - - Use only on small trees, or reimplement by overriding - child-addition methods to allow accurate caching. - - For the tree - +-b---d-g - a-+ +-e - +-c-+-f-h-i - this method returns 5. - """ - if len(self) == 0: - return 1 - else: - return 1 + max([child.branch_len() for child in self]) - - def sort(self, *args, **kwargs): - """ - This method can be slow, e.g. on a branch_len() sort, since a - node at depth N from the root has it's branch_len() method - called N times. - """ - list.sort(self, *args, **kwargs) - for child in self: - child.sort(*args, **kwargs) - - def traverse(self, depthFirst=True): - """ - Note: you might want to sort() your tree first. - """ - if depthFirst == True: - yield self - for child in self: - for descendant in child.traverse(): - yield descendant - else: # breadth first, Wikipedia algorithm - # http://en.wikipedia.org/wiki/Breadth-first_search - queue = [self] - while len(queue) > 0: - node = queue.pop(0) - yield node - queue.extend(node) - - def thread(self, flatten=False): - """ - When flatten==False, the depth of any node is one greater than - the depth of its parent. That way the inheritance is - explicit, but you can end up with highly indented threads. - - When flatten==True, the depth of any node is only greater than - the depth of its parent when there is a branch, and the node - is not the last child. This can lead to ancestry ambiguity, - but keeps the total indentation down. E.g. - +-b +-b-c - a-+-c and a-+ - +-d-e-f +-d-e-f - would both produce (after sorting by branch_len()) - (0, a) - (1, b) - (1, c) - (0, d) - (0, e) - (0, f) - """ - stack = [] # ancestry of the current node - if flatten == True: - depthDict = {} - - for node in self.traverse(depthFirst=True): - while len(stack) > 0 \ - and id(node) not in [id(c) for c in stack[-1]]: - stack.pop(-1) - if flatten == False: - depth = len(stack) - else: - if len(stack) == 0: - depth = 0 - else: - parent = stack[-1] - depth = depthDict[id(parent)] - if len(parent) > 1 and node != parent[-1]: - depth += 1 - depthDict[id(node)] = depth - yield (depth,node) - stack.append(node) - -suite = doctest.DocTestSuite() diff --git a/libbe/utility.py b/libbe/utility.py index 8a0f318..e69de29 100644 --- a/libbe/utility.py +++ b/libbe/utility.py @@ -1,93 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# W. Trevor King -# -# -# 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 -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# 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. -# -# 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 calendar -import codecs -import os -import shutil -import tempfile -import time -import doctest - - -def search_parent_directories(path, filename): - """ - Find the file (or directory) named filename in path or in any - of path's parents. - - e.g. - search_parent_directories("/a/b/c", ".be") - will return the path to the first existing file from - /a/b/c/.be - /a/b/.be - /a/.be - /.be - or None if none of those files exist. - """ - path = os.path.realpath(path) - assert os.path.exists(path) - old_path = None - while True: - check_path = os.path.join(path, filename) - if os.path.exists(check_path): - return check_path - if path == old_path: - return None - old_path = path - path = os.path.dirname(path) - -class Dir (object): - "A temporary directory for testing use" - def __init__(self): - self.path = tempfile.mkdtemp(prefix="BEtest") - self.rmtree = shutil.rmtree # save local reference for __del__ - self.removed = False - def __del__(self): - self.cleanup() - def cleanup(self): - if self.removed == False: - self.rmtree(self.path) - self.removed = True - def __call__(self): - return self.path - -RFC_2822_TIME_FMT = "%a, %d %b %Y %H:%M:%S +0000" - - -def time_to_str(time_val): - """Convert a time value into an RFC 2822-formatted string. This format - lacks sub-second data. - >>> time_to_str(0) - 'Thu, 01 Jan 1970 00:00:00 +0000' - """ - return time.strftime(RFC_2822_TIME_FMT, time.gmtime(time_val)) - -def str_to_time(str_time): - """Convert an RFC 2822-fomatted string into a time falue. - >>> str_to_time("Thu, 01 Jan 1970 00:00:00 +0000") - 0 - >>> q = time.time() - >>> str_to_time(time_to_str(q)) == int(q) - True - """ - return calendar.timegm(time.strptime(str_time, RFC_2822_TIME_FMT)) - -def handy_time(time_val): - return time.strftime("%a, %d %b %Y %H:%M", time.localtime(time_val)) - - -suite = doctest.DocTestSuite() diff --git a/completion/be.bash b/misc/completion/be.bash similarity index 100% rename from completion/be.bash rename to misc/completion/be.bash diff --git a/misc/gui/beg b/misc/gui/beg index 55e537d..e69de29 100755 --- a/misc/gui/beg +++ b/misc/gui/beg @@ -1,12 +0,0 @@ -#!/usr/bin/env python -import table -from Tkinter import * -from libbe import bugdir - -tk = Tk() -Label(tk, text="Bug list").pack() -mlb = table.MultiListbox(tk, (('Severity', 4), ('Creator', 8), ('Summary', 40))) -for bug in [b for b in bugdir.tree_root(".").list() if b.active]: - mlb.insert(END, (bug.severity, bug.creator, bug.summary)) -mlb.pack(expand=YES,fill=BOTH) -tk.mainloop() diff --git a/misc/gui/table.py b/misc/gui/table.py index 2865f28..e69de29 100644 --- a/misc/gui/table.py +++ b/misc/gui/table.py @@ -1,97 +0,0 @@ -from Tkinter import * - -class MultiListbox(Frame): - def __init__(self, master, lists): - Frame.__init__(self, master) - self.lists = [] - for l,w in lists: - frame = Frame(self); frame.pack(side=LEFT, expand=YES, fill=BOTH) - Label(frame, text=l, borderwidth=1, relief=RAISED).pack(fill=X) - lb = Listbox(frame, width=w, borderwidth=0, selectborderwidth=0, - relief=FLAT, exportselection=FALSE) - lb.pack(expand=YES, fill=BOTH) - self.lists.append(lb) - lb.bind('', lambda e, s=self: s._select(e.y)) - lb.bind('', lambda e, s=self: s._select(e.y)) - lb.bind('', lambda e: 'break') - lb.bind('', lambda e, s=self: s._b2motion(e.x, e.y)) - lb.bind('', lambda e, s=self: s._button2(e.x, e.y)) - frame = Frame(self); frame.pack(side=LEFT, fill=Y) - Label(frame, borderwidth=1, relief=RAISED).pack(fill=X) - sb = Scrollbar(frame, orient=VERTICAL, command=self._scroll) - sb.pack(expand=YES, fill=Y) - self.lists[0]['yscrollcommand']=sb.set - - def _select(self, y): - row = self.lists[0].nearest(y) - self.selection_clear(0, END) - self.selection_set(row) - return 'break' - - def _button2(self, x, y): - for l in self.lists: l.scan_mark(x, y) - return 'break' - - def _b2motion(self, x, y): - for l in self.lists: l.scan_dragto(x, y) - return 'break' - - def _scroll(self, *args): - for l in self.lists: - apply(l.yview, args) - - def curselection(self): - return self.lists[0].curselection() - - def delete(self, first, last=None): - for l in self.lists: - l.delete(first, last) - - def get(self, first, last=None): - result = [] - for l in self.lists: - result.append(l.get(first,last)) - if last: return apply(map, [None] + result) - return result - - def index(self, index): - self.lists[0].index(index) - - def insert(self, index, *elements): - for e in elements: - i = 0 - for l in self.lists: - l.insert(index, e[i]) - i = i + 1 - - def size(self): - return self.lists[0].size() - - def see(self, index): - for l in self.lists: - l.see(index) - - def selection_anchor(self, index): - for l in self.lists: - l.selection_anchor(index) - - def selection_clear(self, first, last=None): - for l in self.lists: - l.selection_clear(first, last) - - def selection_includes(self, index): - return self.lists[0].selection_includes(index) - - def selection_set(self, first, last=None): - for l in self.lists: - l.selection_set(first, last) - -if __name__ == '__main__': - tk = Tk() - Label(tk, text='MultiListbox').pack() - mlb = MultiListbox(tk, (('Subject', 40), ('Sender', 20), ('Date', 10))) - for i in range(1000): - mlb.insert(END, ('Important Message: %d' % i, 'John Doe', '10/10/%04d' % (1900+i))) - mlb.pack(expand=YES,fill=BOTH) - tk.mainloop() - diff --git a/misc/gui/wxbe b/misc/gui/wxbe index e71ae0c..e69de29 100755 --- a/misc/gui/wxbe +++ b/misc/gui/wxbe @@ -1,87 +0,0 @@ -#!/usr/bin/env python -import wx -from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin -import sys, os.path -from libbe import bugdir, names -from libbe.bug import cmp_status, cmp_severity, cmp_time, cmp_full - -class MyApp(wx.App): - def OnInit(self): - frame = BugListFrame(None, title="Bug List") - frame.Show(True) - self.SetTopWindow(frame) - return True - -class BugListFrame(wx.Frame): - def __init__(self, *args, **kwargs): - wx.Frame.__init__(self, *args, **kwargs) - bugs = BugList(self) - - # Widgets to display/sort/edit will go in this panel - # for now it is just a placeholder - panel = wx.Panel(self) - panel.SetBackgroundColour("RED") - - vbox = wx.BoxSizer(wx.VERTICAL) - vbox.Add(panel, 0, wx.EXPAND) - vbox.Add(bugs, 1, wx.EXPAND) - - self.SetAutoLayout(True) - self.SetSizer(vbox) - self.Layout() - -class BugList(wx.ListCtrl, ListCtrlAutoWidthMixin): - def __init__(self, parent): - wx.ListCtrl.__init__(self, parent, - style=wx.LC_REPORT) - ListCtrlAutoWidthMixin.__init__(self) - - self.bugdir = bugdir.tree_root(".") - self.buglist = list(self.bugdir.list()) - self.buglist.sort() - self.columns = ("id", "status", "severity", "summary") - - dataIndex = 0 - for x in range(len(self.columns)): - self.InsertColumn(x, self.columns[x].capitalize()) - self.SetColumnWidth(x, wx.LIST_AUTOSIZE_USEHEADER) - for bug in [b for b in self.buglist if b.active]: - name = names.unique_name(bug, self.buglist) - id = self.InsertStringItem(self.GetItemCount(), name) - self.SetStringItem(id, 1, bug.status) - self.SetStringItem(id, 2, bug.severity) - self.SetStringItem(id, 3, bug.summary) - self.SetItemData(id, dataIndex) # set keys for each line - dataIndex += 1 - self.EnsureVisible(id) - for x in range(len(self.columns)): - self.SetColumnWidth(x, wx.LIST_AUTOSIZE) - conts_width = self.GetColumnWidth(x) - self.SetColumnWidth(x, wx.LIST_AUTOSIZE_USEHEADER) - if conts_width > self.GetColumnWidth(x): - self.SetColumnWidth(x, conts_width) - - self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColumnClick) - self.bugcmp_fn = cmp_full - # For reasons I don't understant, sorting is broken... - #self.SortItems(self.Sorter) - #self.Refresh() - def Sorter(self, key1, key2): - """Get bug info from the keys and pass to self.bugcmp_fn""" - bug1 = self.buglist[key1-1] - bug2 = self.buglist[key2-1] - # Another way of getting bug information - #bug1uuid = self.GetItem(key1, 0).GetText() - #bug2uuid = self.GetItem(key2, 0).GetText() - #print bug1uuid, bug2uuid - #bug1 = self.bugdir.get_bug(bug1uuid) - #bug2 = self.bugdir.get_bug(bug1uuid) - print self.bugcmp_fn(bug1,bug2) - return self.bugcmp_fn(bug1,bug2) - def OnColumnClick(self, event): - """Resort bug list depending on which column was clicked""" - print "TODO: sort by column %d" % event.Column - # change self.bugcmp_fn and resort, but I can't get it working - -app = MyApp() -app.MainLoop() diff --git a/setup.py b/setup.py index b909f3f..e770419 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,16 @@ #!/usr/bin/env python from distutils.core import setup +from libbe import _version + +rev_id = _version.version_info["revision_id"] +rev_date = rev_id.split("-")[1] setup( name='Bugs Everywhere', - version='0.0.193', + version=rev_date, description='Bugtracker built on distributed revision control', - url='http://panoramicfeedback.com/opensource/', + url='http://bugseverywhere.org/', packages=['becommands', 'libbe'], scripts=['be'], data_files=[ diff --git a/test_usage.sh b/test_usage.sh index b2e2cab..13be2ff 100755 --- a/test_usage.sh +++ b/test_usage.sh @@ -18,6 +18,8 @@ set -v # verbose, echo commands to stdout exec 6>&2 # save stderr to file descriptor 6 exec 2>&1 # fd 2 now writes to stdout +ONLY_TEST_COMMIT="true" + if [ $# -gt 1 ] then echo "usage: test_usage.sh [RCS]" @@ -75,8 +77,8 @@ then darcs init elif [ "$RCS" == "git" ] then - NAME=`git-config user.name` - EMAIL=`git-config user.email` + NAME=`git config user.name` + EMAIL=`git config user.email` ID="$NAME <$EMAIL>" git init elif [ "$RCS" == "hg" ] @@ -124,7 +126,16 @@ BUGB=`echo "$OUT" | sed -n 's/Created bug with ID //p'` be comment $BUGB "Blissfully unaware of a similar bug" be merge $BUG $BUGB # join BUGB to BUG be show $BUG # show bug details & comments +# you can also export/import XML bugs/comments +OUT=`be new 'yet more fun'` +BUGC=`echo "$OUT" | sed -n 's/Created bug with ID //p'` +be comment $BUGC "The ants go marching..." +be show --xml $BUGC | be comment --xml ${BUG}:2 - be remove $BUG # decide that you don't like that bug after all +be commit "You can even commit using BE" +be commit --allow-empty "And you can add empty commits if you like" +be commit "But this will fail" || echo "Failed" + cd / rm -rf $TESTDIR diff --git a/update_copyright.sh b/update_copyright.sh index 260cb3b..28eb0e0 100755 --- a/update_copyright.sh +++ b/update_copyright.sh @@ -1,10 +1,62 @@ #!/bin/bash # +# Copyright (C) 2009 W. Trevor King +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + # Update copyright information in source code with information from # the bzr repository. Run from the BE repository root. +# +# Replaces everything starting with '^# Copyright' and continuing with +# '^#' with an auto-generated copyright blurb. If you want to add +# #-commented material after a copyright blurb, please insert a blank +# line between the blurb and your comment (as in this file), so the +# next run of update_copyright.sh doesn't clobber your comment. +# +# usage: update_copyright.sh [files ...] +# +# If no files are given, a list of files to update is generated +# automatically. set -o pipefail +if [ $# -gt 0 ]; then + FILES="$*" +else + FILES=`grep -rc "# Copyright" . | grep -v ':0$' | cut -d: -f1` +fi + +COPYRIGHT_TEXT="# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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. +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA." +# escape newlines and special characters +SED_RM_TRAIL_END='s/[\]n$//' # strip trailing newline escape +SED_ESC_SPECIAL='s/\([()/]\)/\\\1/g' # escape special characters +ESCAPED_TEXT=`echo "$COPYRIGHT_TEXT" | awk '{printf("%s\\\\n", $0)}' | sed "$SED_RM_TRAIL_END" | sed "$SED_ESC_SPECIAL"` + # adjust the AUTHORS file AUTHORS=`bzr log | grep '^ *committer\|^ *author' | cut -d: -f2 | sed 's/ <.*//;s/^ *//' | sort | uniq` AUTHORS=`echo "$AUTHORS" | grep -v 'j\^\|wking\|John Doe'` # remove non-names @@ -12,7 +64,6 @@ echo "Bugs Everywhere was written by:" > AUTHORS echo "$AUTHORS" >> AUTHORS CURRENT_YEAR=`date +"%Y"` -FILES=`grep -rc "# Copyright" . | grep -v ':0$' | cut -d: -f1` TMP=`mktemp BE_update_copyright.XXXXXXX` for file in $FILES @@ -91,10 +142,10 @@ do COPYRIGHT=`echo "$COPYRIGHT\\n# $DATE_SPACE $AUTHOR"` done < <(echo "$OTHER_AUTHORS") fi - echo -e "$COPYRIGHT" + COPYRIGHT=`echo "$COPYRIGHT\\n$ESCAPED_TEXT"` # Strip old copyright info and replace with tag - awk 'BEGIN{incopy=0}{if(match($0, "^# Copyright")>0){incopy=1; print "-xyz-COPYRIGHT-zyx-"}else{if(incopy=0){print $0}else{if(match($0, "^# ")==0){incopy=0; print $0}}}}' "$file" > "$TMP" + awk 'BEGIN{incopy==0}{if(match($0, "^# Copyright")>0){incopy=1; print "-xyz-COPYRIGHT-zyx-"}else{if(incopy==0){print $0}else{if(match($0, "^#")==0){incopy=0; print $0}}}}' "$file" > "$TMP" # Replace tag in with new string sed -i "s/^-xyz-COPYRIGHT-zyx-$/$COPYRIGHT/" "$TMP" -- 2.26.2