From b0b3c9473e3a4b728ea72a2876e39fe41284a9ed Mon Sep 17 00:00:00 2001 From: Gianluca Montecchi Date: Fri, 2 Oct 2009 23:46:24 +0200 Subject: [PATCH] Merged with Trevor's -rr branch --- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 25 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 6 +- .../values | 6 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 25 +- .../values | 19 +- .../values | 19 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 19 +- .../values | 25 +- .../values | 19 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 19 +- .../values | 19 +- .../values | 6 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 19 +- .../values | 25 +- .../values | 25 +- .../values | 19 +- .../values | 19 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 6 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .../values | 6 +- .../values | 6 +- .../values | 19 +- .../values | 19 +- .../values | 25 +- .../values | 19 +- .../values | 6 +- .../values | 19 +- .../values | 19 +- .../values | 19 +- .be/settings | 2 +- .be/version | 2 +- Makefile | 4 +- be | 8 +- becommands/assign.py | 15 +- becommands/close.py | 12 +- becommands/comment.py | 34 +- becommands/commit.py | 13 +- becommands/depend.py | 321 +++++- becommands/diff.py | 67 +- becommands/help.py | 6 +- becommands/html.py | 214 ++-- becommands/init.py | 28 +- becommands/list.py | 17 +- becommands/merge.py | 14 +- becommands/new.py | 12 +- becommands/open.py | 12 +- becommands/remove.py | 14 +- becommands/set.py | 24 +- becommands/severity.py | 18 +- becommands/show.py | 14 +- becommands/status.py | 21 +- becommands/subscribe.py | 390 +++++++ becommands/tag.py | 26 +- becommands/target.py | 22 +- interfaces/README | 34 + interfaces/email/interactive/README | 145 +++ interfaces/email/interactive/_procmailrc | 22 + interfaces/email/interactive/be-handle-mail | 950 ++++++++++++++++++ .../email/interactive/becommands/assign.py | 87 ++ .../email/interactive/becommands/close.py | 60 ++ .../email/interactive/becommands/comment.py | 228 +++++ .../email/interactive/becommands/commit.py | 78 ++ .../email/interactive/becommands/depend.py | 339 +++++++ .../email/interactive/becommands/diff.py | 120 +++ .../email/interactive/becommands/help.py | 68 ++ .../email/interactive/becommands/html.py | 588 +++++++++++ .../email/interactive/becommands/init.py | 100 ++ .../email/interactive/becommands/list.py | 248 +++++ .../email/interactive/becommands/merge.py | 165 +++ .../email/interactive/becommands/new.py | 80 ++ .../email/interactive/becommands/open.py | 58 ++ .../email/interactive/becommands/remove.py | 62 ++ .../email/interactive/becommands/set.py | 130 +++ .../email/interactive/becommands/severity.py | 103 ++ .../email/interactive/becommands/show.py | 116 +++ .../email/interactive/becommands/status.py | 115 +++ .../email/interactive/becommands/subscribe.py | 390 +++++++ .../email/interactive/becommands/tag.py | 134 +++ .../email/interactive/becommands/target.py | 95 ++ interfaces/email/interactive/examples/comment | 11 + .../interactive/examples/failing_multiples | 16 + .../interactive/examples/invalid_command | 11 + .../interactive/examples/invalid_subject | 9 + interfaces/email/interactive/examples/list | 11 + .../interactive/examples/missing_command | 11 + .../interactive/examples/multiple_commands | 14 + interfaces/email/interactive/examples/new | 19 + .../interactive/examples/new_with_comment | 13 + interfaces/email/interactive/examples/show | 11 + interfaces/email/interactive/examples/unicode | 11 + interfaces/email/interactive/libbe/arch.py | 312 ++++++ interfaces/email/interactive/libbe/beuuid.py | 63 ++ interfaces/email/interactive/libbe/bug.py | 580 +++++++++++ interfaces/email/interactive/libbe/bugdir.py | 832 +++++++++++++++ interfaces/email/interactive/libbe/bzr.py | 113 +++ interfaces/email/interactive/libbe/cmdutil.py | 233 +++++ interfaces/email/interactive/libbe/comment.py | 744 ++++++++++++++ interfaces/email/interactive/libbe/config.py | 89 ++ interfaces/email/interactive/libbe/darcs.py | 184 ++++ interfaces/email/interactive/libbe/diff.py | 419 ++++++++ interfaces/email/interactive/libbe/editor.py | 108 ++ .../email/interactive/libbe/encoding.py | 61 ++ interfaces/email/interactive/libbe/git.py | 148 +++ interfaces/email/interactive/libbe/hg.py | 103 ++ interfaces/email/interactive/libbe/mapfile.py | 116 +++ interfaces/email/interactive/libbe/plugin.py | 77 ++ .../email/interactive/libbe/properties.py | 638 ++++++++++++ .../interactive/libbe/settings_object.py | 412 ++++++++ interfaces/email/interactive/libbe/tree.py | 183 ++++ interfaces/email/interactive/libbe/upgrade.py | 187 ++++ interfaces/email/interactive/libbe/utility.py | 134 +++ interfaces/email/interactive/libbe/vcs.py | 938 +++++++++++++++++ interfaces/email/interactive/libbe/version.py | 50 + interfaces/email/interactive/send_pgp_mime.py | 611 +++++++++++ .../Bugs-Everywhere-Web/beweb/controllers.py | 6 +- .../web/Bugs-Everywhere-Web/libbe/arch.py | 67 +- .../web/Bugs-Everywhere-Web/libbe/beuuid.py | 2 + .../web/Bugs-Everywhere-Web/libbe/bug.py | 117 ++- .../web/Bugs-Everywhere-Web/libbe/bugdir.py | 476 ++++++--- .../web/Bugs-Everywhere-Web/libbe/bzr.py | 52 +- .../web/Bugs-Everywhere-Web/libbe/cmdutil.py | 19 +- .../web/Bugs-Everywhere-Web/libbe/comment.py | 306 +++--- .../web/Bugs-Everywhere-Web/libbe/config.py | 6 + .../web/Bugs-Everywhere-Web/libbe/darcs.py | 93 +- .../web/Bugs-Everywhere-Web/libbe/diff.py | 486 +++++++-- .../web/Bugs-Everywhere-Web/libbe/editor.py | 9 +- .../web/Bugs-Everywhere-Web/libbe/encoding.py | 10 + .../web/Bugs-Everywhere-Web/libbe/git.py | 78 +- .../web/Bugs-Everywhere-Web/libbe/hg.py | 63 +- .../web/Bugs-Everywhere-Web/libbe/mapfile.py | 39 +- .../web/Bugs-Everywhere-Web/libbe/plugin.py | 6 + .../Bugs-Everywhere-Web/libbe/properties.py | 10 +- .../web/Bugs-Everywhere-Web/libbe/rcs.py | 876 ---------------- .../libbe/settings_object.py | 11 +- .../web/Bugs-Everywhere-Web/libbe/tree.py | 4 + .../web/Bugs-Everywhere-Web/libbe/upgrade.py | 187 ++++ .../web/Bugs-Everywhere-Web/libbe/utility.py | 5 + .../web/Bugs-Everywhere-Web/libbe/vcs.py | 938 +++++++++++++++++ .../web/Bugs-Everywhere-Web/libbe/version.py | 50 + interfaces/xml/be-mbox-to-xml | 39 +- interfaces/xml/be-xml-to-mbox | 6 +- libbe/arch.py | 67 +- libbe/beuuid.py | 2 + libbe/bug.py | 117 ++- libbe/bugdir.py | 476 ++++++--- libbe/bzr.py | 52 +- libbe/cmdutil.py | 19 +- libbe/comment.py | 306 +++--- libbe/config.py | 6 + libbe/darcs.py | 93 +- libbe/diff.py | 486 +++++++-- libbe/editor.py | 9 +- libbe/encoding.py | 10 + libbe/git.py | 78 +- libbe/hg.py | 63 +- libbe/mapfile.py | 39 +- libbe/plugin.py | 6 + libbe/properties.py | 10 +- libbe/rcs.py | 876 ---------------- libbe/settings_object.py | 11 +- libbe/tree.py | 4 + libbe/upgrade.py | 187 ++++ libbe/utility.py | 5 + libbe/vcs.py | 938 +++++++++++++++++ libbe/version.py | 50 + update_copyright.sh | 17 +- 236 files changed, 18237 insertions(+), 4293 deletions(-) create mode 100644 becommands/subscribe.py create mode 100644 interfaces/README create mode 100644 interfaces/email/interactive/README create mode 100644 interfaces/email/interactive/_procmailrc create mode 100644 interfaces/email/interactive/be-handle-mail create mode 100644 interfaces/email/interactive/becommands/assign.py create mode 100644 interfaces/email/interactive/becommands/close.py create mode 100644 interfaces/email/interactive/becommands/comment.py create mode 100644 interfaces/email/interactive/becommands/commit.py create mode 100644 interfaces/email/interactive/becommands/depend.py create mode 100644 interfaces/email/interactive/becommands/diff.py create mode 100644 interfaces/email/interactive/becommands/help.py create mode 100644 interfaces/email/interactive/becommands/html.py create mode 100644 interfaces/email/interactive/becommands/init.py create mode 100644 interfaces/email/interactive/becommands/list.py create mode 100644 interfaces/email/interactive/becommands/merge.py create mode 100644 interfaces/email/interactive/becommands/new.py create mode 100644 interfaces/email/interactive/becommands/open.py create mode 100644 interfaces/email/interactive/becommands/remove.py create mode 100644 interfaces/email/interactive/becommands/set.py create mode 100644 interfaces/email/interactive/becommands/severity.py create mode 100644 interfaces/email/interactive/becommands/show.py create mode 100644 interfaces/email/interactive/becommands/status.py create mode 100644 interfaces/email/interactive/becommands/subscribe.py create mode 100644 interfaces/email/interactive/becommands/tag.py create mode 100644 interfaces/email/interactive/becommands/target.py create mode 100644 interfaces/email/interactive/examples/comment create mode 100644 interfaces/email/interactive/examples/failing_multiples create mode 100644 interfaces/email/interactive/examples/invalid_command create mode 100644 interfaces/email/interactive/examples/invalid_subject create mode 100644 interfaces/email/interactive/examples/list create mode 100644 interfaces/email/interactive/examples/missing_command create mode 100644 interfaces/email/interactive/examples/multiple_commands create mode 100644 interfaces/email/interactive/examples/new create mode 100644 interfaces/email/interactive/examples/new_with_comment create mode 100644 interfaces/email/interactive/examples/show create mode 100644 interfaces/email/interactive/examples/unicode create mode 100644 interfaces/email/interactive/libbe/arch.py create mode 100644 interfaces/email/interactive/libbe/beuuid.py create mode 100644 interfaces/email/interactive/libbe/bug.py create mode 100644 interfaces/email/interactive/libbe/bugdir.py create mode 100644 interfaces/email/interactive/libbe/bzr.py create mode 100644 interfaces/email/interactive/libbe/cmdutil.py create mode 100644 interfaces/email/interactive/libbe/comment.py create mode 100644 interfaces/email/interactive/libbe/config.py create mode 100644 interfaces/email/interactive/libbe/darcs.py create mode 100644 interfaces/email/interactive/libbe/diff.py create mode 100644 interfaces/email/interactive/libbe/editor.py create mode 100644 interfaces/email/interactive/libbe/encoding.py create mode 100644 interfaces/email/interactive/libbe/git.py create mode 100644 interfaces/email/interactive/libbe/hg.py create mode 100644 interfaces/email/interactive/libbe/mapfile.py create mode 100644 interfaces/email/interactive/libbe/plugin.py create mode 100644 interfaces/email/interactive/libbe/properties.py create mode 100644 interfaces/email/interactive/libbe/settings_object.py create mode 100644 interfaces/email/interactive/libbe/tree.py create mode 100644 interfaces/email/interactive/libbe/upgrade.py create mode 100644 interfaces/email/interactive/libbe/utility.py create mode 100644 interfaces/email/interactive/libbe/vcs.py create mode 100644 interfaces/email/interactive/libbe/version.py create mode 100644 interfaces/email/interactive/send_pgp_mime.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/upgrade.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/vcs.py create mode 100644 interfaces/web/Bugs-Everywhere-Web/libbe/version.py create mode 100644 libbe/upgrade.py create mode 100644 libbe/vcs.py create mode 100644 libbe/version.py diff --git a/.be/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/values b/.be/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/values index a68a21d..02ee559 100644 --- a/.be/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/values +++ b/.be/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/values @@ -1,21 +1,8 @@ +Author: benf - -Content-type=text/plain - - - - - - -Date=Fri, 18 Apr 2008 11:21:03 +0000 - - - - - - -From=benf +Content-type: text/plain +Date: Fri, 18 Apr 2008 11:21:03 +0000 diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values index 5ed19bf..34b6514 100644 --- a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values +++ b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Thu, 04 Dec 2008 13:35:41 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Thu, 04 Dec 2008 13:35:41 +0000 diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values index 58e8ffa..66a9f19 100644 --- a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values +++ b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values @@ -1,21 +1,8 @@ +Author: abentley - -Content-type=text/rst - - - - - - -Date=Thu, 06 Apr 2006 16:47:25 +0000 - - - - - - -From=abentley +Content-type: text/rst +Date: Thu, 06 Apr 2006 16:47:25 +0000 diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values index 2a1c84d..3f1fb15 100644 --- a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values +++ b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values @@ -1,28 +1,11 @@ +Author: abentley +Content-type: text/restructured -Content-type=text/restructured +Date: Thu, 06 Apr 2006 16:54:57 +0000 - - - -Date=Thu, 06 Apr 2006 16:54:57 +0000 - - - - - - -From=abentley - - - - - - -In-reply-to=144c238c-75d1-40f1-82c1-647668bcf2bc - - +In-reply-to: 144c238c-75d1-40f1-82c1-647668bcf2bc diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values index d55baa7..8dc0882 100644 --- a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values @@ -1,21 +1,8 @@ +Author: hubert - -Content-type=text/plain - - - - - - -Date=Mon, 23 Jun 2008 05:02:22 +0000 - - - - - - -From=hubert +Content-type: text/plain +Date: Mon, 23 Jun 2008 05:02:22 +0000 diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values index 1350ffb..176ae7f 100644 --- a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values @@ -1,21 +1,8 @@ +Author: hubert - -Content-type=text/plain - - - - - - -Date=Tue, 24 Jun 2008 02:45:18 +0000 - - - - - - -From=hubert +Content-type: text/plain +Date: Tue, 24 Jun 2008 02:45:18 +0000 diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values index 67b182a..777a3f8 100644 --- a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values @@ -1,21 +1,8 @@ +Author: wking - -Content-type=text/plain - - - - - - -Date=Sun, 16 Nov 2008 20:36:20 +0000 - - - - - - -From=wking +Content-type: text/plain +Date: Sun, 16 Nov 2008 20:36:20 +0000 diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values index 4a2e108..461a5ab 100644 --- a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values @@ -1,21 +1,8 @@ +Author: wking - -Content-type=text/plain - - - - - - -Date=Thu, 13 Nov 2008 19:31:04 +0000 - - - - - - -From=wking +Content-type: text/plain +Date: Thu, 13 Nov 2008 19:31:04 +0000 diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values index cbf7142..e550f5c 100644 --- a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values @@ -1,21 +1,8 @@ +Author: wking - -Content-type=text/plain - - - - - - -Date=Thu, 13 Nov 2008 20:18:02 +0000 - - - - - - -From=wking +Content-type: text/plain +Date: Thu, 13 Nov 2008 20:18:02 +0000 diff --git a/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values index dae549f..a346a7c 100644 --- a/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values +++ b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 04 Dec 2008 17:16:11 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 04 Dec 2008 17:16:11 +0000 diff --git a/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/values b/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/values index b778afb..b82fbcb 100644 --- a/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/values +++ b/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: abentley -Date: Mon, 17 Apr 2006 20:59:15 +0000 +Content-type: text/plain -From: abentley +Date: Mon, 17 Apr 2006 20:59:15 +0000 diff --git a/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values b/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values index f6daae2..42836a5 100644 --- a/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values +++ b/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 21:29:13 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 21:29:13 +0000 diff --git a/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/values b/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/values index 2425f4f..ac20cae 100644 --- a/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/values +++ b/.be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 21:29:33 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 21:29:33 +0000 diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values index 74ffa83..01296d8 100644 --- a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values +++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values @@ -1,21 +1,8 @@ +Author: abentley - -Content-type=text/plain - - - - - - -Date=Sat, 01 Apr 2006 18:32:47 +0000 - - - - - - -From=abentley +Content-type: text/plain +Date: Sat, 01 Apr 2006 18:32:47 +0000 diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values index 6c7fb63..31bcacb 100644 --- a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values +++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values @@ -1,21 +1,8 @@ +Author: wking - -Content-type=text/plain - - - - - - -Date=Fri, 14 Nov 2008 05:00:43 +0000 - - - - - - -From=wking +Content-type: text/plain +Date: Fri, 14 Nov 2008 05:00:43 +0000 diff --git a/.be/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values b/.be/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values index fe5568e..1402892 100644 --- a/.be/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values +++ b/.be/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values @@ -1,21 +1,8 @@ +Author: abentley - -Content-type=text/plain - - - - - - -Date=Wed, 04 Jan 2006 21:03:54 +0000 - - - - - - -From=abentley +Content-type: text/plain +Date: Wed, 04 Jan 2006 21:03:54 +0000 diff --git a/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values index ad389a7..4c495f7 100644 --- a/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values +++ b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 04 Dec 2008 17:21:08 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 04 Dec 2008 17:21:08 +0000 diff --git a/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values index 6e9546e..0eaf9c9 100644 --- a/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values +++ b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 04 Dec 2008 17:40:08 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 04 Dec 2008 17:40:08 +0000 diff --git a/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values b/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values index c499bfe..ab313b9 100644 --- a/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values +++ b/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values @@ -1,21 +1,8 @@ +Author: wking - -Content-type=text/plain - - - - - - -Date=Thu, 13 Nov 2008 17:27:17 +0000 - - - - - - -From=wking +Content-type: text/plain +Date: Thu, 13 Nov 2008 17:27:17 +0000 diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values index 1f7615e..ab2a567 100644 --- a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values +++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Thu, 04 Dec 2008 13:44:33 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Thu, 04 Dec 2008 13:44:33 +0000 diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values index f88e71f..f692e19 100644 --- a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values +++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values @@ -1,21 +1,8 @@ +Author: abentley - -Content-type=text/plain - - - - - - -Date=Tue, 17 May 2005 13:42:52 +0000 - - - - - - -From=abentley +Content-type: text/plain +Date: Tue, 17 May 2005 13:42:52 +0000 diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values index e939438..a0c9a34 100644 --- a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values +++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values @@ -1,28 +1,11 @@ +Author: W. Trevor King +Content-type: text/plain -Content-type=text/plain +Date: Thu, 04 Dec 2008 13:46:32 +0000 - - - -Date=Thu, 04 Dec 2008 13:46:32 +0000 - - - - - - -From=W. Trevor King - - - - - - -In-reply-to=9e33512e-e3cb-42ec-bc99-8e77587d0d3f - - +In-reply-to: 9e33512e-e3cb-42ec-bc99-8e77587d0d3f diff --git a/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values b/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values index b5100d0..e434e1e 100644 --- a/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values +++ b/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values @@ -1,21 +1,8 @@ +Author: wking - -Content-type=text/plain - - - - - - -Date=Thu, 13 Nov 2008 15:58:18 +0000 - - - - - - -From=wking +Content-type: text/plain +Date: Thu, 13 Nov 2008 15:58:18 +0000 diff --git a/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values b/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values index b19c065..e9bbdac 100644 --- a/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values +++ b/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Sat, 22 Nov 2008 18:53:20 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Sat, 22 Nov 2008 18:53:20 +0000 diff --git a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values index 667dc94..63842d1 100644 --- a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values +++ b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 04 Dec 2008 17:05:50 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 04 Dec 2008 17:05:50 +0000 diff --git a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values index 225f59e..6a4005c 100644 --- a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values +++ b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 04 Dec 2008 15:42:07 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 04 Dec 2008 15:42:07 +0000 diff --git a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/3e83dd98-d421-43b6-a78c-5da7aac5f279/values b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/3e83dd98-d421-43b6-a78c-5da7aac5f279/values index 062b638..a5cf814 100644 --- a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/3e83dd98-d421-43b6-a78c-5da7aac5f279/values +++ b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/3e83dd98-d421-43b6-a78c-5da7aac5f279/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: Gianluca Montecchi -Date: Tue, 21 Jul 2009 21:33:37 +0000 +Content-type: text/plain -From: Gianluca Montecchi +Date: Tue, 21 Jul 2009 21:33:37 +0000 In-reply-to: f776d667-6959-4cab-b05d-39e07702c04b diff --git a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/433e2090-55d6-4b13-bc6d-0b509556f21b/values b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/433e2090-55d6-4b13-bc6d-0b509556f21b/values index f335ced..0fc877b 100644 --- a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/433e2090-55d6-4b13-bc6d-0b509556f21b/values +++ b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/433e2090-55d6-4b13-bc6d-0b509556f21b/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: Gianluca Montecchi -Date: Tue, 21 Jul 2009 21:33:29 +0000 +Content-type: text/plain -From: Gianluca Montecchi +Date: Tue, 21 Jul 2009 21:33:29 +0000 In-reply-to: a0cbbd2e-a078-41ac-b583-900e9bb2abf3 diff --git a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/9d56f097-bf5b-4d8a-a83e-7ade8afd2b4c/values b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/9d56f097-bf5b-4d8a-a83e-7ade8afd2b4c/values index b2249ff..5423bb5 100644 --- a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/9d56f097-bf5b-4d8a-a83e-7ade8afd2b4c/values +++ b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/9d56f097-bf5b-4d8a-a83e-7ade8afd2b4c/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: Gianluca Montecchi -Date: Tue, 04 Aug 2009 19:48:58 +0000 +Content-type: text/plain -From: Gianluca Montecchi +Date: Tue, 04 Aug 2009 19:48:58 +0000 In-reply-to: 433e2090-55d6-4b13-bc6d-0b509556f21b diff --git a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/a0cbbd2e-a078-41ac-b583-900e9bb2abf3/values b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/a0cbbd2e-a078-41ac-b583-900e9bb2abf3/values index 06691af..cc2c5ba 100644 --- a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/a0cbbd2e-a078-41ac-b583-900e9bb2abf3/values +++ b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/a0cbbd2e-a078-41ac-b583-900e9bb2abf3/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: Gianluca Montecchi -Date: Tue, 21 Jul 2009 21:31:21 +0000 +Content-type: text/plain -From: Gianluca Montecchi +Date: Tue, 21 Jul 2009 21:31:21 +0000 In-reply-to: f776d667-6959-4cab-b05d-39e07702c04b diff --git a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/b1a772a0-241f-42fc-8209-765162485b0a/values b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/b1a772a0-241f-42fc-8209-765162485b0a/values index b6abe1f..dc8f664 100644 --- a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/b1a772a0-241f-42fc-8209-765162485b0a/values +++ b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/b1a772a0-241f-42fc-8209-765162485b0a/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: Gianluca Montecchi -Date: Wed, 22 Jul 2009 21:48:23 +0000 +Content-type: text/plain -From: Gianluca Montecchi +Date: Wed, 22 Jul 2009 21:48:23 +0000 diff --git a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/c1b9bc11-71e1-473e-ad9c-cfba0a2533d5/values b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/c1b9bc11-71e1-473e-ad9c-cfba0a2533d5/values index 2b282c0..785b9b0 100644 --- a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/c1b9bc11-71e1-473e-ad9c-cfba0a2533d5/values +++ b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/c1b9bc11-71e1-473e-ad9c-cfba0a2533d5/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: Gianluca Montecchi -Date: Mon, 27 Jul 2009 20:08:02 +0000 +Content-type: text/plain -From: Gianluca Montecchi +Date: Mon, 27 Jul 2009 20:08:02 +0000 In-reply-to: d74a6a82-6a08-472b-86d8-b1546c4d460f diff --git a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/d74a6a82-6a08-472b-86d8-b1546c4d460f/values b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/d74a6a82-6a08-472b-86d8-b1546c4d460f/values index e26b88e..4d949f3 100644 --- a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/d74a6a82-6a08-472b-86d8-b1546c4d460f/values +++ b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/d74a6a82-6a08-472b-86d8-b1546c4d460f/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: Gianluca Montecchi -Date: Wed, 22 Jul 2009 20:05:15 +0000 +Content-type: text/plain -From: Gianluca Montecchi +Date: Wed, 22 Jul 2009 20:05:15 +0000 diff --git a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/f776d667-6959-4cab-b05d-39e07702c04b/values b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/f776d667-6959-4cab-b05d-39e07702c04b/values index dd1fd4b..9872298 100644 --- a/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/f776d667-6959-4cab-b05d-39e07702c04b/values +++ b/.be/bugs/4ddf1313-bb3c-45d3-8dca-79ed5830d606/comments/f776d667-6959-4cab-b05d-39e07702c04b/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: Gianluca Montecchi -Date: Mon, 20 Jul 2009 21:54:57 +0000 +Content-type: text/plain -From: Gianluca Montecchi +Date: Mon, 20 Jul 2009 21:54:57 +0000 diff --git a/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values index 1f7615e..ab2a567 100644 --- a/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values +++ b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Thu, 04 Dec 2008 13:44:33 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Thu, 04 Dec 2008 13:44:33 +0000 diff --git a/.be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/values b/.be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/values index c8719a0..3ea62d2 100644 --- a/.be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/values +++ b/.be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/values @@ -1,28 +1,11 @@ +Author: abentley +Content-type: text/plain -Content-type=text/plain +Date: Mon, 16 Jul 2007 15:23:47 +0000 - - - -Date=Mon, 16 Jul 2007 15:23:47 +0000 - - - - - - -From=abentley - - - - - - -In-reply-to=e173c09a-1b3e-4d8a-a86a-6b8c94a76247 - - +In-reply-to: e173c09a-1b3e-4d8a-a86a-6b8c94a76247 diff --git a/.be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/values b/.be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/values index 96cc18c..3292da3 100644 --- a/.be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/values +++ b/.be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/values @@ -1,21 +1,8 @@ +Author: jelmer - -Content-type=text/plain - - - - - - -Date=Sun, 15 Jul 2007 13:34:52 +0000 - - - - - - -From=jelmer +Content-type: text/plain +Date: Sun, 15 Jul 2007 13:34:52 +0000 diff --git a/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/values b/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/values index 3a2ebfb..f460840 100644 --- a/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/values +++ b/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/values @@ -1,10 +1,10 @@ -Content-type: text/html +Author: W. Trevor King -Date: Mon, 22 Jun 2009 20:05:00 +0000 +Content-type: text/html -From: W. Trevor King +Date: Mon, 22 Jun 2009 20:05:00 +0000 In-reply-to: c454aa67-ca30-43e8-9be4-58cbddd01b63 diff --git a/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/values b/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/values index 1456cca..0c7cd6c 100644 --- a/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/values +++ b/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 20:03:27 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 20:03:27 +0000 In-reply-to: d83a5436-85e3-42c7-9a89-a6d50df9d279 diff --git a/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/values b/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/values index 4aea60a..7c8dc1c 100644 --- a/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/values +++ b/.be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Fri, 19 Jun 2009 20:22:19 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Fri, 19 Jun 2009 20:22:19 +0000 diff --git a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values index d7e4f49..929dce6 100644 --- a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values +++ b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Thu, 04 Dec 2008 13:35:42 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Thu, 04 Dec 2008 13:35:42 +0000 diff --git a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values index 9121d12..54570b2 100644 --- a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values +++ b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values @@ -1,21 +1,8 @@ +Author: abentley - -Content-type=text/restructured - - - - - - -Date=Thu, 06 Apr 2006 16:54:57 +0000 - - - - - - -From=abentley +Content-type: text/restructured +Date: Thu, 06 Apr 2006 16:54:57 +0000 diff --git a/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values index 84da235..9f4fd8c 100644 --- a/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values +++ b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 04 Dec 2008 17:29:30 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 04 Dec 2008 17:29:30 +0000 diff --git a/.be/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values b/.be/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values index ba9e33e..a1e25ea 100644 --- a/.be/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values +++ b/.be/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values @@ -1,21 +1,8 @@ +Author: abentley - -Content-type=text/plain - - - - - - -Date=Fri, 27 Jan 2006 14:30:26 +0000 - - - - - - -From=abentley +Content-type: text/plain +Date: Fri, 27 Jan 2006 14:30:26 +0000 diff --git a/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values b/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values index 4cb1f35..ead68e1 100644 --- a/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values +++ b/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values @@ -1,21 +1,8 @@ +Author: abentley - -Content-type=text/plain - - - - - - -Date=Thu, 24 Mar 2005 17:04:47 +0000 - - - - - - -From=abentley +Content-type: text/plain +Date: Thu, 24 Mar 2005 17:04:47 +0000 diff --git a/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values b/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values index 51af41d..90beac0 100644 --- a/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values +++ b/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values @@ -1,21 +1,8 @@ +Author: abentley - -Content-type=text/plain - - - - - - -Date=Thu, 24 Mar 2005 13:05:13 +0000 - - - - - - -From=abentley +Content-type: text/plain +Date: Thu, 24 Mar 2005 13:05:13 +0000 diff --git a/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values index ada2348..2d5059c 100644 --- a/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values +++ b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Mon, 24 Nov 2008 13:08:07 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Mon, 24 Nov 2008 13:08:07 +0000 diff --git a/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values index 2bde2a3..dcdd529 100644 --- a/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values +++ b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values @@ -1,21 +1,8 @@ +Author: abentley - -Content-type=text/plain - - - - - - -Date=Wed, 21 Dec 2005 21:53:47 +0000 - - - - - - -From=abentley +Content-type: text/plain +Date: Wed, 21 Dec 2005 21:53:47 +0000 diff --git a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/values b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/values index 4d945d0..c054670 100644 --- a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/values +++ b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 20:39:39 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 20:39:39 +0000 diff --git a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/values b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/values index 98b7985..32e49e7 100644 --- a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/values +++ b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: abentley -Date: Mon, 17 Apr 2006 20:59:15 +0000 +Content-type: text/plain -From: abentley +Date: Mon, 17 Apr 2006 20:59:15 +0000 In-reply-to: f87fd684-6af1-498d-98d5-f915bcee76a9 diff --git a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/values b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/values index cd3e2bf..d2f0f5c 100644 --- a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/values +++ b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 25 Jun 2009 12:39:26 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 25 Jun 2009 12:39:26 +0000 diff --git a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/values b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/values index 3754f28..8874446 100644 --- a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/values +++ b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 20:42:12 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 20:42:12 +0000 In-reply-to: ec133a4e-c9ff-4499-b469-cb0a2ca9a685 diff --git a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values index 11fb7b0..fe86bd4 100644 --- a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values +++ b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 21:29:13 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 21:29:13 +0000 In-reply-to: f87fd684-6af1-498d-98d5-f915bcee76a9 diff --git a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/values b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/values index 9078dd6..c85b16f 100644 --- a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/values +++ b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 20:40:54 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 20:40:54 +0000 In-reply-to: 401950a0-a5ff-46f3-afac-a9cfb300f94b diff --git a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/values b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/values index 0465a85..2b6307e 100644 --- a/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/values +++ b/.be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 21:29:32 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 21:29:32 +0000 diff --git a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values index 39df7ff..924ca86 100644 --- a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values +++ b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Thu, 27 Nov 2008 14:26:18 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Thu, 27 Nov 2008 14:26:18 +0000 diff --git a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values index ea73789..fb952c2 100644 --- a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values +++ b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Thu, 27 Nov 2008 13:43:47 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Thu, 27 Nov 2008 13:43:47 +0000 diff --git a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values index f109f3e..3453fd9 100644 --- a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values +++ b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Fri, 21 Nov 2008 18:41:47 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Fri, 21 Nov 2008 18:41:47 +0000 diff --git a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values index e0e3783..2a76a4e 100644 --- a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values +++ b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Fri, 21 Nov 2008 19:12:42 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Fri, 21 Nov 2008 19:12:42 +0000 diff --git a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values index e5498c9..d63f4e1 100644 --- a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values +++ b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Fri, 21 Nov 2008 19:01:19 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Fri, 21 Nov 2008 19:01:19 +0000 diff --git a/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/values b/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/values index cebf3cf..dea0808 100644 --- a/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/values +++ b/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 19:46:45 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 19:46:45 +0000 diff --git a/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values b/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values index 560e158..ca6c353 100644 --- a/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values +++ b/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 24 Nov 2008 13:10:38 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 24 Nov 2008 13:10:38 +0000 diff --git a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values index 8270e8e..901c32f 100644 --- a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values +++ b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 04 Dec 2008 18:05:38 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 04 Dec 2008 18:05:38 +0000 diff --git a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values index eb5d3c0..4031ab2 100644 --- a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values +++ b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: abentley -Date: Fri, 31 Mar 2006 22:15:09 +0000 +Content-type: text/plain -From: abentley +Date: Fri, 31 Mar 2006 22:15:09 +0000 diff --git a/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/values b/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/values index 7ba64d0..98f869b 100644 --- a/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/values +++ b/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 18:40:43 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 18:40:43 +0000 diff --git a/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/values b/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/values index 47ac983..cebbded 100644 --- a/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/values +++ b/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 21:12:00 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 21:12:00 +0000 In-reply-to: d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00 diff --git a/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/values b/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/values index 2355aa5..c2f0da8 100644 --- a/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/values +++ b/.be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 19:43:21 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 19:43:21 +0000 diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values index b0ecc8f..1bd47bf 100644 --- a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values +++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Tue, 25 Nov 2008 02:24:04 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Tue, 25 Nov 2008 02:24:04 +0000 diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values index a93e649..7a04fc3 100644 --- a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values +++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values @@ -1,28 +1,11 @@ +Author: W. Trevor King +Content-type: text/plain -Content-type=text/plain +Date: Sat, 22 Nov 2008 21:43:29 +0000 - - - -Date=Sat, 22 Nov 2008 21:43:29 +0000 - - - - - - -From=W. Trevor King - - - - - - -In-reply-to=0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1 - - +In-reply-to: 0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1 diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values index 35b6806..0c87273 100644 --- a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values +++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values @@ -1,28 +1,11 @@ +Author: W. Trevor King +Content-type: text/plain -Content-type=text/plain +Date: Sun, 23 Nov 2008 12:37:57 +0000 - - - -Date=Sun, 23 Nov 2008 12:37:57 +0000 - - - - - - -From=W. Trevor King - - - - - - -In-reply-to=0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1 - - +In-reply-to: 0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1 diff --git a/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values b/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values index 6df7a97..8086b48 100644 --- a/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values +++ b/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Mon, 24 Nov 2008 13:05:07 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Mon, 24 Nov 2008 13:05:07 +0000 diff --git a/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/values b/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/values index 8e0fad6..e94fece 100644 --- a/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/values +++ b/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/values @@ -1,21 +1,8 @@ +Author: abentley - -Content-type=text/plain - - - - - - -Date=Mon, 10 Apr 2006 23:23:25 +0000 - - - - - - -From=abentley +Content-type: text/plain +Date: Mon, 10 Apr 2006 23:23:25 +0000 diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values index afd88e5..8fcd9d4 100644 --- a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values +++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 04 Dec 2008 17:05:49 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 04 Dec 2008 17:05:49 +0000 diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values index 366395d..3033bc1 100644 --- a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values +++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 04 Dec 2008 15:42:07 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 04 Dec 2008 15:42:07 +0000 In-reply-to: 2628eeca-96c6-4933-8484-d55bb1dbf985 diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values index 80e328b..4a24d7e 100644 --- a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values +++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 04 Dec 2008 17:07:25 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 04 Dec 2008 17:07:25 +0000 diff --git a/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/values b/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/values index b7b289e..251453e 100644 --- a/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/values +++ b/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/values @@ -1,21 +1,8 @@ +Author: benf - -Content-type=text/plain - - - - - - -Date=Mon, 21 Apr 2008 03:24:11 +0000 - - - - - - -From=benf +Content-type: text/plain +Date: Mon, 21 Apr 2008 03:24:11 +0000 diff --git a/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values index 1ff89fa..549a346 100644 --- a/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values +++ b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Thu, 04 Dec 2008 13:48:47 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Thu, 04 Dec 2008 13:48:47 +0000 diff --git a/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/values b/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/values index 3a3c77b..cdba2a5 100644 --- a/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/values +++ b/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/values @@ -1,21 +1,8 @@ +Author: j - -Content-type=text/plain - - - - - - -Date=Mon, 14 Apr 2008 16:43:07 +0000 - - - - - - -From=j +Content-type: text/plain +Date: Mon, 14 Apr 2008 16:43:07 +0000 diff --git a/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values b/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values index f94558c..6a0464f 100644 --- a/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values +++ b/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Mon, 24 Nov 2008 13:23:43 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Mon, 24 Nov 2008 13:23:43 +0000 diff --git a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values index 9f2b558..ae76653 100644 --- a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values +++ b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values @@ -1,21 +1,8 @@ +Author: wking - -Content-type=text/plain - - - - - - -Date=Thu, 13 Nov 2008 16:35:24 +0000 - - - - - - -From=wking +Content-type: text/plain +Date: Thu, 13 Nov 2008 16:35:24 +0000 diff --git a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values index 9b72e2c..cf27de6 100644 --- a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values +++ b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Wed, 19 Nov 2008 17:11:51 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Wed, 19 Nov 2008 17:11:51 +0000 diff --git a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values index c404aa9..8103512 100644 --- a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values +++ b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values @@ -1,21 +1,8 @@ +Author: wking - -Content-type=text/plain - - - - - - -Date=Thu, 13 Nov 2008 16:38:36 +0000 - - - - - - -From=wking +Content-type: text/plain +Date: Thu, 13 Nov 2008 16:38:36 +0000 diff --git a/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/values b/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/values index bfc4ff7..6bc9258 100644 --- a/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/values +++ b/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 20:12:35 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 20:12:35 +0000 In-reply-to: 354dcfc6-5997-4ffe-b7a0-baa852213539 diff --git a/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/values b/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/values index bc3434d..d000e42 100644 --- a/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/values +++ b/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 20:11:02 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 20:11:02 +0000 diff --git a/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/values b/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/values index 172a87c..34d6548 100644 --- a/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/values +++ b/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 20:20:39 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 20:20:39 +0000 In-reply-to: f847c981-873e-41ae-b5ce-83dfe60b9afe diff --git a/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/values b/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/values index 0ecd143..62a3fb7 100644 --- a/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/values +++ b/.be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/values @@ -1,10 +1,10 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 20:14:26 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 20:14:26 +0000 In-reply-to: 22348320-40d3-422c-bdf0-0f6a6bde3fab diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values index 368afb3..645e8c9 100644 --- a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values +++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Sat, 22 Nov 2008 21:43:29 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Sat, 22 Nov 2008 21:43:29 +0000 diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values index 5953360..32491b7 100644 --- a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values +++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Sun, 23 Nov 2008 12:37:57 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Sun, 23 Nov 2008 12:37:57 +0000 diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values index 8283996..a0c3bd7 100644 --- a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values +++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Tue, 25 Nov 2008 02:24:05 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Tue, 25 Nov 2008 02:24:05 +0000 diff --git a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/values b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/values index e8c9da6..2dea024 100644 --- a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/values +++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Mon, 22 Jun 2009 19:48:44 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Mon, 22 Jun 2009 19:48:44 +0000 diff --git a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values index e964891..405abc1 100644 --- a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values +++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: wking -Date: Sat, 15 Nov 2008 23:56:51 +0000 +Content-type: text/plain -From: wking +Date: Sat, 15 Nov 2008 23:56:51 +0000 diff --git a/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values index 92e7e86..5972d7a 100644 --- a/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values +++ b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Wed, 19 Nov 2008 01:12:37 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Wed, 19 Nov 2008 01:12:37 +0000 diff --git a/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values index 13df021..8496f0a 100644 --- a/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values +++ b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values @@ -1,21 +1,8 @@ +Author: wking - -Content-type=text/plain - - - - - - -Date=Mon, 17 Nov 2008 15:03:58 +0000 - - - - - - -From=wking +Content-type: text/plain +Date: Mon, 17 Nov 2008 15:03:58 +0000 diff --git a/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/values b/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/values index b2bedbb..ae0ca8f 100644 --- a/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/values +++ b/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/values @@ -1,28 +1,11 @@ +Author: abentley +Content-type: text/plain -Content-type=text/plain +Date: Thu, 14 Sep 2006 18:05:48 +0000 - - - -Date=Thu, 14 Sep 2006 18:05:48 +0000 - - - - - - -From=abentley - - - - - - -In-reply-to=e5decfc6-050b-4283-8776-977bf85b2c99 - - +In-reply-to: e5decfc6-050b-4283-8776-977bf85b2c99 diff --git a/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/values b/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/values index 9e82a6e..72d84b7 100644 --- a/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/values +++ b/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/values @@ -1,21 +1,8 @@ +Author: abentley - -Content-type=text/plain - - - - - - -Date=Thu, 14 Sep 2006 18:03:41 +0000 - - - - - - -From=abentley +Content-type: text/plain +Date: Thu, 14 Sep 2006 18:03:41 +0000 diff --git a/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values index 7bf391a..9825ae8 100644 --- a/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values +++ b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values @@ -1,8 +1,8 @@ -Content-type: text/plain +Author: W. Trevor King -Date: Thu, 04 Dec 2008 17:20:20 +0000 +Content-type: text/plain -From: W. Trevor King +Date: Thu, 04 Dec 2008 17:20:20 +0000 diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values index eb56317..1e12a53 100644 --- a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values +++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Tue, 25 Nov 2008 19:41:02 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Tue, 25 Nov 2008 19:41:02 +0000 diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values index f976972..95751fd 100644 --- a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values +++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Tue, 25 Nov 2008 02:36:16 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Tue, 25 Nov 2008 02:36:16 +0000 diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values index bf5085b..1e4f9c5 100644 --- a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values +++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values @@ -1,21 +1,8 @@ +Author: W. Trevor King - -Content-type=text/plain - - - - - - -Date=Tue, 25 Nov 2008 03:02:59 +0000 - - - - - - -From=W. Trevor King +Content-type: text/plain +Date: Tue, 25 Nov 2008 03:02:59 +0000 diff --git a/.be/settings b/.be/settings index a9bd6dd..2fd475d 100644 --- a/.be/settings +++ b/.be/settings @@ -9,5 +9,5 @@ inactive_status: - Unknown meaning. For backwards compatibility with old BE bugs. -rcs_name: bzr +vcs_name: bzr diff --git a/.be/version b/.be/version index 990837e..7bd05c2 100644 --- a/.be/version +++ b/.be/version @@ -1 +1 @@ -Bugs Everywhere Tree 1 0 +Bugs Everywhere Directory v1.2 diff --git a/Makefile b/Makefile index b1207c2..fe482c3 100644 --- a/Makefile +++ b/Makefile @@ -57,8 +57,8 @@ build: libbe/_version.py .PHONY: install install: doc build python setup.py install ${INSTALL_OPTIONS} - cp -v interfaces/xml/* ${PREFIX}/bin - cp -v interfaces/email/catmutt ${PREFIX}/bin +#cp -v interfaces/xml/* ${PREFIX}/bin +#cp -v interfaces/email/catmutt ${PREFIX}/bin .PHONY: clean diff --git a/be b/be index 36deaba..feacfb4 100755 --- a/be +++ b/be @@ -21,7 +21,7 @@ import os import sys -from libbe import cmdutil, _version +from libbe import cmdutil, version __doc__ = cmdutil.help() @@ -31,6 +31,8 @@ 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("--verbose-version", action="store_true", dest="verbose_version", + help="Print verbose version information and exit.") parser.add_option("-d", "--dir", dest="dir", metavar="DIR", help="Run this command from DIR instead of the current directory.") @@ -50,8 +52,8 @@ except cmdutil.GetCompletions, e: print '\n'.join(e.completions) sys.exit(0) -if options.version == True: - print _version.version_info["revision_id"] +if options.version == True or options.verbose_version == True: + print version.version(verbose=options.verbose_version) sys.exit(0) if options.dir != None: os.chdir(options.dir) diff --git a/becommands/assign.py b/becommands/assign.py index 536bca6..794f028 100644 --- a/becommands/assign.py +++ b/becommands/assign.py @@ -20,28 +20,29 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) >>> bd.bug_from_shortname("a").assigned is None True - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) >>> bd._clear_bugs() >>> bd.bug_from_shortname("a").assigned == bd.user_id True - >>> execute(["a", "someone"], test=True) + >>> execute(["a", "someone"], manipulate_encodings=False) >>> bd._clear_bugs() >>> print bd.bug_from_shortname("a").assigned someone - >>> execute(["a","none"], test=True) + >>> execute(["a","none"], manipulate_encodings=False) >>> bd._clear_bugs() >>> bd.bug_from_shortname("a").assigned is None True + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -53,7 +54,9 @@ def execute(args, test=False): if len(args) > 2: help() raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) bug = bd.bug_from_shortname(args[0]) if len(args) == 1: bug.assigned = bd.user_id diff --git a/becommands/close.py b/becommands/close.py index 0ba8f50..0532ed2 100644 --- a/becommands/close.py +++ b/becommands/close.py @@ -20,18 +20,19 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> from libbe import bugdir >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) >>> print bd.bug_from_shortname("a").status open - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) >>> bd._clear_bugs() >>> print bd.bug_from_shortname("a").status closed + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -41,8 +42,9 @@ def execute(args, test=False): raise cmdutil.UsageError("Please specify a bug id.") if len(args) > 1: raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) - bug = bd.bug_from_shortname(args[0]) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) bug.status = "closed" bd.save() diff --git a/becommands/comment.py b/becommands/comment.py index 55b5913..9a614b2 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -25,20 +25,20 @@ except ImportError: # look for non-core module from elementtree import ElementTree __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import time - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) - >>> execute(["a", "This is a comment about a"], test=True) + >>> execute(["a", "This is a comment about a"], manipulate_encodings=False) >>> bd._clear_bugs() - >>> bug = bd.bug_from_shortname("a") + >>> bug = cmdutil.bug_from_shortname(bd, "a") >>> bug.load_comments(load_full=False) >>> comment = bug.comment_root[0] >>> print comment.body This is a comment about a - >>> comment.From == bd.user_id + >>> comment.author == bd.user_id True >>> comment.time <= int(time.time()) True @@ -47,19 +47,20 @@ def execute(args, test=False): >>> if 'EDITOR' in os.environ: ... del os.environ["EDITOR"] - >>> execute(["b"], test=True) + >>> execute(["b"], manipulate_encodings=False) Traceback (most recent call last): UserError: No comment supplied, and EDITOR not specified. >>> os.environ["EDITOR"] = "echo 'I like cheese' > " - >>> execute(["b"], test=True) + >>> execute(["b"], manipulate_encodings=False) >>> bd._clear_bugs() - >>> bug = bd.bug_from_shortname("b") + >>> bug = cmdutil.bug_from_shortname(bd, "b") >>> bug.load_comments(load_full=False) >>> comment = bug.comment_root[0] >>> print comment.body I like cheese + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -68,10 +69,10 @@ def execute(args, test=False): raise cmdutil.UsageError("Please specify a bug or comment id.") if len(args) > 2: raise cmdutil.UsageError("Too many arguments.") - + shortname = args[0] if shortname.count(':') > 1: - raise cmdutil.UserError("Invalid id '%s'." % shortname) + raise cmdutil.UserError("Invalid id '%s'." % shortname) elif shortname.count(':') == 1: # Split shortname generated by Comment.comment_shortnames() bugname = shortname.split(':')[0] @@ -79,17 +80,17 @@ def execute(args, test=False): else: bugname = shortname is_reply = False - + bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=not test) - bug = bd.bug_from_shortname(bugname) + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, bugname) bug.load_comments(load_full=False) if is_reply: parent = bug.comment_root.comment_from_shortname(shortname, bug_shortname=bugname) else: parent = bug.comment_root - + if len(args) == 1: # try to launch an editor for comment-body entry try: if parent == bug.comment_root: @@ -103,7 +104,6 @@ def execute(args, test=False): raise cmdutil.UserError, "No comment supplied, and EDITOR not specified." if body is None: raise cmdutil.UserError("No comment entered.") - body = body.decode('utf-8') elif args[1] == '-': # read body from stdin binary = not (options.content_type == None or options.content_type.startswith("text/")) @@ -117,11 +117,11 @@ def execute(args, test=False): body = args[1] if not body.endswith('\n'): body+='\n' - + if options.XML == False: new = parent.new_reply(body=body) if options.author != None: - new.From = options.author + new.author = options.author if options.alt_id != None: new.alt_id = options.alt_id if options.content_type != None: diff --git a/becommands/commit.py b/becommands/commit.py index 4f3bdbd..dc70e7e 100644 --- a/becommands/commit.py +++ b/becommands/commit.py @@ -14,7 +14,7 @@ # 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 +from libbe import cmdutil, bugdir, editor, vcs import sys __desc__ = __doc__ @@ -22,13 +22,14 @@ def execute(args, manipulate_encodings=True): """ >>> import os, time >>> from libbe import bug - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) >>> full_path = "testfile" >>> test_contents = "A test file" - >>> bd.rcs.set_file_contents(full_path, test_contents) + >>> bd.vcs.set_file_contents(full_path, test_contents) >>> execute(["Added %s." % (full_path)], manipulate_encodings=False) # doctest: +ELLIPSIS Committed ... + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -48,11 +49,11 @@ def execute(args, manipulate_encodings=True): 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) + body = bd.vcs.get_file_contents(options.body, allow_no_vcs=True) try: - revision = bd.rcs.commit(summary, body=body, + revision = bd.vcs.commit(summary, body=body, allow_empty=options.allow_empty) - except rcs.EmptyCommit, e: + except vcs.EmptyCommit, e: print e return 1 else: diff --git a/becommands/depend.py b/becommands/depend.py index 4a23b0f..f72b8ba 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -14,26 +14,56 @@ # 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 +from libbe import cmdutil, bugdir, tree import os, copy __desc__ = __doc__ -def execute(args, test=False): +BLOCKS_TAG="BLOCKS:" +BLOCKED_BY_TAG="BLOCKED-BY:" + +class BrokenLink (Exception): + def __init__(self, blocked_bug, blocking_bug, blocks=True): + if blocks == True: + msg = "Missing link: %s blocks %s" \ + % (blocking_bug.uuid, blocked_bug.uuid) + else: + msg = "Missing link: %s blocked by %s" \ + % (blocked_bug.uuid, blocking_bug.uuid) + Exception.__init__(self, msg) + self.blocked_bug = blocked_bug + self.blocking_bug = blocking_bug + + +def execute(args, manipulate_encodings=True): """ >>> from libbe import utility - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> bd.save() >>> os.chdir(bd.root) - >>> execute(["a", "b"], test=True) - Blocks on a: + >>> execute(["a", "b"], manipulate_encodings=False) + a blocked by: b - >>> execute(["a"], test=True) - Blocks on a: + >>> execute(["a"], manipulate_encodings=False) + a blocked by: b - >>> execute(["--show-status", "a"], test=True) # doctest: +NORMALIZE_WHITESPACE - Blocks on a: + >>> execute(["--show-status", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + a blocked by: + b closed + >>> execute(["b", "a"], manipulate_encodings=False) + b blocked by: + a + b blocks: + a + >>> execute(["--show-status", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + a blocked by: + b closed + a blocks: b closed - >>> execute(["-r", "a", "b"], test=True) + >>> execute(["-r", "b", "a"], manipulate_encodings=False) + b blocks: + a + >>> execute(["-r", "a", "b"], manipulate_encodings=False) + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -41,45 +71,83 @@ def execute(args, test=False): bugid_args={0: lambda bug : bug.active==True, 1: lambda bug : bug.active==True}) - if len(args) < 1: + if options.repair == True: + if len(args) > 0: + raise cmdutil.UsageError("No arguments with --repair calls.") + elif len(args) < 1: raise cmdutil.UsageError("Please a bug id.") - if len(args) > 2: + elif len(args) > 2: help() raise cmdutil.UsageError("Too many arguments.") - - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) - bugA = bd.bug_from_shortname(args[0]) + elif len(args) == 2 and options.tree_depth != None: + raise cmdutil.UsageError("Only one bug id used in tree mode.") + + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if options.repair == True: + good,fixed,broken = check_dependencies(bd, repair_broken_links=True) + assert len(broken) == 0, broken + if len(fixed) > 0: + print "Fixed the following links:" + print "\n".join(["%s |-- %s" % (blockee.uuid, blocker.uuid) + for blockee,blocker in fixed]) + return 0 + + bugA = cmdutil.bug_from_shortname(bd, args[0]) + + if options.tree_depth != None: + dtree = DependencyTree(bd, bugA, options.tree_depth) + if len(dtree.blocked_by_tree()) > 0: + print "%s blocked by:" % bugA.uuid + for depth,node in dtree.blocked_by_tree().thread(): + if depth == 0: continue + print "%s%s" % (" "*(depth), node.bug.string(shortlist=True)) + if len(dtree.blocks_tree()) > 0: + print "%s blocks:" % bugA.uuid + for depth,node in dtree.blocks_tree().thread(): + if depth == 0: continue + print "%s%s" % (" "*(depth), node.bug.string(shortlist=True)) + return 0 + if len(args) == 2: - bugB = bd.bug_from_shortname(args[1]) - estrs = bugA.extra_strings - depend_string = "BLOCKED-BY:%s" % bugB.uuid + bugB = cmdutil.bug_from_shortname(bd, args[1]) if options.remove == True: - estrs.remove(depend_string) + remove_block(bugA, bugB) else: # add the dependency - estrs.append(depend_string) - bugA.extra_strings = estrs # reassign to notice change - - depends = [] - for estr in bugA.extra_strings: - if estr.startswith("BLOCKED-BY:"): - uuid = estr[11:] - if options.show_status == True: - blocker = bd.bug_from_uuid(uuid) - block_string = "%s\t%s" % (uuid, blocker.status) - else: - block_string = uuid - depends.append(block_string) - if len(depends) > 0: - print "Blocks on %s:" % bugA.uuid - print '\n'.join(depends) + add_block(bugA, bugB) + + blocked_by = get_blocked_by(bd, bugA) + if len(blocked_by) > 0: + print "%s blocked by:" % bugA.uuid + if options.show_status == True: + print '\n'.join(["%s\t%s" % (bug.uuid, bug.status) + for bug in blocked_by]) + else: + print '\n'.join([bug.uuid for bug in blocked_by]) + blocks = get_blocks(bd, bugA) + if len(blocks) > 0: + print "%s blocks:" % bugA.uuid + if options.show_status == True: + print '\n'.join(["%s\t%s" % (bug.uuid, bug.status) + for bug in blocks]) + else: + print '\n'.join([bug.uuid for bug in blocks]) def get_parser(): - parser = cmdutil.CmdOptionParser("be depend BUG-ID [BUG-ID]") - parser.add_option("-r", "--remove", action="store_true", dest="remove", + parser = cmdutil.CmdOptionParser("be depend BUG-ID [BUG-ID]\nor: be depend --repair") + parser.add_option("-r", "--remove", action="store_true", + dest="remove", default=False, help="Remove dependency (instead of adding it)") parser.add_option("-s", "--show-status", action="store_true", - dest="show_status", + dest="show_status", default=False, help="Show status of blocking bugs") + parser.add_option("-t", "--tree-depth", metavar="DEPTH", default=None, + type="int", dest="tree_depth", + help="Print dependency tree rooted at BUG-ID with DEPTH levels of both blockers and blockees. Set DEPTH <= 0 to disable the depth limit.") + parser.add_option("--repair", action="store_true", + dest="repair", default=False, + help="Check for and repair one-way links") return parser longhelp=""" @@ -88,7 +156,184 @@ If bug B is not specified, just print a list of bugs blocking (A). To search for bugs blocked by a particular bug, try $ be list --extra-strings BLOCKED-BY: + +In repair mode, add the missing direction to any one-way links. + +The "|--" symbol in the repair-mode output is inspired by the +"negative feedback" arrow common in biochemistry. See, for example + http://www.nature.com/nature/journal/v456/n7223/images/nature07513-f5.0.jpg """ def help(): return get_parser().help_str() + longhelp + +# internal helper functions + +def _generate_blocks_string(blocked_bug): + return "%s%s" % (BLOCKS_TAG, blocked_bug.uuid) + +def _generate_blocked_by_string(blocking_bug): + return "%s%s" % (BLOCKED_BY_TAG, blocking_bug.uuid) + +def _parse_blocks_string(string): + assert string.startswith(BLOCKS_TAG) + return string[len(BLOCKS_TAG):] + +def _parse_blocked_by_string(string): + assert string.startswith(BLOCKED_BY_TAG) + return string[len(BLOCKED_BY_TAG):] + +def _add_remove_extra_string(bug, string, add): + estrs = bug.extra_strings + if add == True: + estrs.append(string) + else: # remove the string + estrs.remove(string) + bug.extra_strings = estrs # reassign to notice change + +def _get_blocks(bug): + uuids = [] + for line in bug.extra_strings: + if line.startswith(BLOCKS_TAG): + uuids.append(_parse_blocks_string(line)) + return uuids + +def _get_blocked_by(bug): + uuids = [] + for line in bug.extra_strings: + if line.startswith(BLOCKED_BY_TAG): + uuids.append(_parse_blocked_by_string(line)) + return uuids + +def _repair_one_way_link(blocked_bug, blocking_bug, blocks=None): + if blocks == True: # add blocks link + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=True) + else: # add blocked by link + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=True) + +# functions exposed to other modules + +def add_block(blocked_bug, blocking_bug): + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=True) + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=True) + +def remove_block(blocked_bug, blocking_bug): + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=False) + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=False) + +def get_blocks(bugdir, bug): + """ + Return a list of bugs that the given bug blocks. + """ + blocks = [] + for uuid in _get_blocks(bug): + blocks.append(bugdir.bug_from_uuid(uuid)) + return blocks + +def get_blocked_by(bugdir, bug): + """ + Return a list of bugs blocking the given bug blocks. + """ + blocked_by = [] + for uuid in _get_blocked_by(bug): + blocked_by.append(bugdir.bug_from_uuid(uuid)) + return blocked_by + +def check_dependencies(bugdir, repair_broken_links=False): + """ + Check that links are bi-directional for all bugs in bugdir. + + >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) + >>> a = bd.bug_from_uuid("a") + >>> b = bd.bug_from_uuid("b") + >>> blocked_by_string = _generate_blocked_by_string(b) + >>> _add_remove_extra_string(a, blocked_by_string, add=True) + >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=False) + >>> good + [] + >>> repaired + [] + >>> broken + [(Bug(uuid='a'), Bug(uuid='b'))] + >>> _get_blocks(b) + [] + >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=True) + >>> _get_blocks(b) + ['a'] + >>> good + [] + >>> repaired + [(Bug(uuid='a'), Bug(uuid='b'))] + >>> broken + [] + """ + if bugdir.sync_with_disk == True: + bugdir.load_all_bugs() + good_links = [] + fixed_links = [] + broken_links = [] + for bug in bugdir: + for blocker in get_blocked_by(bugdir, bug): + blocks = get_blocks(bugdir, blocker) + if (bug, blocks) in good_links+fixed_links+broken_links: + continue # already checked that link + if bug not in blocks: + if repair_broken_links == True: + _repair_one_way_link(bug, blocker, blocks=True) + fixed_links.append((bug, blocker)) + else: + broken_links.append((bug, blocker)) + else: + good_links.append((bug, blocker)) + for blockee in get_blocks(bugdir, bug): + blocked_by = get_blocked_by(bugdir, blockee) + if (blockee, bug) in good_links+fixed_links+broken_links: + continue # already checked that link + if bug not in blocked_by: + if repair_broken_links == True: + _repair_one_way_link(blockee, bug, blocks=False) + fixed_links.append((blockee, bug)) + else: + broken_links.append((blockee, bug)) + else: + good_links.append((blockee, bug)) + return (good_links, fixed_links, broken_links) + +class DependencyTree (object): + """ + Note: should probably be DependencyDiGraph. + """ + def __init__(self, bugdir, root_bug, depth_limit=0): + self.bugdir = bugdir + self.root_bug = root_bug + self.depth_limit = depth_limit + def _build_tree(self, child_fn): + root = tree.Tree() + root.bug = self.root_bug + root.depth = 0 + stack = [root] + while len(stack) > 0: + node = stack.pop() + if self.depth_limit > 0 and node.depth == self.depth_limit: + continue + for bug in child_fn(self.bugdir, node.bug): + child = tree.Tree() + child.bug = bug + child.depth = node.depth+1 + node.append(child) + stack.append(child) + return root + def blocks_tree(self): + if not hasattr(self, "_blocks_tree"): + self._blocks_tree = self._build_tree(get_blocks) + return self._blocks_tree + def blocked_by_tree(self): + if not hasattr(self, "_blocked_by_tree"): + self._blocked_by_tree = self._build_tree(get_blocked_by) + return self._blocked_by_tree diff --git a/becommands/diff.py b/becommands/diff.py index 13402c0..b6ac5b0 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -20,23 +20,35 @@ from libbe import cmdutil, bugdir, diff import os __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> bd.set_sync_with_disk(True) - >>> original = bd.rcs.commit("Original status") + >>> original = bd.vcs.commit("Original status") >>> bug = bd.bug_from_uuid("a") >>> bug.status = "closed" - >>> changed = bd.rcs.commit("Closed bug a") + >>> changed = bd.vcs.commit("Closed bug a") >>> os.chdir(bd.root) - >>> if bd.rcs.versioned == True: - ... execute([original], test=True) + >>> if bd.vcs.versioned == True: + ... execute([original], manipulate_encodings=False) ... else: - ... print "a:cm: Bug A\\nstatus: open -> closed\\n" - Modified bug reports: - a:cm: Bug A - status: open -> closed + ... print "Modified bugs:\\n a:cm: Bug A\\n Changed bug settings:\\n status: open -> closed" + Modified bugs: + a:cm: Bug A + Changed bug settings: + status: open -> closed + >>> if bd.vcs.versioned == True: + ... execute(["--modified", original], manipulate_encodings=False) + ... else: + ... print "a" + a + >>> if bd.vcs.versioned == False: + ... execute([original], manipulate_encodings=False) + ... else: + ... print "This directory is not revision-controlled." + This directory is not revision-controlled. + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -47,28 +59,31 @@ def execute(args, test=False): revision = args[0] if len(args) > 1: raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) - if bd.rcs.versioned == False: + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if bd.vcs.versioned == False: print "This directory is not revision-controlled." else: + if revision == None: # get the most recent revision + revision = bd.vcs.revision_id(-1) old_bd = bd.duplicate_bugdir(revision) - r,m,a = diff.bug_diffs(old_bd, bd) - - optbugs = [] + d = diff.Diff(old_bd, bd) + tree = d.report_tree() + + uuids = [] if options.all == True: options.new = options.modified = options.removed = True if options.new == True: - optbugs.extend(a) + uuids.extend([c.name for c in tree.child_by_path("/bugs/new")]) if options.modified == True: - optbugs.extend([new for old,new in m]) + uuids.extend([c.name for c in tree.child_by_path("/bugs/mod")]) if options.removed == True: - optbugs.extend(r) - if len(optbugs) > 0: - for bug in optbugs: - print bug.uuid + uuids.extend([c.name for c in tree.child_by_path("/bugs/rem")]) + if (options.new or options.modified or options.removed) == True: + print "\n".join(uuids) else : - rep = diff.diff_report((r,m,a), old_bd, bd).encode(bd.encoding) - if len(rep) > 0: + rep = tree.report_string() + if rep != None: print rep bd.remove_duplicate_bugdir() @@ -85,14 +100,14 @@ def get_parser(): long = "--%s" % s[1] help = s[2] parser.add_option(short, long, action="store_true", - dest=attr, help=help) + default=False, dest=attr, help=help) return parser longhelp=""" -Uses the RCS to compare the current tree with a previous tree, and +Uses the VCS 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. +VCS. For Arch your specifier must be a fully-qualified revision name. diff --git a/becommands/help.py b/becommands/help.py index a8ae338..a8f346a 100644 --- a/becommands/help.py +++ b/becommands/help.py @@ -19,9 +19,11 @@ from libbe import cmdutil, utility __desc__ = __doc__ -def execute(args): +def execute(args, manipulate_encodings=False): """ - Print help of specified command. + Print help of specified command (the manipulate_encodings argument + is ignored). + >>> execute(["help"]) Usage: be help [COMMAND] diff --git a/becommands/html.py b/becommands/html.py index 4835227..908c714 100644 --- a/becommands/html.py +++ b/becommands/html.py @@ -1,40 +1,47 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Marien Zwart -# Thomas Gerigk -# W. Trevor King -# +# Copyright (C) 2009 Gianluca Montecchi +# 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 -"""Re-open a bug""" +# 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. +"""Generate a static HTML dump of the current repository status""" from libbe import cmdutil, bugdir, bug #from html_data import * -import os, re, time, string +import codecs, os, re, string, time +import xml.sax.saxutils, htmlentitydefs __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) - >>> print bd.bug_from_shortname("b").status - closed - >>> execute(["b"], test=True) - >>> bd._clear_bugs() - >>> print bd.bug_from_shortname("b").status - open + >>> execute([], manipulate_encodings=False) + Creating the html output in html_export + >>> os.path.exists("./html_export") + True + >>> os.path.exists("./html_export/index.html") + True + >>> os.path.exists("./html_export/index_inactive.html") + True + >>> os.path.exists("./html_export/bugs") + True + >>> os.path.exists("./html_export/bugs/a.html") + True + >>> os.path.exists("./html_export/bugs/b.html") + True + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -50,7 +57,8 @@ def execute(args, test=False): if len(args) > 0: raise cmdutil.UsageError, "Too many arguments." - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bd.load_all_bugs() status_list = bug.status_values severity_list = bug.severity_values @@ -61,9 +69,9 @@ def execute(args, test=False): bugs_inactive = [] for s in status_list: st[s] = 0 - for b in bd: + for b in sorted(bd, reverse=True): stime[b.uuid] = b.time - if b.status in ["open", "test", "unconfirmed", "assigned"]: + if b.active == True: bugs_active.append(b) else: bugs_inactive.append(b) @@ -73,8 +81,8 @@ def execute(args, test=False): #open_bug_list = sorted([(value,key) for (key,value) in bugs.items()]) html_gen = BEHTMLGen(bd) - html_gen.create_index_file(out_dir, st, bugs_active, ordered_bug_list, "active") - html_gen.create_index_file(out_dir, st, bugs_inactive, ordered_bug_list, "inactive") + html_gen.create_index_file(out_dir, st, bugs_active, ordered_bug_list, "active", bd.encoding) + html_gen.create_index_file(out_dir, st, bugs_inactive, ordered_bug_list, "inactive", bd.encoding) def get_parser(): parser = cmdutil.CmdOptionParser("be open OUTPUT_DIR") @@ -83,7 +91,8 @@ def get_parser(): return parser longhelp=""" -Generate a set of html pages. +Generate a set of html pages representing the current state of the bug +directory. """ def help(): @@ -94,7 +103,18 @@ def complete(options, args, parser): if "--complete" in args: raise cmdutil.GetCompletions() # no positional arguments for list - + +def escape(string): + if string == None: + return "" + chars = [] + for char in xml.sax.saxutils.escape(string): + codepoint = ord(char) + if codepoint in htmlentitydefs.codepoint2name: + char = "&%s;" % htmlentitydefs.codepoint2name[codepoint] + chars.append(char) + return "".join(chars) + class BEHTMLGen(): def __init__(self, bd): self.index_value = "" @@ -353,10 +373,12 @@ class BEHTMLGen(): """ self.index_first = """ + BugsEverywhere Issue Tracker - + @@ -368,17 +390,17 @@ class BEHTMLGen(): - - + +
Active BugsInactive BugsActive BugsInactive Bugs
- +
- """ + """ % self.bd.encoding self.bug_line =""" - + @@ -388,10 +410,12 @@ class BEHTMLGen(): """ self.detail_first = """ + BugsEverywhere Issue Tracker - + @@ -399,11 +423,11 @@ class BEHTMLGen():

BugsEverywhere Bug List

-
Back to Index
+
Back to Index

Bug: _bug_id_

%s %s %s
- """ + """ % self.bd.encoding @@ -430,7 +454,7 @@ class BEHTMLGen(): self.begin_comment_section =""" - ") + FD.write(self.detail_line%("Short name : ", escape(bug.uuid[0:3]))) + FD.write(self.detail_line%("Severity : ", escape(bug.severity))) + FD.write(self.detail_line%("Status : ", escape(bug.status))) + FD.write(self.detail_line%("Assigned : ", escape(bug.assigned))) + FD.write(self.detail_line%("Target : ", escape(bug.target))) + FD.write(self.detail_line%("Reporter : ", escape(bug.reporter))) + FD.write(self.detail_line%("Creator : ", escape(bug.creator))) + FD.write(self.detail_line%("Created : ", escape(bug.time_string))) + FD.write(self.detail_line%("Summary : ", escape(bug.summary))) + FD.write("") FD.write(self.begin_comment_section) tr = [] b = '' level = 0 - for i in bug_.comments(): - if not isinstance(i.in_reply_to,str): - first = True - a = i.string_thread(flatten=False) - d = re.split('\n',a) - for x in range(0,len(d)): - hr = "" - if re.match(" *--------- Comment ---------",d[x]): - com = """ - %s
- %s
- %s
- %s
- %s
- """%(d[x+1],d[x+2],d[x+3],d[x+4],d[x+5]) - l = re.sub("--------- Comment ---------", "", d[x]) - ll = l.split(" ") - la = l - ba = "" - if len(la) > level: - FD.write("
") - if len(la) < level: - FD.write("
") - if len(la) == 0: - if not first : - FD.write("") - first = False - FD.write("
") - level = len(la) - x += 5 - FD.write("--------- Comment ---------

") - FD.write(com) - FD.write("

") + stack = [] + for depth,comment in bug_.comment_root.thread(flatten=False): + while len(stack) > depth: + stack.pop(-1) # pop non-parents off the stack + FD.write("\n") # close non-parent
') + else: + FD.write('
') + FD.write("
\n".join(lines)+"
\n") + while len(stack) > 0: + stack.pop(-1) + FD.write("
\n") # close every remaining
>> from libbe import utility, rcs + >>> from libbe import utility, vcs >>> import os >>> dir = utility.Dir() >>> try: @@ -29,28 +29,28 @@ def execute(args, test=False): ... except bugdir.NoBugDir, e: ... True True - >>> execute(['--root', dir.path], test=True) + >>> execute(['--root', dir.path], manipulate_encodings=False) No revision control detected. Directory initialized. >>> del(dir) >>> dir = utility.Dir() >>> os.chdir(dir.path) - >>> rcs = rcs.installed_rcs() - >>> rcs.init('.') - >>> print rcs.name + >>> vcs = vcs.installed_vcs() + >>> vcs.init('.') + >>> print vcs.name Arch - >>> execute([], test=True) + >>> execute([], manipulate_encodings=False) Using Arch for revision control. Directory initialized. - >>> rcs.cleanup() + >>> vcs.cleanup() >>> try: - ... execute(['--root', '.'], test=True) + ... execute(['--root', '.'], manipulate_encodings=False) ... except cmdutil.UserError, e: ... str(e).startswith("Directory already initialized: ") True - >>> execute(['--root', '/highly-unlikely-to-exist'], test=True) + >>> execute(['--root', '/highly-unlikely-to-exist'], manipulate_encodings=False) Traceback (most recent call last): UserError: No such directory: /highly-unlikely-to-exist >>> os.chdir('/') @@ -64,14 +64,14 @@ def execute(args, test=False): bd = bugdir.BugDir(options.root_dir, from_disk=False, sink_to_existing_root=False, assert_new_BugDir=True, - manipulate_encodings=not test) + manipulate_encodings=manipulate_encodings) except bugdir.NoRootEntry: raise cmdutil.UserError("No such directory: %s" % options.root_dir) except bugdir.AlreadyInitialized: raise cmdutil.UserError("Directory already initialized: %s" % options.root_dir) bd.save() - if bd.rcs.name is not "None": - print "Using %s for revision control." % bd.rcs.name + if bd.vcs.name is not "None": + print "Using %s for revision control." % bd.vcs.name else: print "No revision control detected." print "Directory initialized." @@ -86,7 +86,7 @@ def get_parser(): longhelp=""" This command initializes Bugs Everywhere support for the specified directory and all its subdirectories. It will auto-detect any supported revision control -system. You can use "be set rcs_name" to change the rcs being used. +system. You can use "be set vcs_name" to change the vcs being used. The directory defaults to your current working directory. diff --git a/becommands/list.py b/becommands/list.py index 5ba1821..12e1e29 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -26,16 +26,17 @@ __desc__ = __doc__ AVAILABLE_CMPS = [fn[4:] for fn in dir(bug) if fn[:4] == 'cmp_'] AVAILABLE_CMPS.remove("attr") # a cmp_* template. -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) - >>> execute([], test=True) + >>> execute([], manipulate_encodings=False) a:om: Bug A - >>> execute(["--status", "all"], test=True) + >>> execute(["--status", "all"], manipulate_encodings=False) a:om: Bug A b:cm: Bug B + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -46,11 +47,13 @@ def execute(args, test=False): if options.sort_by != None: for cmp in options.sort_by.split(','): if cmp not in AVAILABLE_CMPS: - raise cmdutil.UserError("Invalid sort on '%s'.\nValid sorts:\n %s" - % (cmp, '\n '.join(AVAILABLE_CMPS))) + raise cmdutil.UserError( + "Invalid sort on '%s'.\nValid sorts:\n %s" + % (cmp, '\n '.join(AVAILABLE_CMPS))) cmp_list.append(eval('bug.cmp_%s' % cmp)) - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bd.load_all_bugs() # select status if options.status != None: diff --git a/becommands/merge.py b/becommands/merge.py index 4aaefa8..f212b01 100644 --- a/becommands/merge.py +++ b/becommands/merge.py @@ -18,10 +18,10 @@ from libbe import cmdutil, bugdir import os, copy __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> from libbe import utility - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> bd.set_sync_with_disk(True) >>> a = bd.bug_from_shortname("a") >>> a.comment_root.time = 0 @@ -37,7 +37,7 @@ def execute(args, test=False): >>> dummy = dummy.new_reply("1 2 3 4") >>> dummy.time = 2 >>> os.chdir(bd.root) - >>> execute(["a", "b"], test=True) + >>> execute(["a", "b"], manipulate_encodings=False) Merging bugs a and b >>> bd._clear_bugs() >>> a = bd.bug_from_shortname("a") @@ -120,6 +120,7 @@ def execute(args, test=False): Merged into bug a >>> print b.status closed + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -133,10 +134,11 @@ def execute(args, test=False): help() raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) - bugA = bd.bug_from_shortname(args[0]) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bugA = cmdutil.bug_from_shortname(bd, args[0]) bugA.load_comments() - bugB = bd.bug_from_shortname(args[1]) + bugB = cmdutil.bug_from_shortname(bd, args[1]) bugB.load_comments() mergeA = bugA.new_comment("Merged from bug %s" % bugB.uuid) newCommTree = copy.deepcopy(bugB.comment_root) diff --git a/becommands/new.py b/becommands/new.py index af599d7..a8ee2ec 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -19,16 +19,16 @@ from libbe import cmdutil, bugdir import sys __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os, time >>> from libbe import bug - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) >>> bug.uuid_gen = lambda: "X" - >>> execute (["this is a test",], test=True) + >>> execute (["this is a test",], manipulate_encodings=False) Created bug with ID X - >>> bd.load() + >>> bd._clear_bugs() >>> bug = bd.bug_from_uuid("X") >>> print bug.summary this is a test @@ -38,13 +38,15 @@ def execute(args, test=False): minor >>> bug.target == None True + >>> bd.cleanup() """ 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 summary message") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) if args[0] == '-': # read summary from stdin summary = sys.stdin.readline() else: diff --git a/becommands/open.py b/becommands/open.py index 2ef5f43..0c6bf05 100644 --- a/becommands/open.py +++ b/becommands/open.py @@ -20,17 +20,18 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) >>> print bd.bug_from_shortname("b").status closed - >>> execute(["b"], test=True) + >>> execute(["b"], manipulate_encodings=False) >>> bd._clear_bugs() >>> print bd.bug_from_shortname("b").status open + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -40,8 +41,9 @@ def execute(args, test=False): raise cmdutil.UsageError, "Please specify a bug id." if len(args) > 1: raise cmdutil.UsageError, "Too many arguments." - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) - bug = bd.bug_from_shortname(args[0]) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) bug.status = "open" def get_parser(): diff --git a/becommands/remove.py b/becommands/remove.py index d79a7be..8d85033 100644 --- a/becommands/remove.py +++ b/becommands/remove.py @@ -17,22 +17,23 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> from libbe import mapfile >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) >>> print bd.bug_from_shortname("b").status closed - >>> execute (["b"], test=True) + >>> execute (["b"], manipulate_encodings=False) Removed bug b >>> bd._clear_bugs() >>> try: ... bd.bug_from_shortname("b") - ... except KeyError: + ... except bugdir.NoBugMatches: ... print "Bug not found" Bug not found + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -40,8 +41,9 @@ def execute(args, test=False): bugid_args={0: lambda bug : bug.active==True}) if len(args) != 1: raise cmdutil.UsageError, "Please specify a bug id." - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) - bug = bd.bug_from_shortname(args[0]) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) bd.remove_bug(bug) print "Removed bug %s" % bug.uuid diff --git a/becommands/set.py b/becommands/set.py index 0c0862f..f7e68d3 100644 --- a/becommands/set.py +++ b/becommands/set.py @@ -19,7 +19,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Change tree settings""" import textwrap -from libbe import cmdutil, bugdir, rcs, settings_object +from libbe import cmdutil, bugdir, vcs, settings_object __desc__ = __doc__ def _value_string(bd, setting): @@ -32,26 +32,28 @@ def _value_string(bd, setting): val = None return str(val) -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) - >>> execute(["target"], test=True) + >>> execute(["target"], manipulate_encodings=False) None - >>> execute(["target", "tomorrow"], test=True) - >>> execute(["target"], test=True) + >>> execute(["target", "tomorrow"], manipulate_encodings=False) + >>> execute(["target"], manipulate_encodings=False) tomorrow - >>> execute(["target", "none"], test=True) - >>> execute(["target"], test=True) + >>> execute(["target", "none"], manipulate_encodings=False) + >>> execute(["target"], manipulate_encodings=False) None + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) complete(options, args, parser) if len(args) > 2: raise cmdutil.UsageError, "Too many arguments" - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) if len(args) == 0: keys = bd.settings_properties keys.sort() @@ -85,12 +87,12 @@ def get_bugdir_settings(): set = getattr(bugdir.BugDir, s) dstr = set.__doc__.strip() # per-setting comment adjustments - if s == "rcs_name": + if s == "vcs_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 + "Unexpected vcs_name docstring:\n '%s'" % dstr lines.insert( 0, "The name of the revision control system to use.\n") dstr = '\n'.join(lines) diff --git a/becommands/severity.py b/becommands/severity.py index 65467e3..660586e 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -20,27 +20,29 @@ from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) minor - >>> execute(["a", "wishlist"], test=True) - >>> execute(["a"], test=True) + >>> execute(["a", "wishlist"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) wishlist - >>> execute(["a", "none"], test=True) + >>> execute(["a", "none"], manipulate_encodings=False) Traceback (most recent call last): UserError: Invalid severity level: none + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) complete(options, args, parser) if len(args) not in (1,2): raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) - bug = bd.bug_from_shortname(args[0]) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) if len(args) == 1: print bug.severity elif len(args) == 2: diff --git a/becommands/show.py b/becommands/show.py index e43cfb9..50bd6eb 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -22,12 +22,12 @@ import sys from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) - >>> execute (["a",], test=True) # doctest: +ELLIPSIS + >>> execute (["a",], manipulate_encodings=False) # doctest: +ELLIPSIS ID : a Short name : a Severity : minor @@ -39,7 +39,7 @@ def execute(args, test=False): Created : ... Bug A - >>> execute (["--xml", "a"], test=True) # doctest: +ELLIPSIS + >>> execute (["--xml", "a"], manipulate_encodings=False) # doctest: +ELLIPSIS a @@ -50,6 +50,7 @@ def execute(args, test=False): ... Bug A + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -57,7 +58,8 @@ def execute(args, test=False): 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) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) if options.XML: print '' % bd.encoding for shortname in args: @@ -72,7 +74,7 @@ def execute(args, test=False): is_comment = False if is_comment == True and options.comments == False: continue - bug = bd.bug_from_shortname(bugname) + bug = cmdutil.bug_from_shortname(bd, bugname) if is_comment == False: if options.XML: print bug.xml(show_comments=options.comments) diff --git a/becommands/status.py b/becommands/status.py index edc948d..f315003 100644 --- a/becommands/status.py +++ b/becommands/status.py @@ -17,27 +17,29 @@ from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) open - >>> execute(["a", "closed"], test=True) - >>> execute(["a"], test=True) + >>> execute(["a", "closed"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) closed - >>> execute(["a", "none"], test=True) + >>> execute(["a", "none"], manipulate_encodings=False) Traceback (most recent call last): UserError: Invalid status: none + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) complete(options, args, parser) if len(args) not in (1,2): raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) - bug = bd.bug_from_shortname(args[0]) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) if len(args) == 1: print bug.status else: @@ -55,7 +57,8 @@ def get_parser(): def help(): try: # See if there are any per-tree status configurations - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False) + 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]) diff --git a/becommands/subscribe.py b/becommands/subscribe.py new file mode 100644 index 0000000..0a23057 --- /dev/null +++ b/becommands/subscribe.py @@ -0,0 +1,390 @@ +# 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. +"""(Un)subscribe to change notification""" +from libbe import cmdutil, bugdir, tree +import os, copy +__desc__ = __doc__ + +TAG="SUBSCRIBE:" + +class SubscriptionType (tree.Tree): + """ + Trees of subscription types to allow users to select exactly what + notifications they want to subscribe to. + """ + def __init__(self, type_name, *args, **kwargs): + tree.Tree.__init__(self, *args, **kwargs) + self.type = type_name + def __str__(self): + return self.type + def __repr__(self): + return "" % str(self) + def string_tree(self, indent=0): + lines = [] + for depth,node in self.thread(): + lines.append("%s%s" % (" "*(indent+2*depth), node)) + return "\n".join(lines) + +BUGDIR_TYPE_NEW = SubscriptionType("new") +BUGDIR_TYPE_ALL = SubscriptionType("all", [BUGDIR_TYPE_NEW]) + +# same name as BUGDIR_TYPE_ALL for consistency +BUG_TYPE_ALL = SubscriptionType(str(BUGDIR_TYPE_ALL)) + +INVALID_TYPE = SubscriptionType("INVALID") + +class InvalidType (ValueError): + def __init__(self, type_name, type_root): + msg = "Invalid type %s for tree:\n%s" \ + % (type_name, type_root.string_tree(4)) + ValueError.__init__(self, msg) + self.type_name = type_name + self.type_root = type_root + + +def execute(args, manipulate_encodings=True): + """ + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> os.chdir(bd.root) + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + [] + >>> execute(["-s","John Doe ", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + John Doe all * + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + ['SUBSCRIBE:John Doe \\tall\\t*'] + >>> execute(["-s","Jane Doe ", "-S", "a.com,b.net", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all a.com,b.net + John Doe all * + >>> execute(["-s","Jane Doe ", "-S", "a.edu", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all a.com,a.edu,b.net + John Doe all * + >>> execute(["-u", "-s","Jane Doe ", "-S", "a.com", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all a.edu,b.net + John Doe all * + >>> execute(["-s","Jane Doe ", "-S", "*", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all * + John Doe all * + >>> execute(["-u", "-s","Jane Doe ", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + John Doe all * + >>> execute(["-u", "-s","John Doe ", "a"], manipulate_encodings=False) + >>> execute(["-s","Jane Doe ", "-t", "new", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for bug directory: + Jane Doe new * + >>> execute(["-s","Jane Doe ", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for bug directory: + Jane Doe all * + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if len(args) > 1: + help() + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + + subscriber = options.subscriber + if subscriber == None: + subscriber = bd.user_id + if options.unsubscribe == True: + if options.servers == None: + options.servers = "INVALID" + if options.types == None: + options.types = "INVALID" + else: + if options.servers == None: + options.servers = "*" + if options.types == None: + options.types = "all" + servers = options.servers.split(",") + types = options.types.split(",") + + if len(args) == 0 or args[0] == "DIR": # directory-wide subscriptions + type_root = BUGDIR_TYPE_ALL + entity = bd + entity_name = "bug directory" + else: # bug-specific subscriptions + type_root = BUG_TYPE_ALL + bug = bd.bug_from_shortname(args[0]) + entity = bug + entity_name = bug.uuid + if options.list_all == True: + entity_name = "anything in the bug directory" + + types = [type_from_name(name, type_root, default=INVALID_TYPE, + default_ok=options.unsubscribe) + for name in types] + estrs = entity.extra_strings + if options.list == True or options.list_all == True: + pass + else: # alter subscriptions + if options.unsubscribe == True: + estrs = unsubscribe(estrs, subscriber, types, servers, type_root) + else: # add the tag + estrs = subscribe(estrs, subscriber, types, servers, type_root) + entity.extra_strings = estrs # reassign to notice change + + if options.list_all == True: + bd.load_all_bugs() + subscriptions = get_bugdir_subscribers(bd, servers[0]) + else: + subscriptions = [] + for estr in entity.extra_strings: + if estr.startswith(TAG): + subscriptions.append(estr[len(TAG):]) + + if len(subscriptions) > 0: + print "Subscriptions for %s:" % entity_name + print '\n'.join(subscriptions) + + +def get_parser(): + parser = cmdutil.CmdOptionParser("be subscribe ID") + parser.add_option("-u", "--unsubscribe", action="store_true", + dest="unsubscribe", default=False, + help="Unsubscribe instead of subscribing.") + parser.add_option("-a", "--list-all", action="store_true", + dest="list_all", default=False, + help="List all subscribers (no ID argument, read only action).") + parser.add_option("-l", "--list", action="store_true", + dest="list", default=False, + help="List subscribers (read only action).") + parser.add_option("-s", "--subscriber", dest="subscriber", + metavar="SUBSCRIBER", + help="Email address of the subscriber (defaults to bugdir.user_id).") + parser.add_option("-S", "--servers", dest="servers", metavar="SERVERS", + help="Servers from which you want notification.") + parser.add_option("-t", "--type", dest="types", metavar="TYPES", + help="Types of changes you wish to be notified about.") + return parser + +longhelp=""" +ID can be either a bug id, or blank/"DIR", in which case it refers to the +whole bug directory. + +SERVERS specifies the servers from which you would like to receive +notification. Multiple severs may be specified in a comma-separated +list, or you can use "*" to match all servers (the default). If you +have not selected a server, it should politely refrain from notifying +you of changes, although there is no way to guarantee this behavior. + +Available TYPES: + For bugs: +%s + For DIR : +%s + +For unsubscription, any listed SERVERS and TYPES are removed from your +subscription. Either the catch-all server "*" or type "%s" will +remove SUBSCRIBER entirely from the specified ID. + +This command is intended for use primarily by public interfaces, since +if you're just hacking away on your private repository, you'll known +what's changed ;). This command just (un)sets the appropriate +subscriptions, and leaves it up to each interface to perform the +notification. +""" % (BUG_TYPE_ALL.string_tree(6), BUGDIR_TYPE_ALL.string_tree(6), + BUGDIR_TYPE_ALL) + +def help(): + return get_parser().help_str() + longhelp + +# internal helper functions + +def _generate_string(subscriber, types, servers): + types = sorted([str(t) for t in types]) + servers = sorted(servers) + return "%s%s\t%s\t%s" % (TAG,subscriber,",".join(types),",".join(servers)) + +def _parse_string(string, type_root): + assert string.startswith(TAG), string + string = string[len(TAG):] + subscriber,types,servers = string.split("\t") + types = [type_from_name(name, type_root) for name in types.split(",")] + return (subscriber,types,servers.split(",")) + +def _get_subscriber(extra_strings, subscriber, type_root): + for i,string in enumerate(extra_strings): + if string.startswith(TAG): + s,ts,srvs = _parse_string(string, type_root) + if s == subscriber: + return i,s,ts,srvs # match! + return None # no match + +# functions exposed to other modules + +def type_from_name(name, type_root, default=None, default_ok=False): + if name == str(type_root): + return type_root + for t in type_root.traverse(): + if name == str(t): + return t + if default_ok: + return default + raise InvalidType(name, type_root) + +def subscribe(extra_strings, subscriber, types, servers, type_root): + args = _get_subscriber(extra_strings, subscriber, type_root) + if args == None: # no match + extra_strings.append(_generate_string(subscriber, types, servers)) + return extra_strings + # Alter matched string + i,s,ts,srvs = args + for t in types: + if t not in ts: + ts.append(t) + # remove descendant types + all_ts = copy.copy(ts) + for t in all_ts: + for tt in all_ts: + if tt in ts and t.has_descendant(tt): + ts.remove(tt) + if "*" in servers+srvs: + srvs = ["*"] + else: + srvs = list(set(servers+srvs)) + extra_strings[i] = _generate_string(subscriber, ts, srvs) + return extra_strings + +def unsubscribe(extra_strings, subscriber, types, servers, type_root): + args = _get_subscriber(extra_strings, subscriber, type_root) + if args == None: # no match + return extra_strings # pass + # Remove matched string + i,s,ts,srvs = args + all_ts = copy.copy(ts) + for t in types: + for tt in all_ts: + if tt in ts and t.has_descendant(tt): + ts.remove(tt) + if "*" in servers+srvs: + srvs = [] + else: + for srv in servers: + if srv in srvs: + srvs.remove(srv) + if len(ts) == 0 or len(srvs) == 0: + extra_strings.pop(i) + else: + extra_strings[i] = _generate_string(subscriber, ts, srvs) + return extra_strings + +def get_subscribers(extra_strings, type, server, type_root, + match_ancestor_types=False, + match_descendant_types=False): + """ + Set match_ancestor_types=True if you want to find eveyone who + cares about your particular type. + + Set match_descendant_types=True if you want to find subscribers + who may only care about some subset of your type. This is useful + for generating lists of all the subscribers in a given set of + extra_strings. + + >>> def sgs(*args, **kwargs): + ... return sorted(get_subscribers(*args, **kwargs)) + >>> es = [] + >>> es = subscribe(es, "John Doe ", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL) + >>> es = subscribe(es, "Jane Doe ", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL) + >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL) + ['John Doe '] + >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL, match_descendant_types=True) + ['Jane Doe ', 'John Doe '] + >>> sgs(es, BUGDIR_TYPE_ALL, "b.net", BUGDIR_TYPE_ALL, match_descendant_types=True) + ['Jane Doe '] + >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL) + ['Jane Doe '] + >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL, match_ancestor_types=True) + ['Jane Doe ', 'John Doe '] + """ + for string in extra_strings: + if not string.startswith(TAG): + continue + subscriber,types,servers = _parse_string(string, type_root) + type_match = False + if type in types: + type_match = True + if type_match == False and match_ancestor_types == True: + for t in types: + if t.has_descendant(type): + type_match = True + break + if type_match == False and match_descendant_types == True: + for t in types: + if type.has_descendant(t): + type_match = True + break + server_match = False + if server in servers or servers == ["*"] or server == "*": + server_match = True + if type_match == True and server_match == True: + yield subscriber + +def get_bugdir_subscribers(bugdir, server): + """ + I have a bugdir. Who cares about it, and what do they care about? + Returns a dict of dicts: + subscribers[user][id] = types + where id is either a bug.uuid (in the case of a bug subscription) + or "DIR" (in the case of a bugdir subscription). + + Only checks bugs that are currently in memory, so you might want + to call bugdir.load_all_bugs() first. + + >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) + >>> a = bd.bug_from_shortname("a") + >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe ", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL) + >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe ", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL) + >>> a.extra_strings = subscribe(a.extra_strings, "John Doe ", [BUG_TYPE_ALL], ["a.com"], BUG_TYPE_ALL) + >>> subscribers = get_bugdir_subscribers(bd, "a.com") + >>> subscribers["Jane Doe "]["DIR"] + [] + >>> subscribers["John Doe "]["DIR"] + [] + >>> subscribers["John Doe "]["a"] + [] + >>> get_bugdir_subscribers(bd, "b.net") + {'Jane Doe ': {'DIR': []}} + >>> bd.cleanup() + """ + subscribers = {} + for sub in get_subscribers(bugdir.extra_strings, BUGDIR_TYPE_ALL, server, + BUGDIR_TYPE_ALL, match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bugdir.extra_strings,sub,BUGDIR_TYPE_ALL) + subscribers[sub] = {"DIR":ts} + for bug in bugdir: + for sub in get_subscribers(bug.extra_strings, BUG_TYPE_ALL, server, + BUG_TYPE_ALL, match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bug.extra_strings,sub,BUG_TYPE_ALL) + if sub in subscribers: + subscribers[sub][bug.uuid] = ts + else: + subscribers[sub] = {bug.uuid:ts} + return subscribers diff --git a/becommands/tag.py b/becommands/tag.py index 216ffbc..ecd853f 100644 --- a/becommands/tag.py +++ b/becommands/tag.py @@ -18,34 +18,34 @@ from libbe import cmdutil, bugdir import os, copy __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> from libbe import utility - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> bd.set_sync_with_disk(True) >>> os.chdir(bd.root) >>> a = bd.bug_from_shortname("a") >>> print a.extra_strings [] - >>> execute(["a", "GUI"], test=True) + >>> execute(["a", "GUI"], manipulate_encodings=False) Tags for a: GUI >>> bd._clear_bugs() # resync our copy of bug >>> a = bd.bug_from_shortname("a") >>> print a.extra_strings ['TAG:GUI'] - >>> execute(["a", "later"], test=True) + >>> execute(["a", "later"], manipulate_encodings=False) Tags for a: GUI later - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) Tags for a: GUI later - >>> execute(["--list"], test=True) + >>> execute(["--list"], manipulate_encodings=False) GUI later - >>> execute(["a", "Alphabetically first"], test=True) + >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) Tags for a: Alphabetically first GUI @@ -57,15 +57,16 @@ def execute(args, test=False): >>> a.extra_strings = [] >>> print a.extra_strings [] - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) >>> bd._clear_bugs() # resync our copy of bug >>> a = bd.bug_from_shortname("a") >>> print a.extra_strings [] - >>> execute(["a", "Alphabetically first"], test=True) + >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) Tags for a: Alphabetically first - >>> execute(["--remove", "a", "Alphabetically first"], test=True) + >>> execute(["--remove", "a", "Alphabetically first"], manipulate_encodings=False) + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -78,7 +79,8 @@ def execute(args, test=False): help() raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) if options.list: bd.load_all_bugs() tags = [] @@ -92,7 +94,7 @@ def execute(args, test=False): if len(tags) > 0: print '\n'.join(tags) return - bug = bd.bug_from_shortname(args[0]) + bug = cmdutil.bug_from_shortname(bd, args[0]) if len(args) == 2: given_tag = args[1] estrs = bug.extra_strings diff --git a/becommands/target.py b/becommands/target.py index 527b16a..7e41451 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -22,21 +22,22 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os - >>> bd = bugdir.simple_bug_dir() + >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) No target assigned. - >>> execute(["a", "tomorrow"], test=True) - >>> execute(["a"], test=True) + >>> execute(["a", "tomorrow"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) tomorrow - >>> execute(["--list"], test=True) + >>> execute(["--list"], manipulate_encodings=False) tomorrow - >>> execute(["a", "none"], test=True) - >>> execute(["a"], test=True) + >>> execute(["a", "none"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) No target assigned. + >>> bd.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) @@ -46,14 +47,15 @@ def execute(args, test=False): if len(args) not in (1, 2): if not (options.list == True and len(args) == 0): raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) if options.list: ts = set([bd.bug_from_uuid(bug).target for bug in bd.list_uuids()]) for target in sorted(ts): if target and isinstance(target,str): print target return - bug = bd.bug_from_shortname(args[0]) + bug = cmdutil.bug_from_shortname(bd, args[0]) if len(args) == 1: if bug.target is None: print "No target assigned." diff --git a/interfaces/README b/interfaces/README new file mode 100644 index 0000000..4d74580 --- /dev/null +++ b/interfaces/README @@ -0,0 +1,34 @@ +Removing spam commits from the history +====================================== + +arch bzr darcs git hg none + +In the case that some spam or inappropriate comment makes its way +through you interface, you can remove the offending commit XYZ with: + + If the offending commit is the last commit: + + arch: + bzr: bzr uncommit && bzr revert + darcs: darcs obliterate --last=1 + git: git reset --hard HEAD^ + hg: hg rollback && hg revert + + If the offending commit is not the last commit: + + arch: + bzr: bzr rebase -r ..-1 --onto before:XYZ . + (requires bzr-rebase plugin, note, you have to increment XYZ by + hand for , because bzr does not support "after:XYZ".) + darcs: darcs obliterate --matches 'name XYZ' + git: git rebase --onto XYZ~1 XYZ + hg: -not-supported- + (From http://hgbook.red-bean.com/read/finding-and-fixing-mistakes.html#id394667 + "Mercurial also does not provide a way to make a file or + changeset completely disappear from history, because there is no + way to enforce its disappearance") + +Note that all of these _change_the_repo_history_, so only do this on +your interface-specific repo before it interacts with any other repo. +Otherwise, you'll have to survive by cherry-picking only the good +commits. diff --git a/interfaces/email/interactive/README b/interfaces/email/interactive/README new file mode 100644 index 0000000..79ef9a9 --- /dev/null +++ b/interfaces/email/interactive/README @@ -0,0 +1,145 @@ +Overview +======== + +The interactive email interface to Bugs Everywhere (BE) attempts to +provide a Debian-bug-tracking-system-style interface to a BE +repository. Users can mail in bug reports, comments, or control +requests, which will be committed to the served repository. +Developers can then pull the changes they approve of from the served +repository into their other repositories and push updates back onto +the served repository. + +For details about the Debian bug tracking system that inspired this +interface, see http://www.debian.org/Bugs . + +Architecture +============ + +In order to reduce setup costs, the entire interface can piggyback on +an existing email address, although from a security standpoint it's +probably best to create a dedicated user. Incoming email is filtered +by procmail, with matching emails being piped into be-handle-mail for +execution. + +Once be-handle-mail receives the email, the parsing method is selected +according to the subject tag that procmail used grab the email in the +first place. There are three parsing styles: + Style Subject + creating bugs [be-bug:submit] new bug summary + commenting on bugs [be-bug:] commit message + control [be-bug] commit message +These are analogous to submit@bugs.debian.org, nnn@bugs.debian.org, +and control@bugs.debian.org respectively. + +Creating bugs +============= + +This interface creates a bug whose summary is given by the email's +post-tag subject. The body of the email must begin with a +pseudo-header containing at least the "Version" field. Anything after +the pseudo-header and before a line starting with '--' is, if present, +attached as the bug's first comment. + + From jdoe@example.com Fri Apr 18 12:00:00 2008 + From: John Doe + Date: Fri, 18 Apr 2008 12:00:00 +0000 + Content-Type: text/plain; charset=UTF-8 + Content-Transfer-Encoding: 8bit + Subject: [be-bug:submit] Need tests for the email interface. + + Version: XYZ + Severity: minor + + Someone should write up a series of test emails to send into + be-handle mail so we can test changes quickly without having to + use procmail. + + -- + Goofy tagline not included. + +Available pseudo-headers are Version, Reporter, Assign, Depend, +Severity, Status, Tag, and Target. + +Commenting on bugs +================== + +This interface appends a comment to the bug specified in the subject +tag. The the first non-multipart body is attached with the +appropriate content-type. In the case of "text/plain" contents, +anything following a line starting with '--' is stripped. + + From jdoe@example.com Fri Apr 18 12:00:00 2008 + From: John Doe + Date: Fri, 18 Apr 2008 12:00:00 +0000 + Content-Type: text/plain; charset=UTF-8 + Content-Transfer-Encoding: 8bit + Subject: [be-bug:XYZ] Isolated problem in baz() + + Finally tracked it down to the bar() call. Some sort of + string<->unicode conversion problem. Solution ideas? + + -- + Goofy tagline not included. + +Controlling bugs +================ + +This interface consists of a list of allowed be commands, with one +command per line. Blank lines and lines beginning with '#' are +ignored, as well anything following a line starting with '--'. All +the listed commands are executed in order and their output returned. +The commands are split into arguments with the POSIX-compliant +shlex.split(). + + From jdoe@example.com Fri Apr 18 12:00:00 2008 + From: John Doe + Date: Fri, 18 Apr 2008 12:00:00 +0000 + Content-Type: text/plain; charset=UTF-8 + Content-Transfer-Encoding: 8bit + Subject: [be-bug] I'll handle XYZ by release 1.2.3 + + assign XYZ "John Doe " + status XYZ assigned + severity XYZ critical + target XYZ 1.2.3 + + -- + Goofy tagline ignored. + +Example emails +============== + +Take a look at my interfaces/email/interactive/examples for some +more examples. + +Procmail rules +============== + +The file _procmailrc as it stands is fairly appropriate for as a +dedicated user's ~/.procmailrc. It forwards matching mail to +be-handle-mail, which should be installed somewhere in the user's +path. All non-matching mail is dumped into /dev/null. Everything +procmail does will be logged to ~/be-mail/procmail.log. + +If you're piggybacking the interface on top of an existing account, +you probably only need to add the be-handle-mail stanza to your +existing ~/.procmailrc, since you will still want to receive non-bug +emails. + +Note that you will probably have to add a + --be-dir /path/to/served/repository +option to the be-handle-mail invocation so it knows what repository to +serve. + +Multiple repositories may be served by the same email address by adding +multiple be-handle-mail stanzas, each matching a different tag, for +example the "[be-bug" portion of the stanza could be "[projectX-bug", +"[projectY-bug", etc. If you change the base tag, be sure to add a + --tag-base "projectX-bug" +or equivalent to your be-handle-mail invocation. + +Testing +======= + +Send test emails in to be-handle-mail with something like + cat examples/blank | ./be-handle-mail -o -l - -a diff --git a/interfaces/email/interactive/_procmailrc b/interfaces/email/interactive/_procmailrc new file mode 100644 index 0000000..d42c0cf --- /dev/null +++ b/interfaces/email/interactive/_procmailrc @@ -0,0 +1,22 @@ +# .procmailrc +# +# see man procmail, procmailrc, and procmailex +# +# If you already have a ~/.procmailrc file, you probably only need to +# insert the bug-email grabbing stanza in your ~/.procmailrc. +# +# This file is released to the Public Domain. + +MAILDIR=$HOME/be-mail +LOGFILE=$MAILDIR/procmail.log + +# Grab all incoming bug emails (but not replies). This rule eats +# matching emails (i.e. no further procmail processing). +:0 +* ^Subject: \[be-bug +* !^Subject:.*\[be-bug].*Re: +| be-handle-mail + +# Drop everything else +:0 +/dev/null diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail new file mode 100644 index 0000000..fa80698 --- /dev/null +++ b/interfaces/email/interactive/be-handle-mail @@ -0,0 +1,950 @@ +#!/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. +""" +Provide and email interface to the distributed bugtracker Bugs +Everywhere. Recieves incoming email via procmail. Provides an +interface similar to the Debian Bug Tracker. There are currently +three distinct email types: submits, comments, and controls. The +email types are differentiated by tags in the email subject. See +SUBJECT_TAG* for the current values. + +Submit emails create a bug (and optionally add some intitial +comments). The post-tag subject is used as the bug summary, and the +email body is parsed for a pseudo-header. Any text after the +psuedo-header but before a possible line starting with BREAK is added +as the initial bug comment. + +Comment emails add comments to a bug. The first non-multipart portion +of the email is used as the comment body. If that portion has a +"text/plain" type, any text after and including a possible line +starting with BREAK is stripped to avoid lots of taglines cluttering +up the repository. + +Control emails preform any allowed BE commands. The first +non-multipart portion of the email is used as the comment body. If +that portion has a "text/plain" type, any text after and including a +possible line starting with BREAK is stripped. Each pre-BREAK line of +the portion should be a valid BE command, with the initial "be" +omitted, e.g. "be status XYZ fixed" --> "status XYZ fixed". + +Any changes made to the repository are commited after the email is +executed, with the email's post-tag subject as the commit message. +""" + +import codecs +import StringIO as StringIO +import email +from email.mime.multipart import MIMEMultipart +import email.utils +import os +import os.path +import re +import shlex +import sys +import time +import traceback +import doctest +import unittest + +from becommands import subscribe +import libbe.cmdutil, libbe.encoding, libbe.utility, libbe.diff, \ + libbe.bugdir, libbe.bug, libbe.comment +import send_pgp_mime + +THIS_SERVER = u"thor.physics.drexel.edu" +THIS_ADDRESS = u"BE Bugs " + +_THIS_DIR = os.path.abspath(os.path.dirname(__file__)) +BE_DIR = _THIS_DIR +LOGPATH = os.path.join(_THIS_DIR, u"be-handle-mail.log") +LOGFILE = None + +# Tag strings generated by generate_global_tags() +SUBJECT_TAG_BASE = u"be-bug" +SUBJECT_TAG_RESPONSE = None +SUBJECT_TAG_START = None +SUBJECT_TAG_NEW = None +SUBJECT_TAG_COMMENT = None +SUBJECT_TAG_CONTROL = None + +BREAK = u"--" +NEW_REQUIRED_PSEUDOHEADERS = [u"Version"] +NEW_OPTIONAL_PSEUDOHEADERS = [u"Reporter", u"Assign", u"Depend", u"Severity", + u"Status", u"Tag", u"Target", + u"Confirm", u"Subscribe"] +CONTROL_COMMENT = u"#" +ALLOWED_COMMANDS = [u"assign", u"comment", u"commit", u"depend", u"help", + u"list", u"merge", u"new", u"open", u"severity", u"show", + u"status", u"subscribe", u"tag", u"target"] + +AUTOCOMMIT = True + +libbe.encoding.ENCODING = u"utf-8" # force default encoding +ENCODING = libbe.encoding.get_encoding() + +class InvalidEmail (ValueError): + def __init__(self, msg, message): + ValueError.__init__(self, message) + self.msg = msg + def response(self): + header = self.msg.response_header + body = [u"Error processing email:\n", + self.response_body(), u""] + response_generator = \ + send_pgp_mime.PGPMimeMessageFactory(u"\n".join(body)) + response = MIMEMultipart() + response.attach(response_generator.plain()) + response.attach(self.msg.msg) + ret = send_pgp_mime.attach_root(header, response) + return ret + def response_body(self): + err_text = [unicode(self)] + return u"\n".join(err_text) + +class InvalidSubject (InvalidEmail): + def __init__(self, msg, message=None): + if message == None: + message = u"Invalid subject" + InvalidEmail.__init__(self, msg, message) + def response_body(self): + err_text = u"\n".join([unicode(self), u"", + u"full subject was:", + self.msg.subject()]) + return err_text + +class InvalidPseudoHeader (InvalidEmail): + def response_body(self): + err_text = [u"Invalid pseudo-header:\n", + unicode(self)] + return u"\n".join(err_text) + +class InvalidCommand (InvalidEmail): + def __init__(self, msg, command, message=None): + bigmessage = u"Invalid execution command '%s'" % command + if message != None: + bigmessage += u"\n%s" % message + InvalidEmail.__init__(self, msg, bigmessage) + self.command = command + +class InvalidOption (InvalidCommand): + def __init__(self, msg, option, message=None): + bigmessage = u"Invalid option '%s'" % (option) + if message != None: + bigmessage += u"\n%s" % message + InvalidCommand.__init__(self, msg, info, command, bigmessage) + self.option = option + +class NotificationFailed (Exception): + def __init__(self, msg): + bigmessage = "Notification failed: %s" % msg + Exception.__init__(self, bigmessage) + self.short_msg = msg + +class ID (object): + """ + Sometimes you want to reference the output of a command that + hasn't been executed yet. ID is there for situations like + > a = Command(msg, "new", ["create a bug"]) + > b = Command(msg, "comment", [ID(a), "and comment on it"]) + """ + def __init__(self, command): + self.command = command + def extract_id(self): + if hasattr(self, "cached_id"): + return self._cached_id + assert self.command.ret == 0, self.command.ret + if self.command.command == u"new": + regexp = re.compile(u"Created bug with ID (.*)") + else: + raise NotImplementedError, self.command.command + match = regexp.match(self.command.stdout) + assert len(match.groups()) == 1, str(match.groups()) + self._cached_id = match.group(1) + return self._cached_id + def __str__(self): + if self.command.ret != 0: + return "" % repr(self.command) + return "" % self.extract_id() + +class Command (object): + """ + A becommands command wrapper. + Doesn't validate input, so do that before initializing. + + Initialize with + Command(msg, command, args=None, stdin=None) + where + msg: the Message instance prompting this command + command: name of becommand to execute, e.g. "new" + args: list of arguments to pass to the command + stdin: if non-null, a string to pipe into the command's stdin + """ + def __init__(self, msg, command, args=None, stdin=None): + self.msg = msg + self.command = command + if args == None: + self.args = [] + else: + self.args = args + self.stdin = stdin + self.ret = None + self.stdout = None + self.stderr = None + self.err = None + def __str__(self): + return "" % (self.command, " ".join([str(s) for s in self.args])) + def normalize_args(self): + """ + Expand any ID placeholders in self.args. + """ + for i,arg in enumerate(self.args): + if isinstance(arg, ID): + self.args[i] = arg.extract_id() + def run(self): + """ + Attempt to execute the command whose info is given in the dictionary + info. Returns the exit code, stdout, and stderr produced by the + command. + """ + if self.command in [None, u""]: # don't accept blank commands + raise InvalidCommand(self.msg, self, "Blank") + elif self.command not in ALLOWED_COMMANDS: + raise InvalidCommand(self.msg, self, "Not allowed") + assert self.ret == None, u"running %s twice!" % unicode(self) + self.normalize_args() + # set stdin and catch stdout and stderr + if self.stdin != None: + orig_stdin = sys.stdin + sys.stdin = StringIO.StringIO(self.stdin) + new_stdout = codecs.getwriter(ENCODING)(StringIO.StringIO()) + new_stderr = codecs.getwriter(ENCODING)(StringIO.StringIO()) + orig_stdout = sys.stdout + orig_stderr = sys.stderr + sys.stdout = new_stdout + sys.stderr = new_stderr + # run the command + os.chdir(BE_DIR) + try: + self.ret = libbe.cmdutil.execute(self.command, self.args, + manipulate_encodings=False) + except libbe.cmdutil.GetHelp: + print libbe.cmdutil.help(command) + except libbe.cmdutil.GetCompletions: + self.err = InvalidOption(self.msg, self.command, u"--complete") + except libbe.cmdutil.UsageError, e: + self.err = InvalidCommand(self.msg, self, + "%s\n%s" % (type(e), unicode(e))) + except libbe.cmdutil.UserError, e: + self.err = InvalidCommand(self.msg, self, + "%s\n%s" % (type(e), unicode(e))) + # restore stdin, stdout, and stderr + if self.stdin != None: + sys.stdin = orig_stdin + sys.stdout.flush() + sys.stderr.flush() + sys.stdout = orig_stdout + sys.stderr = orig_stderr + self.stdout = codecs.decode(new_stdout.getvalue(), ENCODING) + self.stderr = codecs.decode(new_stderr.getvalue(), ENCODING) + if self.err != None: + raise self.err + return (self.ret, self.stdout, self.stderr) + def response_msg(self): + if self.ret == None: self.ret = -1 + response_body = [u"Results of running: (exit code %d)" % self.ret, + u" %s %s" % (self.command, u" ".join(self.args))] + if self.stdout != None and len(self.stdout) > 0: + response_body.extend([u"", u"stdout:", u"", self.stdout]) + if self.stderr != None and len(self.stderr) > 0: + response_body.extend([u"", u"stderr:", u"", self.stderr]) + response_body.append(u"") # trailing endline + response_generator = \ + send_pgp_mime.PGPMimeMessageFactory(u"\n".join(response_body)) + return response_generator.plain() + +class DiffTree (libbe.diff.DiffTree): + """ + In order to avoid tons of tiny MIMEText attachments, bug-level + nodes set .add_child_text=True (in .join()), which is propogated + on to their descendents. Instead of creating their own + attachement, each of these descendents appends his data_part to + the end of the bug-level MIMEText attachment. + + For the example tree in the libbe.diff.Diff unittests: + bugdir + bugdir/settings + bugdir/bugs + bugdir/bugs/new + bugdir/bugs/new/c <- sets .add_child_text + bugdir/bugs/rem + bugdir/bugs/rem/b <- sets .add_child_text + bugdir/bugs/mod + bugdir/bugs/mod/a <- sets .add_child_text + bugdir/bugs/mod/a/settings + bugdir/bugs/mod/a/comments + bugdir/bugs/mod/a/comments/new + bugdir/bugs/mod/a/comments/new/acom + bugdir/bugs/mod/a/comments/rem + bugdir/bugs/mod/a/comments/mod + """ + def report_or_none(self): + report = self.report() + payload = report.get_payload() + if payload == None or len(payload) == 0: + return None + return report + def report_string(self): + report = self.report_or_none() + if report == None: + return "No changes" + else: + return send_pgp_mime.flatten(self.report(), to_unicode=True) + def make_root(self): + return MIMEMultipart() + def join(self, root, parent, data_part): + if hasattr(parent, "attach_child_text"): + self.attach_child_text = True + if data_part != None: + send_pgp_mime.append_text(parent.data_mime_part, u"\n\n%s" % (data_part)) + self.data_mime_part = parent.data_mime_part + else: + self.data_mime_part = None + if data_part != None: + self.data_mime_part = send_pgp_mime.encodedMIMEText(data_part) + if parent != None and parent.name in [u"new", u"rem", u"mod"]: + self.attach_child_text = True + if data_part == None: # make blank data_mime_part for children's appends + self.data_mime_part = send_pgp_mime.encodedMIMEText(u"") + if self.data_mime_part != None: + self.data_mime_part[u"Content-Description"] = self.name + root.attach(self.data_mime_part) + def data_part(self, depth, indent=False): + return libbe.diff.DiffTree.data_part(self, depth, indent=indent) + +class Diff (libbe.diff.Diff): + def bug_add_string(self, bug): + return bug.string(show_comments=True) + def _comment_summary_string(self, comment): + return comment.string() + def comment_add_string(self, comment): + return self._comment_summary_string(comment) + def comment_rem_string(self, comment): + return self._comment_summary_string(comment) + +class Message (object): + def __init__(self, email_text=None, disable_parsing=False): + if disable_parsing == False: + self.text = email_text + p=email.Parser.Parser() + self.msg=p.parsestr(self.text) + if LOGFILE != None: + LOGFILE.write(u"handling %s\n" % self.author_addr()) + LOGFILE.write(u"\n%s\n\n" % self.text) + self.confirm = True # enable/disable confirmation email + def _yes_no(self, boolean): + if boolean == True: + return "yes" + return "no" + def author_tuple(self): + """ + Extract and normalize the sender's email address. Returns a + (name, email) tuple. + """ + if not hasattr(self, "author_tuple_cache"): + self._author_tuple_cache = \ + send_pgp_mime.source_email(self.msg, return_realname=True) + return self._author_tuple_cache + def author_addr(self): + return email.utils.formataddr(self.author_tuple()) + def author_name(self): + return self.author_tuple()[0] + def author_email(self): + return self.author_tuple()[1] + def default_msg_attribute_access(self, attr_name, default=None): + if attr_name in self.msg: + return self.msg[attr_name] + return default + def message_id(self, default=None): + return self.default_msg_attribute_access("message-id", default=default) + def subject(self): + if "subject" not in self.msg: + raise InvalidSubject(self, u"Email must contain a subject") + return self.msg["subject"] + def _split_subject(self): + """ + Returns (tag, subject), with missing values replaced by None. + """ + if hasattr(self, "_split_subject_cache"): + return self._split_subject_cache + args = self.subject().split(u"]",1) + if len(args) < 1: + self._split_subject_cache = (None, None) + elif len(args) < 2: + self._split_subject_cache = (args[0]+u"]", None) + else: + self._split_subject_cache = (args[0]+u"]", args[1].strip()) + return self._split_subject_cache + def _subject_tag_type(self): + """ + Parse subject tag, return (type, value), where type is one of + None, "new", "comment", or "control"; and value is None except + in the case of "comment", in which case it's the bug + ID/shortname. + """ + tag,subject = self._split_subject() + type = None + value = None + if tag == SUBJECT_TAG_NEW: + type = u"new" + elif tag == SUBJECT_TAG_CONTROL: + type = u"control" + else: + match = SUBJECT_TAG_COMMENT.match(tag) + if len(match.groups()) == 1: + type = u"comment" + value = match.group(1) + return (type, value) + def validate_subject(self): + """ + Validate the subject line. + """ + tag,subject = self._split_subject() + if not tag.startswith(SUBJECT_TAG_START): + raise InvalidSubject( + self, u"Subject must start with '%s'" % SUBJECT_TAG_START) + tag_type,value = self._subject_tag_type() + if tag_type == None: + raise InvalidSubject(self, u"Invalid tag '%s'" % tag) + elif tag_type == u"new" and len(subject) == 0: + raise InvalidSubject(self, u"Cannot create a bug with blank title") + elif tag_type == u"comment" and len(value) == 0: + raise InvalidSubject(self, u"Must specify a bug ID to comment") + def _get_bodies_and_mime_types(self): + """ + Traverse the email message returning (body, mime_type) for + each non-mulitpart portion of the message. + """ + msg_charset = self.msg.get_content_charset(ENCODING).lower() + for part in self.msg.walk(): + if part.is_multipart(): + continue + body,mime_type=(part.get_payload(decode=True),part.get_content_type()) + charset = part.get_content_charset(msg_charset).lower() + if mime_type.startswith("text/"): + body = unicode(body, charset) # convert text types to unicode + yield (body, mime_type) + def _parse_body_pseudoheaders(self, body, required, optional, + dictionary=None): + """ + Grab any pseudo-headers from the beginning of body. Raise + InvalidPseudoHeader on errors. Returns the body text after + the pseudo-header and a dictionary of set options. If you + like, you can initialize the dictionary with some defaults + and pass your initialized dict in as dictionary. + """ + if dictionary == None: + dictionary = {} + body_lines = body.splitlines() + all = required+optional + for i,line in enumerate(body_lines): + line = line.strip() + if len(line) == 0: + break + if ":" not in line: + raise InvalidPseudoheader(self, line) + key,value = line.split(":", 1) + value = value.strip() + if key not in all: + raise InvalidPseudoHeader(self, key) + if len(value) == 0: + raise InvalidEmail( + self, u"Blank value for: %s" % key) + dictionary[key] = value + missing = [] + for key in required: + if key not in dictionary: + missing.append(key) + if len(missing) > 0: + raise InvalidPseudoHeader(self, + u"Missing required pseudo-headers:\n%s" + % u", ".join(missing)) + remaining_body = u"\n".join(body_lines[i:]).strip() + return (remaining_body, dictionary) + def _strip_footer(self, body): + body_lines = body.splitlines() + for i,line in enumerate(body_lines): + if line.startswith(BREAK): + break + i += 1 # increment past the current valid line. + return u"\n".join(body_lines[:i]).strip() + def parse(self): + """ + Parse the commands given in the email. Raises assorted + subclasses of InvalidEmail in the case of invalid messages, + otherwise returns a list of suggested commands to run. + """ + self.validate_subject() + tag_type,value = self._subject_tag_type() + if tag_type == u"new": + commands = self.parse_new() + elif tag_type == u"comment": + commands = self.parse_comment(value) + elif tag_type == u"control": + commands = self.parse_control() + else: + raise Exception, u"Unrecognized tag type '%s'" % tag_type + return commands + def parse_new(self): + command = u"new" + tag,subject = self._split_subject() + summary = subject + options = {u"Reporter": self.author_addr(), + u"Confirm": self._yes_no(self.confirm), + u"Subscribe": "no", + } + body,mime_type = list(self._get_bodies_and_mime_types())[0] + comment_body,options = \ + self._parse_body_pseudoheaders(body, + NEW_REQUIRED_PSEUDOHEADERS, + NEW_OPTIONAL_PSEUDOHEADERS, + options) + if options[u"Confirm"].lower() == "no": + self.confirm = False + if options[u"Subscribe"].lower() == "yes" and self.confirm == True: + # respond with the subscription format rather than the + # normal command-output format, because the subscription + # format is more user-friendly. + self.confirm = False + args = [u"--reporter", options[u"Reporter"]] + args.append(summary) + commands = [Command(self, command, args)] + id = ID(commands[0]) + comment_body = self._strip_footer(comment_body) + if len(comment_body) > 0: + command = u"comment" + comment = u"Version: %s\n\n"%options[u"Version"] + comment_body + args = [u"--author", self.author_addr(), + u"--alt-id", self.message_id(), + u"--content-type", mime_type] + args.append(id) + args.append(u"-") + commands.append(Command(self, u"comment", args, stdin=comment)) + for key,value in options.items(): + if key in [u"Version", u"Reporter", u"Confirm"]: + continue # we've already handled these options + command = key.lower() + args = [id, value] + if key == u"Subscribe": + if value.lower() != "yes": + continue + args = ["--subscriber", self.author_addr(), id] + commands.append(Command(self, command, args)) + return commands + def parse_comment(self, bug_uuid): + command = u"comment" + bug_id = bug_uuid + author = self.author_addr() + alt_id = self.message_id() + body,mime_type = list(self._get_bodies_and_mime_types())[0] + if mime_type == "text/plain": + body = self._strip_footer(body) + content_type = mime_type + args = [u"--author", author, u"--alt-id", alt_id, + u"--content-type", content_type, bug_id, u"-"] + commands = [Command(self, command, args, stdin=body)] + return commands + def parse_control(self): + body,mime_type = list(self._get_bodies_and_mime_types())[0] + commands = [] + for line in body.splitlines(): + line = line.strip() + if line.startswith(CONTROL_COMMENT) or len(line) == 0: + continue + if line.startswith(BREAK): + break + fields = shlex.split(line) + command,args = (fields[0], fields[1:]) + commands.append(Command(self, command, args)) + if len(commands) == 0: + raise InvalidEmail(self, u"No commands in control email.") + return commands + def run(self): + self._begin_response() + commands = self.parse() + try: + for command in commands: + command.run() + self._add_response(command.response_msg()) + finally: + if AUTOCOMMIT == True: + tag,subject = self._split_subject() + self.commit_command = Command(self, "commit", [subject]) + self.commit_command.run() + if LOGFILE != None: + LOGFILE.write(u"Autocommit:\n%s\n\n" % + send_pgp_mime.flatten(self.commit_command.response_msg(), + to_unicode=True)) + def _begin_response(self): + tag,subject = self._split_subject() + response_header = [u"From: %s" % THIS_ADDRESS, + u"To: %s" % self.author_addr(), + u"Date: %s" % libbe.utility.time_to_str(time.time()), + u"Subject: %s Re: %s"%(SUBJECT_TAG_RESPONSE,subject) + ] + if self.message_id() != None: + response_header.append(u"In-reply-to: %s" % self.message_id()) + self.response_header = \ + send_pgp_mime.header_from_text(text=u"\n".join(response_header)) + self._response_messages = [] + def _add_response(self, response_message): + self._response_messages.append(response_message) + def response_email(self): + assert len(self._response_messages) > 0 + if len(self._response_messages) == 1: + response_body = self._response_messages[0] + else: + response_body = MIMEMultipart() + for message in self._response_messages: + response_body.attach(message) + return send_pgp_mime.attach_root(self.response_header, response_body) + def subscriber_emails(self, previous_revision=None): + if previous_revision == None: + if AUTOCOMMIT != True: # no way to tell what's changed + raise NotificationFailed("Autocommit dissabled") + if len(self._response_messages) == 0: + raise NotificationFailed("Initial email failed.") + if self.commit_command.ret != 0: + # commit failed. Error already logged. + raise NotificationFailed("Commit failed") + + # read only bugdir. + bd = libbe.bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + if bd.vcs.versioned == False: # no way to tell what's changed + raise NotificationFailed("Not versioned") + + bd.load_all_bugs() + subscribers = subscribe.get_bugdir_subscribers(bd, THIS_SERVER) + + if len(subscribers) == 0: + return [] + + before_bd, after_bd = self._get_before_and_after_bugdirs(bd, previous_revision) + diff = Diff(before_bd, after_bd) + diff_tree = diff.report_tree(diff_tree=DiffTree) + bug_index = {} + for child in diff_tree.child_by_path("/bugs/new"): + bug_index[child.name] = ("added", child) + for child in diff_tree.child_by_path("/bugs/mod"): + bug_index[child.name] = ("modified", child) + for child in diff_tree.child_by_path("/bugs/rem"): + bug_index[child.name] = ("removed", child) + header = self._subscriber_header(bd, previous_revision) + + emails = [] + for subscriber,subscriptions in subscribers.items(): + header.replace_header("to", subscriber) + parts = [] + if "DIR" in subscriptions: # make sure we check the DIR level first + ordered_subscriptions = [("DIR", subscriptions.pop("DIR"))] + else: + ordered_subscriptions = [] + ordered_subscriptions.extend(subscriptions.items()) + for id,types in ordered_subscriptions: + if id == "DIR": + if subscribe.BUGDIR_TYPE_ALL in types: + parts.append(diff_tree.report_or_none()) + break # we've attached everything, so stop checking. + if subscribe.BUGDIR_TYPE_NEW in types: + new = diff_tree.child_by_path("/bugs/new") + parts.append(new.report_or_none()) + continue # move on to next id + # if we get this far, id refers to a bug. + assert types == [subscribe.BUG_TYPE_ALL], types + if id not in bug_index: + continue # no changes here, move on to next id + type,bug_root = bug_index[id] + if type == "added" \ + and "DIR" in subscriptions \ + and subscriptions["DIR"] == subscribe.BUGDIR_TYPE_NEW: + # this info already attached at the DIR level + continue # move on to next id + parts.append(bug_root.report_or_none()) + parts = [p for p in parts if p != None] + if len(parts) == 0: + continue # no email to this subscriber + elif len(parts) == 1: + root = parts[0] + else: # join subscription parts into a single body + root = MIMEMultipart() + root[u"Content-Description"] = u"Multiple subscription trees." + for part in parts: + root.attach(part) + emails.append(send_pgp_mime.attach_root(header, root)) + if LOGFILE != None: + LOGFILE.write(u"Preparing to notify %s of changes\n" % subscriber) + return emails + def _get_before_and_after_bugdirs(self, bd, previous_revision=None): + if previous_revision == None: + commit_msg = self.commit_command.stdout + assert commit_msg.startswith("Committed "), commit_msg + after_revision = commit_msg[len("Committed "):] + before_revision = bd.vcs.revision_id(-2) + else: + before_revision = previous_revision + if before_revision == None: + # this commit was the initial commit + before_bd = libbe.bugdir.BugDir(from_disk=False, + manipulate_encodings=False) + else: + before_bd = bd.duplicate_bugdir(before_revision) + #after_bd = bd.duplicate_bugdir(after_revision) + after_bd = bd # assume no changes since commit a few cycles ago + return (before_bd, after_bd) + def _subscriber_header(self, bd, previous_revision=None): + root_dir = os.path.basename(bd.root) + if previous_revision == None: + subject = "Changes to %s on %s by %s" \ + % (root_dir, THIS_SERVER, self.author_addr()) + else: + subject = "Changes to %s on %s since revision %s" \ + % (root_dir, THIS_SERVER, previous_revision) + header = [u"From: %s" % THIS_ADDRESS, + u"To: %s" % u"DUMMY-AUTHOR", + u"Date: %s" % libbe.utility.time_to_str(time.time()), + u"Subject: %s Re: %s" % (SUBJECT_TAG_RESPONSE, subject) + ] + return send_pgp_mime.header_from_text(text=u"\n".join(header)) + +def generate_global_tags(tag_base=u"be-bug"): + """ + Generate a series of tags from a base tag string. + """ + global SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \ + SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL + SUBJECT_TAG_BASE = tag_base + SUBJECT_TAG_START = u"[%s" % tag_base + SUBJECT_TAG_RESPONSE = u"[%s]" % tag_base + SUBJECT_TAG_NEW = u"[%s:submit]" % tag_base + SUBJECT_TAG_COMMENT = re.compile(u"\[%s:([\-0-9a-z]*)]" % tag_base) + SUBJECT_TAG_CONTROL = SUBJECT_TAG_RESPONSE + +def open_logfile(logpath=None): + """ + If logpath=None, default to global LOGPATH. + Special logpath strings: + "-" set LOGFILE to sys.stderr + "none" disable logging + Relative logpaths are expanded relative to _THIS_DIR + """ + global LOGPATH, LOGFILE + if logpath != None: + if logpath == u"-": + LOGPATH = u"stderr" + LOGFILE = sys.stderr + elif logpath == u"none": + LOGPATH = u"none" + LOGFILE = None + elif os.path.isabs(logpath): + LOGPATH = logpath + else: + LOGPATH = os.path.join(_THIS_DIR, logpath) + if LOGFILE == None and LOGPATH != u"none": + LOGFILE = codecs.open(LOGPATH, u"a+", ENCODING) + LOGFILE.write(u"Default encoding: %s\n" % ENCODING) + +def close_logfile(): + if LOGFILE != None and LOGPATH not in [u"stderr", u"none"]: + LOGFILE.close() + +def test(): + unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + result = unittest.TextTestRunner(verbosity=2).run(suite) + num_errors = len(result.errors) + num_failures = len(result.failures) + num_bad = num_errors + num_failures + return num_bad + +def main(args): + from optparse import OptionParser + global AUTOCOMMIT, BE_DIR + + usage="be-handle-mail [options]\n\n%s" % (__doc__) + parser = OptionParser(usage=usage) + parser.add_option('-b', '--be-dir', dest='be_dir', default=BE_DIR, + metavar="DIR", + help='Select the BE directory to serve (%default).') + parser.add_option('-t', '--tag-base', dest='tag_base', + default=SUBJECT_TAG_BASE, metavar="TAG", + help='Set the subject tag base (%default).') + parser.add_option('-o', '--output', dest='output', action='store_true', + help="Don't mail the generated message, print it to stdout instead. Useful for testing be-handle-mail functionality without the whole mail transfer agent and procmail setup.") + parser.add_option('-l', '--logfile', dest='logfile', metavar='LOGFILE', + help='Set the logfile to LOGFILE. Relative paths are relative to the location of this be-handle-mail file (%s). The special value of "-" directs the log output to stderr, and "none" disables logging.' % _THIS_DIR) + parser.add_option('-a', '--disable-autocommit', dest='autocommit', + default=True, action='store_false', + help='Disable the autocommit after parsing the email.') + parser.add_option('-s', '--disable-subscribers', dest='subscribers', + default=True, action='store_false', + help='Disable subscriber notification emails.') + parser.add_option('--notify-since', dest='notify_since', metavar='REVISION', + help='Notify subscribers of all changes since REVISION. When this option is set, no input email parsing is done.') + parser.add_option('--test', dest='test', action='store_true', + help='Run internal unit-tests and exit.') + + pargs = args + options,args = parser.parse_args(args[1:]) + + if options.test == True: + num_bad = test() + if num_bad > 126: + num_bad = 1 + sys.exit(num_bad) + + BE_DIR = options.be_dir + AUTOCOMMIT = options.autocommit + + if options.notify_since == None: + msg_text = sys.stdin.read() + + libbe.encoding.set_IO_stream_encodings(ENCODING) # _after_ reading message + open_logfile(options.logfile) + generate_global_tags(options.tag_base) + + if options.notify_since != None: + if options.subscribers == True: + if LOGFILE != None: + LOGFILE.write(u"Checking for subscribers to notify since revision %s\n" + % options.notify_since) + try: + m = Message(disable_parsing=True) + emails = m.subscriber_emails(options.notify_since) + except NotificationFailed, e: + if LOGFILE != None: + LOGFILE.write(unicode(e) + u"\n") + else: + for msg in emails: + if options.output == True: + print send_pgp_mime.flatten(msg, to_unicode=True) + else: + send_pgp_mime.mail(msg, send_pgp_mime.sendmail) + close_logfile() + sys.exit(0) + + if len(msg_text.strip()) == 0: # blank email!? + if LOGFILE != None: + LOGFILE.write(u"Blank email!\n") + close_logfile() + sys.exit(1) + try: + m = Message(msg_text) + m.run() + except InvalidEmail, e: + response = e.response() + except Exception, e: + if LOGFILE != None: + LOGFILE.write(u"Uncaught exception:\n%s\n" % (e,)) + traceback.print_tb(sys.exc_traceback, file=LOGFILE) + close_logfile() + sys.exit(1) + else: + response = m.response_email() + if options.output == True: + print send_pgp_mime.flatten(response, to_unicode=True) + elif m.confirm == True: + if LOGFILE != None: + LOGFILE.write(u"Sending response to %s\n" % m.author_addr()) + LOGFILE.write(u"\n%s\n\n" % send_pgp_mime.flatten(response, + to_unicode=True)) + send_pgp_mime.mail(response, send_pgp_mime.sendmail) + else: + if LOGFILE != None: + LOGFILE.write(u"Response declined by %s\n" % m.author_addr()) + if options.subscribers == True: + if LOGFILE != None: + LOGFILE.write(u"Checking for subscribers\n") + try: + emails = m.subscriber_emails() + except NotificationFailed, e: + if LOGFILE != None: + LOGFILE.write(unicode(e) + u"\n") + else: + for msg in emails: + if options.output == True: + print send_pgp_mime.flatten(msg, to_unicode=True) + else: + send_pgp_mime.mail(msg, send_pgp_mime.sendmail) + + close_logfile() + + +class GenerateGlobalTagsTestCase (unittest.TestCase): + def setUp(self): + super(GenerateGlobalTagsTestCase, self).setUp() + self.save_global_tags() + def tearDown(self): + self.restore_global_tags() + super(GenerateGlobalTagsTestCase, self).tearDown() + def save_global_tags(self): + self.saved_globals = [SUBJECT_TAG_BASE, SUBJECT_TAG_START, + SUBJECT_TAG_RESPONSE, SUBJECT_TAG_NEW, + SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL] + def restore_global_tags(self): + global SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \ + SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL + SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \ + SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL = \ + self.saved_globals + def test_restore_global_tags(self): + "Test global tag restoration by teardown function." + global SUBJECT_TAG_BASE + self.failUnlessEqual(SUBJECT_TAG_BASE, u"be-bug") + SUBJECT_TAG_BASE = "projectX-bug" + self.failUnlessEqual(SUBJECT_TAG_BASE, u"projectX-bug") + self.restore_global_tags() + self.failUnlessEqual(SUBJECT_TAG_BASE, u"be-bug") + def test_subject_tag_base(self): + "Should set SUBJECT_TAG_BASE global correctly" + generate_global_tags(u"projectX-bug") + self.failUnlessEqual(SUBJECT_TAG_BASE, u"projectX-bug") + def test_subject_tag_start(self): + "Should set SUBJECT_TAG_START global correctly" + generate_global_tags(u"projectX-bug") + self.failUnlessEqual(SUBJECT_TAG_START, u"[projectX-bug") + def test_subject_tag_response(self): + "Should set SUBJECT_TAG_RESPONSE global correctly" + generate_global_tags(u"projectX-bug") + self.failUnlessEqual(SUBJECT_TAG_RESPONSE, u"[projectX-bug]") + def test_subject_tag_new(self): + "Should set SUBJECT_TAG_NEW global correctly" + generate_global_tags(u"projectX-bug") + self.failUnlessEqual(SUBJECT_TAG_NEW, u"[projectX-bug:submit]") + def test_subject_tag_control(self): + "Should set SUBJECT_TAG_CONTROL global correctly" + generate_global_tags(u"projectX-bug") + self.failUnlessEqual(SUBJECT_TAG_CONTROL, u"[projectX-bug]") + def test_subject_tag_comment(self): + "Should set SUBJECT_TAG_COMMENT global correctly" + generate_global_tags(u"projectX-bug") + m = SUBJECT_TAG_COMMENT.match("[projectX-bug:xyz-123]") + self.failUnlessEqual(len(m.groups()), 1) + self.failUnlessEqual(m.group(1), u"xyz-123") + +if __name__ == "__main__": + main(sys.argv) diff --git a/interfaces/email/interactive/becommands/assign.py b/interfaces/email/interactive/becommands/assign.py new file mode 100644 index 0000000..794f028 --- /dev/null +++ b/interfaces/email/interactive/becommands/assign.py @@ -0,0 +1,87 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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 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. +"""Assign an individual or group to fix a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> bd.bug_from_shortname("a").assigned is None + True + + >>> execute(["a"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bd.bug_from_shortname("a").assigned == bd.user_id + True + + >>> execute(["a", "someone"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("a").assigned + someone + + >>> execute(["a","none"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bd.bug_from_shortname("a").assigned is None + True + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + assert(len(args) in (0, 1, 2)) + if len(args) == 0: + raise cmdutil.UsageError("Please specify a bug id.") + if len(args) > 2: + help() + raise cmdutil.UsageError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + bug = bd.bug_from_shortname(args[0]) + if len(args) == 1: + bug.assigned = bd.user_id + elif len(args) == 2: + if args[1] == "none": + bug.assigned = None + else: + bug.assigned = args[1] + bd.save() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be assign BUG-ID [ASSIGNEE]") + return parser + +longhelp = """ +Assign a person to fix a bug. + +By default, the bug is self-assigned. If an assignee is specified, the bug +will be assigned to that person. + +Assignees should be the person's Bugs Everywhere identity, the string that +appears in Creator fields. + +To un-assign a bug, specify "none" for the assignee. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/close.py b/interfaces/email/interactive/becommands/close.py new file mode 100644 index 0000000..0532ed2 --- /dev/null +++ b/interfaces/email/interactive/becommands/close.py @@ -0,0 +1,60 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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 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. +"""Close a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import bugdir + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("a").status + open + >>> execute(["a"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("a").status + closed + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + if len(args) == 0: + raise cmdutil.UsageError("Please specify a bug id.") + if len(args) > 1: + raise cmdutil.UsageError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + bug.status = "closed" + bd.save() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be close BUG-ID") + return parser + +longhelp=""" +Close the bug identified by BUG-ID. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/comment.py b/interfaces/email/interactive/becommands/comment.py new file mode 100644 index 0000000..9a614b2 --- /dev/null +++ b/interfaces/email/interactive/becommands/comment.py @@ -0,0 +1,228 @@ +# 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 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. +"""Add a comment to a bug""" +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, manipulate_encodings=True): + """ + >>> import time + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a", "This is a comment about a"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bug = cmdutil.bug_from_shortname(bd, "a") + >>> bug.load_comments(load_full=False) + >>> comment = bug.comment_root[0] + >>> print comment.body + This is a comment about a + + >>> comment.author == bd.user_id + True + >>> comment.time <= int(time.time()) + True + >>> comment.in_reply_to is None + True + + >>> if 'EDITOR' in os.environ: + ... del os.environ["EDITOR"] + >>> execute(["b"], manipulate_encodings=False) + Traceback (most recent call last): + UserError: No comment supplied, and EDITOR not specified. + + >>> os.environ["EDITOR"] = "echo 'I like cheese' > " + >>> execute(["b"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bug = cmdutil.bug_from_shortname(bd, "b") + >>> bug.load_comments(load_full=False) + >>> comment = bug.comment_root[0] + >>> print comment.body + I like cheese + + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) == 0: + raise cmdutil.UsageError("Please specify a bug or comment id.") + if len(args) > 2: + raise cmdutil.UsageError("Too many arguments.") + + shortname = args[0] + 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_reply = True + else: + bugname = shortname + is_reply = False + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, bugname) + bug.load_comments(load_full=False) + if is_reply: + parent = bug.comment_root.comment_from_shortname(shortname, + bug_shortname=bugname) + else: + parent = bug.comment_root + + if len(args) == 1: # try to launch an editor for comment-body entry + try: + 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: + raise cmdutil.UserError("No comment entered.") + elif args[1] == '-': # read body from stdin + binary = not (options.content_type == None + or options.content_type.startswith("text/")) + if not binary: + body = sys.stdin.read() + if not body.endswith('\n'): + body+='\n' + else: # read-in without decoding + body = sys.__stdin__.read() + else: # body = arg[1] + body = args[1] + if not body.endswith('\n'): + body+='\n' + + if options.XML == False: + new = parent.new_reply(body=body) + if options.author != None: + new.author = 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=""" +To add a comment to a bug, use the bug ID as the argument. To reply +to another comment, specify the comment name (as shown in "be show" +output). COMMENT, if specified, should be either the text of your +comment or "-", in which case the text will be read from stdin. If +you do not specify a COMMENT, $EDITOR is used to launch an editor. If +COMMENT is unspecified and EDITOR is not set, no comment will be +created. +""" + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + if pos == 0: # fist positional argument is a bug or comment id + if len(args) >= 2: + partial = args[1].split(':')[0] # take only bugid portion + else: + partial = "" + ids = [] + try: + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + bugs = [] + for uuid in bd.list_uuids(): + if uuid.startswith(partial): + bug = bd.bug_from_uuid(uuid) + if bug.active == True: + bugs.append(bug) + for bug in bugs: + shortname = bd.bug_shortname(bug) + ids.append(shortname) + bug.load_comments(load_full=False) + for id,comment in bug.comment_shortnames(shortname): + ids.append(id) + except bugdir.NoBugDir: + pass + raise cmdutil.GetCompletions(ids) + raise cmdutil.GetCompletions() diff --git a/interfaces/email/interactive/becommands/commit.py b/interfaces/email/interactive/becommands/commit.py new file mode 100644 index 0000000..dc70e7e --- /dev/null +++ b/interfaces/email/interactive/becommands/commit.py @@ -0,0 +1,78 @@ +# 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, vcs +import sys +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os, time + >>> from libbe import bug + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> full_path = "testfile" + >>> test_contents = "A test file" + >>> bd.vcs.set_file_contents(full_path, test_contents) + >>> execute(["Added %s." % (full_path)], manipulate_encodings=False) # doctest: +ELLIPSIS + Committed ... + >>> bd.cleanup() + """ + 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.vcs.get_file_contents(options.body, allow_no_vcs=True) + try: + revision = bd.vcs.commit(summary, body=body, + allow_empty=options.allow_empty) + except vcs.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/interfaces/email/interactive/becommands/depend.py b/interfaces/email/interactive/becommands/depend.py new file mode 100644 index 0000000..f72b8ba --- /dev/null +++ b/interfaces/email/interactive/becommands/depend.py @@ -0,0 +1,339 @@ +# 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. +"""Add/remove bug dependencies""" +from libbe import cmdutil, bugdir, tree +import os, copy +__desc__ = __doc__ + +BLOCKS_TAG="BLOCKS:" +BLOCKED_BY_TAG="BLOCKED-BY:" + +class BrokenLink (Exception): + def __init__(self, blocked_bug, blocking_bug, blocks=True): + if blocks == True: + msg = "Missing link: %s blocks %s" \ + % (blocking_bug.uuid, blocked_bug.uuid) + else: + msg = "Missing link: %s blocked by %s" \ + % (blocked_bug.uuid, blocking_bug.uuid) + Exception.__init__(self, msg) + self.blocked_bug = blocked_bug + self.blocking_bug = blocking_bug + + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import utility + >>> bd = bugdir.SimpleBugDir() + >>> bd.save() + >>> os.chdir(bd.root) + >>> execute(["a", "b"], manipulate_encodings=False) + a blocked by: + b + >>> execute(["a"], manipulate_encodings=False) + a blocked by: + b + >>> execute(["--show-status", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + a blocked by: + b closed + >>> execute(["b", "a"], manipulate_encodings=False) + b blocked by: + a + b blocks: + a + >>> execute(["--show-status", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + a blocked by: + b closed + a blocks: + b closed + >>> execute(["-r", "b", "a"], manipulate_encodings=False) + b blocks: + a + >>> execute(["-r", "a", "b"], manipulate_encodings=False) + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True, + 1: lambda bug : bug.active==True}) + + if options.repair == True: + if len(args) > 0: + raise cmdutil.UsageError("No arguments with --repair calls.") + elif len(args) < 1: + raise cmdutil.UsageError("Please a bug id.") + elif len(args) > 2: + help() + raise cmdutil.UsageError("Too many arguments.") + elif len(args) == 2 and options.tree_depth != None: + raise cmdutil.UsageError("Only one bug id used in tree mode.") + + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if options.repair == True: + good,fixed,broken = check_dependencies(bd, repair_broken_links=True) + assert len(broken) == 0, broken + if len(fixed) > 0: + print "Fixed the following links:" + print "\n".join(["%s |-- %s" % (blockee.uuid, blocker.uuid) + for blockee,blocker in fixed]) + return 0 + + bugA = cmdutil.bug_from_shortname(bd, args[0]) + + if options.tree_depth != None: + dtree = DependencyTree(bd, bugA, options.tree_depth) + if len(dtree.blocked_by_tree()) > 0: + print "%s blocked by:" % bugA.uuid + for depth,node in dtree.blocked_by_tree().thread(): + if depth == 0: continue + print "%s%s" % (" "*(depth), node.bug.string(shortlist=True)) + if len(dtree.blocks_tree()) > 0: + print "%s blocks:" % bugA.uuid + for depth,node in dtree.blocks_tree().thread(): + if depth == 0: continue + print "%s%s" % (" "*(depth), node.bug.string(shortlist=True)) + return 0 + + if len(args) == 2: + bugB = cmdutil.bug_from_shortname(bd, args[1]) + if options.remove == True: + remove_block(bugA, bugB) + else: # add the dependency + add_block(bugA, bugB) + + blocked_by = get_blocked_by(bd, bugA) + if len(blocked_by) > 0: + print "%s blocked by:" % bugA.uuid + if options.show_status == True: + print '\n'.join(["%s\t%s" % (bug.uuid, bug.status) + for bug in blocked_by]) + else: + print '\n'.join([bug.uuid for bug in blocked_by]) + blocks = get_blocks(bd, bugA) + if len(blocks) > 0: + print "%s blocks:" % bugA.uuid + if options.show_status == True: + print '\n'.join(["%s\t%s" % (bug.uuid, bug.status) + for bug in blocks]) + else: + print '\n'.join([bug.uuid for bug in blocks]) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be depend BUG-ID [BUG-ID]\nor: be depend --repair") + parser.add_option("-r", "--remove", action="store_true", + dest="remove", default=False, + help="Remove dependency (instead of adding it)") + parser.add_option("-s", "--show-status", action="store_true", + dest="show_status", default=False, + help="Show status of blocking bugs") + parser.add_option("-t", "--tree-depth", metavar="DEPTH", default=None, + type="int", dest="tree_depth", + help="Print dependency tree rooted at BUG-ID with DEPTH levels of both blockers and blockees. Set DEPTH <= 0 to disable the depth limit.") + parser.add_option("--repair", action="store_true", + dest="repair", default=False, + help="Check for and repair one-way links") + return parser + +longhelp=""" +Set a dependency with the second bug (B) blocking the first bug (A). +If bug B is not specified, just print a list of bugs blocking (A). + +To search for bugs blocked by a particular bug, try + $ be list --extra-strings BLOCKED-BY: + +In repair mode, add the missing direction to any one-way links. + +The "|--" symbol in the repair-mode output is inspired by the +"negative feedback" arrow common in biochemistry. See, for example + http://www.nature.com/nature/journal/v456/n7223/images/nature07513-f5.0.jpg +""" + +def help(): + return get_parser().help_str() + longhelp + +# internal helper functions + +def _generate_blocks_string(blocked_bug): + return "%s%s" % (BLOCKS_TAG, blocked_bug.uuid) + +def _generate_blocked_by_string(blocking_bug): + return "%s%s" % (BLOCKED_BY_TAG, blocking_bug.uuid) + +def _parse_blocks_string(string): + assert string.startswith(BLOCKS_TAG) + return string[len(BLOCKS_TAG):] + +def _parse_blocked_by_string(string): + assert string.startswith(BLOCKED_BY_TAG) + return string[len(BLOCKED_BY_TAG):] + +def _add_remove_extra_string(bug, string, add): + estrs = bug.extra_strings + if add == True: + estrs.append(string) + else: # remove the string + estrs.remove(string) + bug.extra_strings = estrs # reassign to notice change + +def _get_blocks(bug): + uuids = [] + for line in bug.extra_strings: + if line.startswith(BLOCKS_TAG): + uuids.append(_parse_blocks_string(line)) + return uuids + +def _get_blocked_by(bug): + uuids = [] + for line in bug.extra_strings: + if line.startswith(BLOCKED_BY_TAG): + uuids.append(_parse_blocked_by_string(line)) + return uuids + +def _repair_one_way_link(blocked_bug, blocking_bug, blocks=None): + if blocks == True: # add blocks link + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=True) + else: # add blocked by link + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=True) + +# functions exposed to other modules + +def add_block(blocked_bug, blocking_bug): + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=True) + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=True) + +def remove_block(blocked_bug, blocking_bug): + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=False) + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=False) + +def get_blocks(bugdir, bug): + """ + Return a list of bugs that the given bug blocks. + """ + blocks = [] + for uuid in _get_blocks(bug): + blocks.append(bugdir.bug_from_uuid(uuid)) + return blocks + +def get_blocked_by(bugdir, bug): + """ + Return a list of bugs blocking the given bug blocks. + """ + blocked_by = [] + for uuid in _get_blocked_by(bug): + blocked_by.append(bugdir.bug_from_uuid(uuid)) + return blocked_by + +def check_dependencies(bugdir, repair_broken_links=False): + """ + Check that links are bi-directional for all bugs in bugdir. + + >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) + >>> a = bd.bug_from_uuid("a") + >>> b = bd.bug_from_uuid("b") + >>> blocked_by_string = _generate_blocked_by_string(b) + >>> _add_remove_extra_string(a, blocked_by_string, add=True) + >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=False) + >>> good + [] + >>> repaired + [] + >>> broken + [(Bug(uuid='a'), Bug(uuid='b'))] + >>> _get_blocks(b) + [] + >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=True) + >>> _get_blocks(b) + ['a'] + >>> good + [] + >>> repaired + [(Bug(uuid='a'), Bug(uuid='b'))] + >>> broken + [] + """ + if bugdir.sync_with_disk == True: + bugdir.load_all_bugs() + good_links = [] + fixed_links = [] + broken_links = [] + for bug in bugdir: + for blocker in get_blocked_by(bugdir, bug): + blocks = get_blocks(bugdir, blocker) + if (bug, blocks) in good_links+fixed_links+broken_links: + continue # already checked that link + if bug not in blocks: + if repair_broken_links == True: + _repair_one_way_link(bug, blocker, blocks=True) + fixed_links.append((bug, blocker)) + else: + broken_links.append((bug, blocker)) + else: + good_links.append((bug, blocker)) + for blockee in get_blocks(bugdir, bug): + blocked_by = get_blocked_by(bugdir, blockee) + if (blockee, bug) in good_links+fixed_links+broken_links: + continue # already checked that link + if bug not in blocked_by: + if repair_broken_links == True: + _repair_one_way_link(blockee, bug, blocks=False) + fixed_links.append((blockee, bug)) + else: + broken_links.append((blockee, bug)) + else: + good_links.append((blockee, bug)) + return (good_links, fixed_links, broken_links) + +class DependencyTree (object): + """ + Note: should probably be DependencyDiGraph. + """ + def __init__(self, bugdir, root_bug, depth_limit=0): + self.bugdir = bugdir + self.root_bug = root_bug + self.depth_limit = depth_limit + def _build_tree(self, child_fn): + root = tree.Tree() + root.bug = self.root_bug + root.depth = 0 + stack = [root] + while len(stack) > 0: + node = stack.pop() + if self.depth_limit > 0 and node.depth == self.depth_limit: + continue + for bug in child_fn(self.bugdir, node.bug): + child = tree.Tree() + child.bug = bug + child.depth = node.depth+1 + node.append(child) + stack.append(child) + return root + def blocks_tree(self): + if not hasattr(self, "_blocks_tree"): + self._blocks_tree = self._build_tree(get_blocks) + return self._blocks_tree + def blocked_by_tree(self): + if not hasattr(self, "_blocked_by_tree"): + self._blocked_by_tree = self._build_tree(get_blocked_by) + return self._blocked_by_tree diff --git a/interfaces/email/interactive/becommands/diff.py b/interfaces/email/interactive/becommands/diff.py new file mode 100644 index 0000000..b6ac5b0 --- /dev/null +++ b/interfaces/email/interactive/becommands/diff.py @@ -0,0 +1,120 @@ +# 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 bug reports with older tree""" +from libbe import cmdutil, bugdir, diff +import os +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> original = bd.vcs.commit("Original status") + >>> bug = bd.bug_from_uuid("a") + >>> bug.status = "closed" + >>> changed = bd.vcs.commit("Closed bug a") + >>> os.chdir(bd.root) + >>> if bd.vcs.versioned == True: + ... execute([original], manipulate_encodings=False) + ... else: + ... print "Modified bugs:\\n a:cm: Bug A\\n Changed bug settings:\\n status: open -> closed" + Modified bugs: + a:cm: Bug A + Changed bug settings: + status: open -> closed + >>> if bd.vcs.versioned == True: + ... execute(["--modified", original], manipulate_encodings=False) + ... else: + ... print "a" + a + >>> if bd.vcs.versioned == False: + ... execute([original], manipulate_encodings=False) + ... else: + ... print "This directory is not revision-controlled." + This directory is not revision-controlled. + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) + if len(args) == 0: + revision = None + if len(args) == 1: + revision = args[0] + if len(args) > 1: + raise cmdutil.UsageError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if bd.vcs.versioned == False: + print "This directory is not revision-controlled." + else: + if revision == None: # get the most recent revision + revision = bd.vcs.revision_id(-1) + old_bd = bd.duplicate_bugdir(revision) + d = diff.Diff(old_bd, bd) + tree = d.report_tree() + + uuids = [] + if options.all == True: + options.new = options.modified = options.removed = True + if options.new == True: + uuids.extend([c.name for c in tree.child_by_path("/bugs/new")]) + if options.modified == True: + uuids.extend([c.name for c in tree.child_by_path("/bugs/mod")]) + if options.removed == True: + uuids.extend([c.name for c in tree.child_by_path("/bugs/rem")]) + if (options.new or options.modified or options.removed) == True: + print "\n".join(uuids) + else : + rep = tree.report_string() + if rep != None: + print rep + bd.remove_duplicate_bugdir() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be diff [options] REVISION") + # boolean options + bools = (("n", "new", "Print UUIDS for new bugs"), + ("m", "modified", "Print UUIDS for modified bugs"), + ("r", "removed", "Print UUIDS for removed bugs"), + ("a", "all", "Print UUIDS for all changed bugs")) + for s in bools: + attr = s[1].replace('-','_') + short = "-%c" % s[0] + long = "--%s" % s[1] + help = s[2] + parser.add_option(short, long, action="store_true", + default=False, dest=attr, help=help) + return parser + +longhelp=""" +Uses the VCS 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 +VCS. + +For Arch your specifier must be a fully-qualified revision name. + +Besides the standard summary output, you can use the options to output +UUIDS for the different categories. This output can be used as the +input to 'be show' to get and understanding of the current status. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/help.py b/interfaces/email/interactive/becommands/help.py new file mode 100644 index 0000000..a8f346a --- /dev/null +++ b/interfaces/email/interactive/becommands/help.py @@ -0,0 +1,68 @@ +# 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 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. +"""Print help for given subcommand""" +from libbe import cmdutil, utility +__desc__ = __doc__ + +def execute(args, manipulate_encodings=False): + """ + Print help of specified command (the manipulate_encodings argument + is ignored). + + >>> execute(["help"]) + Usage: be help [COMMAND] + + Options: + -h, --help Print a help message + --complete Print a list of available completions + + Print help for specified command or list of all commands. + + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) > 1: + raise cmdutil.UsageError("Too many arguments.") + if len(args) == 0: + print cmdutil.help() + else: + try: + print cmdutil.help(args[0]) + except AttributeError: + print "No help available" + +def get_parser(): + parser = cmdutil.CmdOptionParser("be help [COMMAND]") + return parser + +longhelp=""" +Print help for specified command or list of all commands. +""" + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + if "--complete" in args: + cmds = [command for command,module in cmdutil.iter_commands()] + raise cmdutil.GetCompletions(cmds) diff --git a/interfaces/email/interactive/becommands/html.py b/interfaces/email/interactive/becommands/html.py new file mode 100644 index 0000000..908c714 --- /dev/null +++ b/interfaces/email/interactive/becommands/html.py @@ -0,0 +1,588 @@ +# Copyright (C) 2009 Gianluca Montecchi +# 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. +"""Generate a static HTML dump of the current repository status""" +from libbe import cmdutil, bugdir, bug +#from html_data import * +import codecs, os, re, string, time +import xml.sax.saxutils, htmlentitydefs + +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute([], manipulate_encodings=False) + Creating the html output in html_export + >>> os.path.exists("./html_export") + True + >>> os.path.exists("./html_export/index.html") + True + >>> os.path.exists("./html_export/index_inactive.html") + True + >>> os.path.exists("./html_export/bugs") + True + >>> os.path.exists("./html_export/bugs/a.html") + True + >>> os.path.exists("./html_export/bugs/b.html") + True + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==False}) + + if len(args) == 0: + out_dir = options.outdir + print "Creating the html output in %s"%out_dir + else: + out_dir = args[0] + if len(args) > 0: + raise cmdutil.UsageError, "Too many arguments." + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bd.load_all_bugs() + status_list = bug.status_values + severity_list = bug.severity_values + st = {} + se = {} + stime = {} + bugs_active = [] + bugs_inactive = [] + for s in status_list: + st[s] = 0 + for b in sorted(bd, reverse=True): + stime[b.uuid] = b.time + if b.active == True: + bugs_active.append(b) + else: + bugs_inactive.append(b) + st[b.status] += 1 + ordered_bug_list = sorted([(value,key) for (key,value) in stime.items()]) + ordered_bug_list_in = sorted([(value,key) for (key,value) in stime.items()]) + #open_bug_list = sorted([(value,key) for (key,value) in bugs.items()]) + + html_gen = BEHTMLGen(bd) + html_gen.create_index_file(out_dir, st, bugs_active, ordered_bug_list, "active", bd.encoding) + html_gen.create_index_file(out_dir, st, bugs_inactive, ordered_bug_list, "inactive", bd.encoding) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be open OUTPUT_DIR") + parser.add_option("-o", "--output", metavar="export_dir", dest="outdir", + help="Set the output path, default is ./html_export", default="html_export") + return parser + +longhelp=""" +Generate a set of html pages representing the current state of the bug +directory. +""" + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if "--complete" in args: + raise cmdutil.GetCompletions() # no positional arguments for list + + +def escape(string): + if string == None: + return "" + chars = [] + for char in xml.sax.saxutils.escape(string): + codepoint = ord(char) + if codepoint in htmlentitydefs.codepoint2name: + char = "&%s;" % htmlentitydefs.codepoint2name[codepoint] + chars.append(char) + return "".join(chars) + +class BEHTMLGen(): + def __init__(self, bd): + self.index_value = "" + self.bd = bd + + self.css_file = """ + body { + font-family: "lucida grande", "sans serif"; + color: #333; + width: auto; + margin: auto; + } + + + div.main { + padding: 20px; + margin: auto; + padding-top: 0; + margin-top: 1em; + background-color: #fcfcfc; + } + + .comment { + padding: 20px; + margin: auto; + padding-top: 20px; + margin-top: 0; + } + + .commentF { + padding: 0px; + margin: auto; + padding-top: 0px; + paddin-bottom: 20px; + margin-top: 0; + } + + tb { + border = 1; + } + + .wishlist-row { + background-color: #B4FF9B; + width: auto; + } + + .minor-row { + background-color: #FCFF98; + width: auto; + } + + + .serious-row { + background-color: #FFB648; + width: auto; + } + + .critical-row { + background-color: #FF752A; + width: auto; + } + + .fatal-row { + background-color: #FF3300; + width: auto; + } + + .person { + font-family: courier; + } + + a, a:visited { + background: inherit; + text-decoration: none; + } + + a { + color: #003d41; + } + + a:visited { + color: #553d41; + } + + ul { + list-style-type: none; + padding: 0; + } + + p { + width: auto; + } + + .inline-status-image { + position: relative; + top: 0.2em; + } + + .dimmed { + color: #bbb; + } + + table { + border-style: 10px solid #313131; + border-spacing: 0; + width: auto; + } + + table.log { + } + + td { + border-width: 0; + border-style: none; + padding-right: 0.5em; + padding-left: 0.5em; + width: auto; + } + + .td_sel { + background-color: #afafaf; + border: 1px solid #afafaf; + font-weight:bold; + padding-right: 1em; + padding-left: 1em; + + } + + .td_nsel { + border: 0px; + padding-right: 1em; + padding-left: 1em; + } + + tr { + vertical-align: top; + width: auto; + } + + h1 { + padding: 0.5em; + background-color: #305275; + margin-top: 0; + margin-bottom: 0; + color: #fff; + margin-left: -20px; + margin-right: -20px; + } + + wid { + text-transform: uppercase; + font-size: smaller; + margin-top: 1em; + margin-left: -0.5em; + /*background: #fffbce;*/ + /*background: #628a0d;*/ + padding: 5px; + color: #305275; + } + + .attrname { + text-align: right; + font-size: smaller; + } + + .attrval { + color: #222; + } + + .issue-closed-fixed { + background-image: "green-check.png"; + } + + .issue-closed-wontfix { + background-image: "red-check.png"; + } + + .issue-closed-reorg { + background-image: "blue-check.png"; + } + + .inline-issue-link { + text-decoration: underline; + } + + img { + border: 0; + } + + + div.footer { + font-size: small; + padding-left: 20px; + padding-right: 20px; + padding-top: 5px; + padding-bottom: 5px; + margin: auto; + background: #305275; + color: #fffee7; + } + + .footer a { + color: #508d91; + } + + + .header { + font-family: "lucida grande", "sans serif"; + font-size: smaller; + background-color: #a9a9a9; + text-align: left; + + padding-right: 0.5em; + padding-left: 0.5em; + + } + + + .selected-cell { + background-color: #e9e9e2; + } + + .plain-cell { + background-color: #f9f9f9; + } + + + .logcomment { + padding-left: 4em; + font-size: smaller; + } + + .id { + font-family: courier; + } + + .table_bug { + background-color: #afafaf; + border: 2px solid #afafaf; + } + + .message { + } + + .progress-meter-done { + background-color: #03af00; + } + + .progress-meter-undone { + background-color: #ddd; + } + + .progress-meter { + } + + """ + + self.index_first = """ + + + + BugsEverywhere Issue Tracker + + + + + + +
+

BugsEverywhere Bug List

+

+
Comments: + Comments: """ @@ -452,7 +476,7 @@ class BEHTMLGen(): """ - def create_index_file(self, out_dir_path, summary, bugs, ordered_bug, fileid): + def create_index_file(self, out_dir_path, summary, bugs, ordered_bug, fileid, encoding): try: os.stat(out_dir_path) except: @@ -461,7 +485,7 @@ class BEHTMLGen(): except: raise cmdutil.UsageError, "Cannot create output directory." try: - FO = open(out_dir_path+"/style.css", "w") + FO = codecs.open(out_dir_path+"/style.css", "w", encoding) FO.write(self.css_file) FO.close() except: @@ -474,10 +498,10 @@ class BEHTMLGen(): try: if fileid == "active": - FO = open(out_dir_path+"/index.html", "w") + FO = codecs.open(out_dir_path+"/index.html", "w", encoding) FO.write(self.index_first%('td_sel','td_nsel')) if fileid == "inactive": - FO = open(out_dir_path+"/index_inactive.html", "w") + FO = codecs.open(out_dir_path+"/index_inactive.html", "w", encoding) FO.write(self.index_first%('td_nsel','td_sel')) except: raise cmdutil.UsageError, "Cannot create the index.html file." @@ -485,25 +509,25 @@ class BEHTMLGen(): c = 0 t = len(bugs) - 1 for l in range(t, -1, -1): - line = self.bug_line%(bugs[l].severity, - bugs[l].uuid, bugs[l].uuid[0:3], - bugs[l].uuid, bugs[l].status, - bugs[l].uuid, bugs[l].severity, - bugs[l].uuid, bugs[l].summary, - bugs[l].uuid, bugs[l].time_string - ) + line = self.bug_line%(escape(bugs[l].severity), + escape(bugs[l].uuid), escape(bugs[l].uuid[0:3]), + escape(bugs[l].uuid), escape(bugs[l].status), + escape(bugs[l].uuid), escape(bugs[l].severity), + escape(bugs[l].uuid), escape(bugs[l].summary), + escape(bugs[l].uuid), escape(bugs[l].time_string) + ) FO.write(line) c += 1 - self.create_detail_file(bugs[l], out_dir_path, fileid) + self.create_detail_file(bugs[l], out_dir_path, fileid, encoding) when = time.ctime() FO.write(self.index_last%when) - def create_detail_file(self, bug, out_dir_path, fileid): + def create_detail_file(self, bug, out_dir_path, fileid, encoding): f = "%s.html"%bug.uuid p = out_dir_path+"/bugs/"+f try: - FD = open(p, "w") + FD = codecs.open(p, "w", encoding) except: raise cmdutil.UsageError, "Cannot create the detail html file." @@ -519,53 +543,41 @@ class BEHTMLGen(): bug_.load_comments(load_full=True) FD.write(self.detail_line%("ID : ", bug.uuid)) - FD.write(self.detail_line%("Short name : ", bug.uuid[0:3])) - FD.write(self.detail_line%("Severity : ", bug.severity)) - FD.write(self.detail_line%("Status : ", bug.status)) - FD.write(self.detail_line%("Assigned : ", bug.assigned)) - FD.write(self.detail_line%("Target : ", bug.target)) - FD.write(self.detail_line%("Reporter : ", bug.reporter)) - FD.write(self.detail_line%("Creator : ", bug.creator)) - FD.write(self.detail_line%("Created : ", bug.time_string)) - FD.write(self.detail_line%("Summary : ", bug.summary)) - FD.write("


+ + + + + + +
Active BugsInactive Bugs
+ + + """ % self.bd.encoding + + self.bug_line =""" + + + + + + + + """ + + self.detail_first = """ + + + + BugsEverywhere Issue Tracker + + + + + + +
+

BugsEverywhere Bug List

+
Back to Index
+

Bug: _bug_id_

+
%s%s%s%s%s
+ + """ % self.bd.encoding + + + + self.detail_line =""" + + + + """ + + self.index_last = """ + +
%s%s
+ + + + + + + + """ + + self.comment_section = """ + """ + + self.begin_comment_section =""" + + Comments: + + + """ + + + self.end_comment_section =""" + + + """ + + self.detail_last = """ + + + +
Back to Index
+ + + + """ + + + def create_index_file(self, out_dir_path, summary, bugs, ordered_bug, fileid, encoding): + try: + os.stat(out_dir_path) + except: + try: + os.mkdir(out_dir_path) + except: + raise cmdutil.UsageError, "Cannot create output directory." + try: + FO = codecs.open(out_dir_path+"/style.css", "w", encoding) + FO.write(self.css_file) + FO.close() + except: + raise cmdutil.UsageError, "Cannot create the style.css file." + + try: + os.mkdir(out_dir_path+"/bugs") + except: + pass + + try: + if fileid == "active": + FO = codecs.open(out_dir_path+"/index.html", "w", encoding) + FO.write(self.index_first%('td_sel','td_nsel')) + if fileid == "inactive": + FO = codecs.open(out_dir_path+"/index_inactive.html", "w", encoding) + FO.write(self.index_first%('td_nsel','td_sel')) + except: + raise cmdutil.UsageError, "Cannot create the index.html file." + + c = 0 + t = len(bugs) - 1 + for l in range(t, -1, -1): + line = self.bug_line%(escape(bugs[l].severity), + escape(bugs[l].uuid), escape(bugs[l].uuid[0:3]), + escape(bugs[l].uuid), escape(bugs[l].status), + escape(bugs[l].uuid), escape(bugs[l].severity), + escape(bugs[l].uuid), escape(bugs[l].summary), + escape(bugs[l].uuid), escape(bugs[l].time_string) + ) + FO.write(line) + c += 1 + self.create_detail_file(bugs[l], out_dir_path, fileid, encoding) + when = time.ctime() + FO.write(self.index_last%when) + + + def create_detail_file(self, bug, out_dir_path, fileid, encoding): + f = "%s.html"%bug.uuid + p = out_dir_path+"/bugs/"+f + try: + FD = codecs.open(p, "w", encoding) + except: + raise cmdutil.UsageError, "Cannot create the detail html file." + + detail_first_ = re.sub('_bug_id_', bug.uuid[0:3], self.detail_first) + if fileid == "active": + FD.write(detail_first_%"../index.html") + if fileid == "inactive": + FD.write(detail_first_%"../index_inactive.html") + + + + bug_ = self.bd.bug_from_shortname(bug.uuid) + bug_.load_comments(load_full=True) + + FD.write(self.detail_line%("ID : ", bug.uuid)) + FD.write(self.detail_line%("Short name : ", escape(bug.uuid[0:3]))) + FD.write(self.detail_line%("Severity : ", escape(bug.severity))) + FD.write(self.detail_line%("Status : ", escape(bug.status))) + FD.write(self.detail_line%("Assigned : ", escape(bug.assigned))) + FD.write(self.detail_line%("Target : ", escape(bug.target))) + FD.write(self.detail_line%("Reporter : ", escape(bug.reporter))) + FD.write(self.detail_line%("Creator : ", escape(bug.creator))) + FD.write(self.detail_line%("Created : ", escape(bug.time_string))) + FD.write(self.detail_line%("Summary : ", escape(bug.summary))) + FD.write("
") + FD.write(self.begin_comment_section) + tr = [] + b = '' + level = 0 + stack = [] + for depth,comment in bug_.comment_root.thread(flatten=False): + while len(stack) > depth: + stack.pop(-1) # pop non-parents off the stack + FD.write("\n") # close non-parent
') + else: + FD.write('
') + FD.write("
\n".join(lines)+"
\n") + while len(stack) > 0: + stack.pop(-1) + FD.write("
\n") # close every remaining
+# +# 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. +"""Assign the root directory for bug tracking""" +import os.path +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import utility, vcs + >>> import os + >>> dir = utility.Dir() + >>> try: + ... bugdir.BugDir(dir.path) + ... except bugdir.NoBugDir, e: + ... True + True + >>> execute(['--root', dir.path], manipulate_encodings=False) + No revision control detected. + Directory initialized. + >>> del(dir) + + >>> dir = utility.Dir() + >>> os.chdir(dir.path) + >>> vcs = vcs.installed_vcs() + >>> vcs.init('.') + >>> print vcs.name + Arch + >>> execute([], manipulate_encodings=False) + Using Arch for revision control. + Directory initialized. + >>> vcs.cleanup() + + >>> try: + ... execute(['--root', '.'], manipulate_encodings=False) + ... except cmdutil.UserError, e: + ... str(e).startswith("Directory already initialized: ") + True + >>> execute(['--root', '/highly-unlikely-to-exist'], manipulate_encodings=False) + Traceback (most recent call last): + UserError: No such directory: /highly-unlikely-to-exist + >>> os.chdir('/') + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) + if len(args) > 0: + raise cmdutil.UsageError + try: + bd = bugdir.BugDir(options.root_dir, from_disk=False, + sink_to_existing_root=False, + assert_new_BugDir=True, + manipulate_encodings=manipulate_encodings) + except bugdir.NoRootEntry: + raise cmdutil.UserError("No such directory: %s" % options.root_dir) + except bugdir.AlreadyInitialized: + raise cmdutil.UserError("Directory already initialized: %s" % options.root_dir) + bd.save() + if bd.vcs.name is not "None": + print "Using %s for revision control." % bd.vcs.name + else: + print "No revision control detected." + print "Directory initialized." + +def get_parser(): + parser = cmdutil.CmdOptionParser("be init") + parser.add_option("-r", "--root", metavar="DIR", dest="root_dir", + help="Set root dir to something other than the current directory.", + default=".") + return parser + +longhelp=""" +This command initializes Bugs Everywhere support for the specified directory +and all its subdirectories. It will auto-detect any supported revision control +system. You can use "be set vcs_name" to change the vcs being used. + +The directory defaults to your current working directory. + +It is usually a good idea to put the Bugs Everywhere root at the source code +root, but you can put it anywhere. If you root Bugs Everywhere in a +subdirectory, then only bugs created in that subdirectory (and its children) +will appear there. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/list.py b/interfaces/email/interactive/becommands/list.py new file mode 100644 index 0000000..12e1e29 --- /dev/null +++ b/interfaces/email/interactive/becommands/list.py @@ -0,0 +1,248 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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. +"""List bugs""" +from libbe import cmdutil, bugdir, bug +import os +import re +__desc__ = __doc__ + +# get a list of * for cmp_*() comparing two bugs. +AVAILABLE_CMPS = [fn[4:] for fn in dir(bug) if fn[:4] == 'cmp_'] +AVAILABLE_CMPS.remove("attr") # a cmp_* template. + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute([], manipulate_encodings=False) + a:om: Bug A + >>> execute(["--status", "all"], manipulate_encodings=False) + a:om: Bug A + b:cm: Bug B + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) > 0: + raise cmdutil.UsageError("Too many arguments.") + cmp_list = [] + if options.sort_by != None: + for cmp in options.sort_by.split(','): + if cmp not in AVAILABLE_CMPS: + raise cmdutil.UserError( + "Invalid sort on '%s'.\nValid sorts:\n %s" + % (cmp, '\n '.join(AVAILABLE_CMPS))) + cmp_list.append(eval('bug.cmp_%s' % cmp)) + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bd.load_all_bugs() + # select status + if options.status != None: + if options.status == "all": + status = bug.status_values + else: + status = options.status.split(',') + else: + status = [] + if options.active == True: + status.extend(list(bug.active_status_values)) + if options.unconfirmed == True: + status.append("unconfirmed") + if options.open == True: + status.append("opened") + if options.test == True: + status.append("test") + if status == []: # set the default value + status = bug.active_status_values + # select severity + if options.severity != None: + if options.severity == "all": + severity = bug.severity_values + else: + severity = options.severity.split(',') + else: + severity = [] + if options.wishlist == True: + severity.extend("wishlist") + if options.important == True: + serious = bug.severity_values.index("serious") + severity.append(list(bug.severity_values[serious:])) + if severity == []: # set the default value + severity = bug.severity_values + # select assigned + if options.assigned != None: + if options.assigned == "all": + assigned = "all" + else: + assigned = options.assigned.split(',') + else: + assigned = [] + if options.mine == True: + assigned.extend('-') + if assigned == []: # set the default value + assigned = "all" + for i in range(len(assigned)): + if assigned[i] == '-': + assigned[i] = bd.user_id + # select target + if options.target != None: + if options.target == "all": + target = "all" + else: + target = options.target.split(',') + else: + target = [] + if options.cur_target == True: + target.append(bd.target) + if target == []: # set the default value + target = "all" + if options.extra_strings != None: + extra_string_regexps = [re.compile(x) for x in options.extra_strings.split(',')] + + def filter(bug): + if status != "all" and not bug.status in status: + return False + if severity != "all" and not bug.severity in severity: + return False + if assigned != "all" and not bug.assigned in assigned: + return False + if target != "all" and not bug.target in target: + return False + if options.extra_strings != None: + if len(bug.extra_strings) == 0 and len(extra_string_regexps) > 0: + return False + for string in bug.extra_strings: + for regexp in extra_string_regexps: + if not regexp.match(string): + return False + return True + + bugs = [b for b in bd if filter(b) ] + 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: + print cmdutil.underlined(title) + for bg in cur_bugs: + if xml == True: + print bg.xml(show_comments=True) + elif just_uuids: + print bg.uuid + else: + print bg.string(shortlist=True) + if xml == True: + print "" + + # sort bugs + cmp_list.extend(bug.DEFAULT_CMP_FULL_CMP_LIST) + cmp_fn = bug.BugCompoundComparator(cmp_list=cmp_list) + bugs.sort(cmp_fn) + + # print list of bugs + list_bugs(bugs, just_uuids=options.uuids, xml=options.xml) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be list [options]") + parser.add_option("-s", "--status", metavar="STATUS", dest="status", + help="List bugs matching STATUS", default=None) + parser.add_option("-v", "--severity", metavar="SEVERITY", dest="severity", + help="List bugs matching SEVERITY", default=None) + parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", + help="List bugs matching ASSIGNED", default=None) + parser.add_option("-t", "--target", metavar="TARGET", dest="target", + help="List bugs matching TARGET", default=None) + parser.add_option("-e", "--extra-strings", metavar="STRINGS", dest="extra_strings", + help="List bugs matching _all_ extra strings in comma-seperated list STRINGS. e.g. --extra-strings TAG:working,TAG:xml", default=None) + parser.add_option("-S", "--sort", metavar="SORT-BY", dest="sort_by", + help="Adjust bug-sort criteria with comma-separated list SORT-BY. e.g. \"--sort creator,time\". Available criteria: %s" % ','.join(AVAILABLE_CMPS), default=None) + # boolean options. All but uuids and xml are special cases of long forms + bools = (("u", "uuids", "Only print the bug UUIDS"), + ("w", "wishlist", "List bugs with 'wishlist' severity"), + ("i", "important", "List bugs with >= 'serious' severity"), + ("A", "active", "List all active bugs"), + ("U", "unconfirmed", "List unconfirmed bugs"), + ("o", "open", "List open bugs"), + ("T", "test", "List bugs in testing"), + ("m", "mine", "List bugs assigned to you"), + ("c", "cur-target", "List bugs for the current target"), + ("x", "xml", "Dump as XML")) + for s in bools: + attr = s[1].replace('-','_') + short = "-%c" % s[0] + long = "--%s" % s[1] + help = s[2] + parser.add_option(short, long, action="store_true", + dest=attr, help=help) + return parser + + +def help(): + longhelp=""" +This command lists bugs. Normally it prints a short string like + 576:om: Allow attachments +Where + 576 the bug id + o the bug status is 'open' (first letter) + m the bug severity is 'minor' (first letter) + Allo... the bug summary string + +You can optionally (-u) print only the bug ids. + +There are several criteria that you can filter by: + * status + * severity + * assigned (who the bug is assigned to) + * target (bugfix deadline) +Allowed values for each criterion may be given in a comma seperated +list. The special string "all" may be used with any of these options +to match all values of the criterion. + +status + %s +severity + %s +assigned + free form, with the string '-' being a shortcut for yourself. +target + free form + +In addition, there are some shortcut options that set boolean flags. +The boolean options are ignored if the matching string option is used. +""" % (','.join(bug.status_values), + ','.join(bug.severity_values)) + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + if option == "status": + raise cmdutil.GetCompletions(bug.status_values) + elif option == "severity": + raise cmdutil.GetCompletions(bug.severity_values) + raise cmdutil.GetCompletions() + if "--complete" in args: + raise cmdutil.GetCompletions() # no positional arguments for list diff --git a/interfaces/email/interactive/becommands/merge.py b/interfaces/email/interactive/becommands/merge.py new file mode 100644 index 0000000..f212b01 --- /dev/null +++ b/interfaces/email/interactive/becommands/merge.py @@ -0,0 +1,165 @@ +# 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. +"""Merge duplicate bugs""" +from libbe import cmdutil, bugdir +import os, copy +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import utility + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> a = bd.bug_from_shortname("a") + >>> a.comment_root.time = 0 + >>> dummy = a.new_comment("Testing") + >>> dummy.time = 1 + >>> dummy = dummy.new_reply("Testing...") + >>> dummy.time = 2 + >>> b = bd.bug_from_shortname("b") + >>> b.status = "open" + >>> b.comment_root.time = 0 + >>> dummy = b.new_comment("1 2") + >>> dummy.time = 1 + >>> dummy = dummy.new_reply("1 2 3 4") + >>> dummy.time = 2 + >>> os.chdir(bd.root) + >>> execute(["a", "b"], manipulate_encodings=False) + Merging bugs a and b + >>> bd._clear_bugs() + >>> a = bd.bug_from_shortname("a") + >>> a.load_comments() + >>> mergeA = a.comment_from_shortname(":3") + >>> mergeA.time = 3 + >>> print a.string(show_comments=True) # doctest: +ELLIPSIS + ID : a + Short name : a + Severity : minor + Status : open + Assigned : + Target : + Reporter : + Creator : John Doe + Created : ... + Bug A + --------- Comment --------- + Name: a:1 + From: ... + Date: ... + + Testing + --------- Comment --------- + Name: a:2 + From: ... + Date: ... + + Testing... + --------- Comment --------- + Name: a:3 + From: ... + Date: ... + + Merged from bug b + --------- Comment --------- + Name: a:4 + From: ... + Date: ... + + 1 2 + --------- Comment --------- + Name: a:5 + From: ... + Date: ... + + 1 2 3 4 + >>> b = bd.bug_from_shortname("b") + >>> b.load_comments() + >>> mergeB = b.comment_from_shortname(":3") + >>> mergeB.time = 3 + >>> print b.string(show_comments=True) # doctest: +ELLIPSIS + ID : b + Short name : b + Severity : minor + Status : closed + Assigned : + Target : + Reporter : + Creator : Jane Doe + Created : ... + Bug B + --------- Comment --------- + Name: b:1 + From: ... + Date: ... + + 1 2 + --------- Comment --------- + Name: b:2 + From: ... + Date: ... + + 1 2 3 4 + --------- Comment --------- + Name: b:3 + From: ... + Date: ... + + Merged into bug a + >>> print b.status + closed + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True, + 1: lambda bug : bug.active==True}) + + if len(args) < 2: + raise cmdutil.UsageError("Please specify two bug ids.") + if len(args) > 2: + help() + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bugA = cmdutil.bug_from_shortname(bd, args[0]) + bugA.load_comments() + bugB = cmdutil.bug_from_shortname(bd, args[1]) + bugB.load_comments() + mergeA = bugA.new_comment("Merged from bug %s" % bugB.uuid) + newCommTree = copy.deepcopy(bugB.comment_root) + for comment in newCommTree.traverse(): # all descendant comments + comment.bug = bugA + 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" + print "Merging bugs %s and %s" % (bugA.uuid, bugB.uuid) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be merge BUG-ID BUG-ID") + return parser + +longhelp=""" +The second bug (B) is merged into the first (A). This adds merge +comments to both bugs, closes B, and appends B's comment tree to A's +merge comment. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/new.py b/interfaces/email/interactive/becommands/new.py new file mode 100644 index 0000000..a8ee2ec --- /dev/null +++ b/interfaces/email/interactive/becommands/new.py @@ -0,0 +1,80 @@ +# 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. +"""Create a new bug""" +from libbe import cmdutil, bugdir +import sys +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os, time + >>> from libbe import bug + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> bug.uuid_gen = lambda: "X" + >>> execute (["this is a test",], manipulate_encodings=False) + Created bug with ID X + >>> bd._clear_bugs() + >>> bug = bd.bug_from_uuid("X") + >>> print bug.summary + this is a test + >>> bug.time <= int(time.time()) + True + >>> print bug.severity + minor + >>> bug.target == None + True + >>> bd.cleanup() + """ + 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 summary message") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + 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 != None: + bug.assigned = bd.default_assignee + print "Created bug with ID %s" % bd.bug_shortname(bug) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be new SUMMARY") + parser.add_option("-r", "--reporter", metavar="REPORTER", dest="reporter", + help="The user who reported the bug", default=None) + parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", + help="The developer in charge of the bug", default=None) + return parser + +longhelp=""" +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(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/open.py b/interfaces/email/interactive/becommands/open.py new file mode 100644 index 0000000..0c6bf05 --- /dev/null +++ b/interfaces/email/interactive/becommands/open.py @@ -0,0 +1,58 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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 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. +"""Re-open a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("b").status + closed + >>> execute(["b"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("b").status + open + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==False}) + if len(args) == 0: + raise cmdutil.UsageError, "Please specify a bug id." + if len(args) > 1: + raise cmdutil.UsageError, "Too many arguments." + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + bug.status = "open" + +def get_parser(): + parser = cmdutil.CmdOptionParser("be open BUG-ID") + return parser + +longhelp=""" +Mark a bug as 'open'. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/remove.py b/interfaces/email/interactive/becommands/remove.py new file mode 100644 index 0000000..8d85033 --- /dev/null +++ b/interfaces/email/interactive/becommands/remove.py @@ -0,0 +1,62 @@ +# 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. +"""Remove (delete) a bug and its comments""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import mapfile + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("b").status + closed + >>> execute (["b"], manipulate_encodings=False) + Removed bug b + >>> bd._clear_bugs() + >>> try: + ... bd.bug_from_shortname("b") + ... except bugdir.NoBugMatches: + ... print "Bug not found" + Bug not found + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + if len(args) != 1: + raise cmdutil.UsageError, "Please specify a bug id." + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + bd.remove_bug(bug) + print "Removed bug %s" % bug.uuid + +def get_parser(): + parser = cmdutil.CmdOptionParser("be remove BUG-ID") + return parser + +longhelp=""" +Remove (delete) an existing bug. Use with caution: if you're not using a +revision control system, there may be no way to recover the lost information. +You should use this command, for example, to get rid of blank or otherwise +mangled bugs. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/set.py b/interfaces/email/interactive/becommands/set.py new file mode 100644 index 0000000..f7e68d3 --- /dev/null +++ b/interfaces/email/interactive/becommands/set.py @@ -0,0 +1,130 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Chris Ball +# 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 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. +"""Change tree settings""" +import textwrap +from libbe import cmdutil, bugdir, vcs, 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 not in [None, settings_object.EMPTY]: + val = "None (%s)" % default + else: + val = None + return str(val) + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["target"], manipulate_encodings=False) + None + >>> execute(["target", "tomorrow"], manipulate_encodings=False) + >>> execute(["target"], manipulate_encodings=False) + tomorrow + >>> execute(["target", "none"], manipulate_encodings=False) + >>> execute(["target"], manipulate_encodings=False) + None + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) > 2: + raise cmdutil.UsageError, "Too many arguments" + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if len(args) == 0: + keys = bd.settings_properties + keys.sort() + for key in keys: + print "%16s: %s" % (key, _value_string(bd, key)) + elif len(args) == 1: + print _value_string(bd, args[0]) + else: + 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 ' + msg += '\n '.join(bd.settings_properties) + raise cmdutil.UserError(msg) + old_setting = bd.settings.get(args[0]) + setattr(bd, args[0], args[1]) + +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 == "vcs_name": + lines = dstr.split('\n') + while lines[0].startswith("This property defaults to") == False: + lines.pop(0) + assert len(lines) != None, \ + "Unexpected vcs_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. + +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. + +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 + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + if pos == 0: # first positional argument is a setting name + props = bugdir.BugDir.settings_properties + raise cmdutil.GetCompletions(props) + raise cmdutil.GetCompletions() # no positional arguments for list diff --git a/interfaces/email/interactive/becommands/severity.py b/interfaces/email/interactive/becommands/severity.py new file mode 100644 index 0000000..660586e --- /dev/null +++ b/interfaces/email/interactive/becommands/severity.py @@ -0,0 +1,103 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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 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. +"""Show or change a bug's severity level""" +from libbe import cmdutil, bugdir, bug +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + minor + >>> execute(["a", "wishlist"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + wishlist + >>> execute(["a", "none"], manipulate_encodings=False) + Traceback (most recent call last): + UserError: Invalid severity level: none + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) not in (1,2): + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + if len(args) == 1: + print bug.severity + elif len(args) == 2: + try: + bug.severity = args[1] + except ValueError, e: + if e.name != "severity": + raise e + raise cmdutil.UserError ("Invalid severity level: %s" % e.value) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be severity BUG-ID [SEVERITY]") + return parser + +def help(): + longhelp=[""" +Show or change a bug's severity level. + +If no severity is specified, the current value is printed. If a severity level +is specified, it will be assigned to the bug. + +Severity levels are: +"""] + try: # See if there are any per-tree severity configurations + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False) + except bugdir.NoBugDir, e: + pass # No tree, just show the defaults + longest_severity_len = max([len(s) for s in bug.severity_values]) + for severity in bug.severity_values : + description = bug.severity_description[severity] + s = "%*s : %s\n" % (longest_severity_len, severity, description) + longhelp.append(s) + longhelp = ''.join(longhelp) + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + try: # See if there are any per-tree severity configurations + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + except bugdir.NoBugDir: + bd = None + if pos == 0: # fist positional argument is a bug id + ids = [] + if bd != None: + bd.load_all_bugs() + filter = lambda bg : bg.active==True + bugs = [bg for bg in bd if filter(bg)==True] + ids = [bd.bug_shortname(bg) for bg in bugs] + raise cmdutil.GetCompletions(ids) + elif pos == 1: # second positional argument is a severity + raise cmdutil.GetCompletions(bug.severity_values) + raise cmdutil.GetCompletions() diff --git a/interfaces/email/interactive/becommands/show.py b/interfaces/email/interactive/becommands/show.py new file mode 100644 index 0000000..50bd6eb --- /dev/null +++ b/interfaces/email/interactive/becommands/show.py @@ -0,0 +1,116 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Chris Ball +# 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 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. +"""Show a particular bug""" +import sys +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute (["a",], manipulate_encodings=False) # doctest: +ELLIPSIS + ID : a + Short name : a + Severity : minor + Status : open + Assigned : + Target : + Reporter : + Creator : John Doe + Created : ... + Bug A + + >>> execute (["--xml", "a"], manipulate_encodings=False) # doctest: +ELLIPSIS + + + a + a + minor + open + John Doe <jdoe@example.com> + ... + Bug A + + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={-1: lambda bug : bug.active==True}) + if len(args) == 0: + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + 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: + bugname = shortname + is_comment = False + if is_comment == True and options.comments == False: + continue + bug = cmdutil.bug_from_shortname(bd, 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] 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 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(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/status.py b/interfaces/email/interactive/becommands/status.py new file mode 100644 index 0000000..f315003 --- /dev/null +++ b/interfaces/email/interactive/becommands/status.py @@ -0,0 +1,115 @@ +# 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. +"""Show or change a bug's status""" +from libbe import cmdutil, bugdir, bug +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + open + >>> execute(["a", "closed"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + closed + >>> execute(["a", "none"], manipulate_encodings=False) + Traceback (most recent call last): + UserError: Invalid status: none + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) not in (1,2): + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + if len(args) == 1: + print bug.status + else: + try: + bug.status = args[1] + except ValueError, e: + if e.name != "status": + raise + raise cmdutil.UserError ("Invalid status: %s" % e.value) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be status BUG-ID [STATUS]") + return parser + + +def help(): + 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]) + 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" % (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): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + try: # See if there are any per-tree status configurations + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + except bugdir.NoBugDir: + bd = None + if pos == 0: # fist positional argument is a bug id + ids = [] + if bd != None: + bd.load_all_bugs() + ids = [bd.bug_shortname(bg) for bg in bd] + raise cmdutil.GetCompletions(ids) + elif pos == 1: # second positional argument is a status + raise cmdutil.GetCompletions(bug.status_values) + raise cmdutil.GetCompletions() diff --git a/interfaces/email/interactive/becommands/subscribe.py b/interfaces/email/interactive/becommands/subscribe.py new file mode 100644 index 0000000..0a23057 --- /dev/null +++ b/interfaces/email/interactive/becommands/subscribe.py @@ -0,0 +1,390 @@ +# 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. +"""(Un)subscribe to change notification""" +from libbe import cmdutil, bugdir, tree +import os, copy +__desc__ = __doc__ + +TAG="SUBSCRIBE:" + +class SubscriptionType (tree.Tree): + """ + Trees of subscription types to allow users to select exactly what + notifications they want to subscribe to. + """ + def __init__(self, type_name, *args, **kwargs): + tree.Tree.__init__(self, *args, **kwargs) + self.type = type_name + def __str__(self): + return self.type + def __repr__(self): + return "" % str(self) + def string_tree(self, indent=0): + lines = [] + for depth,node in self.thread(): + lines.append("%s%s" % (" "*(indent+2*depth), node)) + return "\n".join(lines) + +BUGDIR_TYPE_NEW = SubscriptionType("new") +BUGDIR_TYPE_ALL = SubscriptionType("all", [BUGDIR_TYPE_NEW]) + +# same name as BUGDIR_TYPE_ALL for consistency +BUG_TYPE_ALL = SubscriptionType(str(BUGDIR_TYPE_ALL)) + +INVALID_TYPE = SubscriptionType("INVALID") + +class InvalidType (ValueError): + def __init__(self, type_name, type_root): + msg = "Invalid type %s for tree:\n%s" \ + % (type_name, type_root.string_tree(4)) + ValueError.__init__(self, msg) + self.type_name = type_name + self.type_root = type_root + + +def execute(args, manipulate_encodings=True): + """ + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> os.chdir(bd.root) + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + [] + >>> execute(["-s","John Doe ", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + John Doe all * + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + ['SUBSCRIBE:John Doe \\tall\\t*'] + >>> execute(["-s","Jane Doe ", "-S", "a.com,b.net", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all a.com,b.net + John Doe all * + >>> execute(["-s","Jane Doe ", "-S", "a.edu", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all a.com,a.edu,b.net + John Doe all * + >>> execute(["-u", "-s","Jane Doe ", "-S", "a.com", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all a.edu,b.net + John Doe all * + >>> execute(["-s","Jane Doe ", "-S", "*", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all * + John Doe all * + >>> execute(["-u", "-s","Jane Doe ", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + John Doe all * + >>> execute(["-u", "-s","John Doe ", "a"], manipulate_encodings=False) + >>> execute(["-s","Jane Doe ", "-t", "new", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for bug directory: + Jane Doe new * + >>> execute(["-s","Jane Doe ", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for bug directory: + Jane Doe all * + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if len(args) > 1: + help() + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + + subscriber = options.subscriber + if subscriber == None: + subscriber = bd.user_id + if options.unsubscribe == True: + if options.servers == None: + options.servers = "INVALID" + if options.types == None: + options.types = "INVALID" + else: + if options.servers == None: + options.servers = "*" + if options.types == None: + options.types = "all" + servers = options.servers.split(",") + types = options.types.split(",") + + if len(args) == 0 or args[0] == "DIR": # directory-wide subscriptions + type_root = BUGDIR_TYPE_ALL + entity = bd + entity_name = "bug directory" + else: # bug-specific subscriptions + type_root = BUG_TYPE_ALL + bug = bd.bug_from_shortname(args[0]) + entity = bug + entity_name = bug.uuid + if options.list_all == True: + entity_name = "anything in the bug directory" + + types = [type_from_name(name, type_root, default=INVALID_TYPE, + default_ok=options.unsubscribe) + for name in types] + estrs = entity.extra_strings + if options.list == True or options.list_all == True: + pass + else: # alter subscriptions + if options.unsubscribe == True: + estrs = unsubscribe(estrs, subscriber, types, servers, type_root) + else: # add the tag + estrs = subscribe(estrs, subscriber, types, servers, type_root) + entity.extra_strings = estrs # reassign to notice change + + if options.list_all == True: + bd.load_all_bugs() + subscriptions = get_bugdir_subscribers(bd, servers[0]) + else: + subscriptions = [] + for estr in entity.extra_strings: + if estr.startswith(TAG): + subscriptions.append(estr[len(TAG):]) + + if len(subscriptions) > 0: + print "Subscriptions for %s:" % entity_name + print '\n'.join(subscriptions) + + +def get_parser(): + parser = cmdutil.CmdOptionParser("be subscribe ID") + parser.add_option("-u", "--unsubscribe", action="store_true", + dest="unsubscribe", default=False, + help="Unsubscribe instead of subscribing.") + parser.add_option("-a", "--list-all", action="store_true", + dest="list_all", default=False, + help="List all subscribers (no ID argument, read only action).") + parser.add_option("-l", "--list", action="store_true", + dest="list", default=False, + help="List subscribers (read only action).") + parser.add_option("-s", "--subscriber", dest="subscriber", + metavar="SUBSCRIBER", + help="Email address of the subscriber (defaults to bugdir.user_id).") + parser.add_option("-S", "--servers", dest="servers", metavar="SERVERS", + help="Servers from which you want notification.") + parser.add_option("-t", "--type", dest="types", metavar="TYPES", + help="Types of changes you wish to be notified about.") + return parser + +longhelp=""" +ID can be either a bug id, or blank/"DIR", in which case it refers to the +whole bug directory. + +SERVERS specifies the servers from which you would like to receive +notification. Multiple severs may be specified in a comma-separated +list, or you can use "*" to match all servers (the default). If you +have not selected a server, it should politely refrain from notifying +you of changes, although there is no way to guarantee this behavior. + +Available TYPES: + For bugs: +%s + For DIR : +%s + +For unsubscription, any listed SERVERS and TYPES are removed from your +subscription. Either the catch-all server "*" or type "%s" will +remove SUBSCRIBER entirely from the specified ID. + +This command is intended for use primarily by public interfaces, since +if you're just hacking away on your private repository, you'll known +what's changed ;). This command just (un)sets the appropriate +subscriptions, and leaves it up to each interface to perform the +notification. +""" % (BUG_TYPE_ALL.string_tree(6), BUGDIR_TYPE_ALL.string_tree(6), + BUGDIR_TYPE_ALL) + +def help(): + return get_parser().help_str() + longhelp + +# internal helper functions + +def _generate_string(subscriber, types, servers): + types = sorted([str(t) for t in types]) + servers = sorted(servers) + return "%s%s\t%s\t%s" % (TAG,subscriber,",".join(types),",".join(servers)) + +def _parse_string(string, type_root): + assert string.startswith(TAG), string + string = string[len(TAG):] + subscriber,types,servers = string.split("\t") + types = [type_from_name(name, type_root) for name in types.split(",")] + return (subscriber,types,servers.split(",")) + +def _get_subscriber(extra_strings, subscriber, type_root): + for i,string in enumerate(extra_strings): + if string.startswith(TAG): + s,ts,srvs = _parse_string(string, type_root) + if s == subscriber: + return i,s,ts,srvs # match! + return None # no match + +# functions exposed to other modules + +def type_from_name(name, type_root, default=None, default_ok=False): + if name == str(type_root): + return type_root + for t in type_root.traverse(): + if name == str(t): + return t + if default_ok: + return default + raise InvalidType(name, type_root) + +def subscribe(extra_strings, subscriber, types, servers, type_root): + args = _get_subscriber(extra_strings, subscriber, type_root) + if args == None: # no match + extra_strings.append(_generate_string(subscriber, types, servers)) + return extra_strings + # Alter matched string + i,s,ts,srvs = args + for t in types: + if t not in ts: + ts.append(t) + # remove descendant types + all_ts = copy.copy(ts) + for t in all_ts: + for tt in all_ts: + if tt in ts and t.has_descendant(tt): + ts.remove(tt) + if "*" in servers+srvs: + srvs = ["*"] + else: + srvs = list(set(servers+srvs)) + extra_strings[i] = _generate_string(subscriber, ts, srvs) + return extra_strings + +def unsubscribe(extra_strings, subscriber, types, servers, type_root): + args = _get_subscriber(extra_strings, subscriber, type_root) + if args == None: # no match + return extra_strings # pass + # Remove matched string + i,s,ts,srvs = args + all_ts = copy.copy(ts) + for t in types: + for tt in all_ts: + if tt in ts and t.has_descendant(tt): + ts.remove(tt) + if "*" in servers+srvs: + srvs = [] + else: + for srv in servers: + if srv in srvs: + srvs.remove(srv) + if len(ts) == 0 or len(srvs) == 0: + extra_strings.pop(i) + else: + extra_strings[i] = _generate_string(subscriber, ts, srvs) + return extra_strings + +def get_subscribers(extra_strings, type, server, type_root, + match_ancestor_types=False, + match_descendant_types=False): + """ + Set match_ancestor_types=True if you want to find eveyone who + cares about your particular type. + + Set match_descendant_types=True if you want to find subscribers + who may only care about some subset of your type. This is useful + for generating lists of all the subscribers in a given set of + extra_strings. + + >>> def sgs(*args, **kwargs): + ... return sorted(get_subscribers(*args, **kwargs)) + >>> es = [] + >>> es = subscribe(es, "John Doe ", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL) + >>> es = subscribe(es, "Jane Doe ", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL) + >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL) + ['John Doe '] + >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL, match_descendant_types=True) + ['Jane Doe ', 'John Doe '] + >>> sgs(es, BUGDIR_TYPE_ALL, "b.net", BUGDIR_TYPE_ALL, match_descendant_types=True) + ['Jane Doe '] + >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL) + ['Jane Doe '] + >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL, match_ancestor_types=True) + ['Jane Doe ', 'John Doe '] + """ + for string in extra_strings: + if not string.startswith(TAG): + continue + subscriber,types,servers = _parse_string(string, type_root) + type_match = False + if type in types: + type_match = True + if type_match == False and match_ancestor_types == True: + for t in types: + if t.has_descendant(type): + type_match = True + break + if type_match == False and match_descendant_types == True: + for t in types: + if type.has_descendant(t): + type_match = True + break + server_match = False + if server in servers or servers == ["*"] or server == "*": + server_match = True + if type_match == True and server_match == True: + yield subscriber + +def get_bugdir_subscribers(bugdir, server): + """ + I have a bugdir. Who cares about it, and what do they care about? + Returns a dict of dicts: + subscribers[user][id] = types + where id is either a bug.uuid (in the case of a bug subscription) + or "DIR" (in the case of a bugdir subscription). + + Only checks bugs that are currently in memory, so you might want + to call bugdir.load_all_bugs() first. + + >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) + >>> a = bd.bug_from_shortname("a") + >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe ", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL) + >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe ", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL) + >>> a.extra_strings = subscribe(a.extra_strings, "John Doe ", [BUG_TYPE_ALL], ["a.com"], BUG_TYPE_ALL) + >>> subscribers = get_bugdir_subscribers(bd, "a.com") + >>> subscribers["Jane Doe "]["DIR"] + [] + >>> subscribers["John Doe "]["DIR"] + [] + >>> subscribers["John Doe "]["a"] + [] + >>> get_bugdir_subscribers(bd, "b.net") + {'Jane Doe ': {'DIR': []}} + >>> bd.cleanup() + """ + subscribers = {} + for sub in get_subscribers(bugdir.extra_strings, BUGDIR_TYPE_ALL, server, + BUGDIR_TYPE_ALL, match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bugdir.extra_strings,sub,BUGDIR_TYPE_ALL) + subscribers[sub] = {"DIR":ts} + for bug in bugdir: + for sub in get_subscribers(bug.extra_strings, BUG_TYPE_ALL, server, + BUG_TYPE_ALL, match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bug.extra_strings,sub,BUG_TYPE_ALL) + if sub in subscribers: + subscribers[sub][bug.uuid] = ts + else: + subscribers[sub] = {bug.uuid:ts} + return subscribers diff --git a/interfaces/email/interactive/becommands/tag.py b/interfaces/email/interactive/becommands/tag.py new file mode 100644 index 0000000..ecd853f --- /dev/null +++ b/interfaces/email/interactive/becommands/tag.py @@ -0,0 +1,134 @@ +# 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. +"""Tag a bug, or search bugs for tags""" +from libbe import cmdutil, bugdir +import os, copy +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import utility + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> os.chdir(bd.root) + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + [] + >>> execute(["a", "GUI"], manipulate_encodings=False) + Tags for a: + GUI + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + ['TAG:GUI'] + >>> execute(["a", "later"], manipulate_encodings=False) + Tags for a: + GUI + later + >>> execute(["a"], manipulate_encodings=False) + Tags for a: + GUI + later + >>> execute(["--list"], manipulate_encodings=False) + GUI + later + >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) + Tags for a: + Alphabetically first + GUI + later + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + ['TAG:Alphabetically first', 'TAG:GUI', 'TAG:later'] + >>> a.extra_strings = [] + >>> print a.extra_strings + [] + >>> execute(["a"], manipulate_encodings=False) + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + [] + >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) + Tags for a: + Alphabetically first + >>> execute(["--remove", "a", "Alphabetically first"], manipulate_encodings=False) + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if len(args) == 0 and options.list == False: + raise cmdutil.UsageError("Please specify a bug id.") + elif len(args) > 2 or (len(args) > 0 and options.list == True): + help() + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if options.list: + bd.load_all_bugs() + tags = [] + for bug in bd: + for estr in bug.extra_strings: + if estr.startswith("TAG:"): + tag = estr[4:] + if tag not in tags: + tags.append(tag) + tags.sort() + if len(tags) > 0: + print '\n'.join(tags) + return + bug = cmdutil.bug_from_shortname(bd, args[0]) + if len(args) == 2: + given_tag = args[1] + estrs = bug.extra_strings + tag_string = "TAG:%s" % given_tag + if options.remove == True: + estrs.remove(tag_string) + else: # add the tag + estrs.append(tag_string) + bug.extra_strings = estrs # reassign to notice change + + tags = [] + for estr in bug.extra_strings: + if estr.startswith("TAG:"): + tags.append(estr[4:]) + + if len(tags) > 0: + print "Tags for %s:" % bug.uuid + print '\n'.join(tags) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be tag BUG-ID [TAG]\nor: be tag --list") + parser.add_option("-r", "--remove", action="store_true", dest="remove", + help="Remove TAG (instead of adding it)") + parser.add_option("-l", "--list", action="store_true", dest="list", + help="List all available tags and exit") + return parser + +longhelp=""" +If TAG is given, add TAG to BUG-ID. If it is not specified, just +print the tags for BUG-ID. + +To search for bugs with a particular tag, try + $ be list --extra-strings TAG: +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/target.py b/interfaces/email/interactive/becommands/target.py new file mode 100644 index 0000000..7e41451 --- /dev/null +++ b/interfaces/email/interactive/becommands/target.py @@ -0,0 +1,95 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Chris Ball +# Gianluca Montecchi +# 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 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. +"""Show or change a bug's target for fixing""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + No target assigned. + >>> execute(["a", "tomorrow"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + tomorrow + >>> execute(["--list"], manipulate_encodings=False) + tomorrow + >>> execute(["a", "none"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + No target assigned. + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if len(args) not in (1, 2): + if not (options.list == True and len(args) == 0): + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if options.list: + ts = set([bd.bug_from_uuid(bug).target for bug in bd.list_uuids()]) + for target in sorted(ts): + if target and isinstance(target,str): + print target + return + bug = cmdutil.bug_from_shortname(bd, args[0]) + if len(args) == 1: + if bug.target is None: + print "No target assigned." + else: + print bug.target + else: + assert len(args) == 2 + if args[1] == "none": + bug.target = None + else: + bug.target = args[1] + +def get_parser(): + parser = cmdutil.CmdOptionParser("be target BUG-ID [TARGET]\nor: be target --list") + parser.add_option("-l", "--list", action="store_true", dest="list", + help="List all available targets and exit") + return parser + +longhelp=""" +Show or change a bug's target for fixing. + +If no target is specified, the current value is printed. If a target +is specified, it will be assigned to the bug. + +Targets are freeform; any text may be specified. They will generally be +milestone names or release numbers. + +The value "none" can be used to unset the target. + +In the alternative `be target --list` form print a list of all +currently specified targets. Note that bug status +(i.e. opened/closed) is ignored. If you want to list all bugs +matching a current target, see `be list --target TARGET'. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/examples/comment b/interfaces/email/interactive/examples/comment new file mode 100644 index 0000000..f22e4b2 --- /dev/null +++ b/interfaces/email/interactive/examples/comment @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug:a1d] Subject ignored + +We sure do. +-- +Goofy tagline ignored diff --git a/interfaces/email/interactive/examples/failing_multiples b/interfaces/email/interactive/examples/failing_multiples new file mode 100644 index 0000000..cf50211 --- /dev/null +++ b/interfaces/email/interactive/examples/failing_multiples @@ -0,0 +1,16 @@ +From jdoe@example.com Fri Apr 18 12:00:00 2008 +Message-ID: +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Commit message... + +new "test bug" +new "test bug 2" +failing-command +new "test bug 3" + +-- +This message fails partway through, but the partial changes should be +recorded in a commit... diff --git a/interfaces/email/interactive/examples/invalid_command b/interfaces/email/interactive/examples/invalid_command new file mode 100644 index 0000000..f2963c7 --- /dev/null +++ b/interfaces/email/interactive/examples/invalid_command @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] + +close +-- +Close is currently disabled for the email interface. diff --git a/interfaces/email/interactive/examples/invalid_subject b/interfaces/email/interactive/examples/invalid_subject new file mode 100644 index 0000000..1e2eb88 --- /dev/null +++ b/interfaces/email/interactive/examples/invalid_subject @@ -0,0 +1,9 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: Spam! + +This should elicit an "invalid subject" response email. diff --git a/interfaces/email/interactive/examples/list b/interfaces/email/interactive/examples/list new file mode 100644 index 0000000..acba424 --- /dev/null +++ b/interfaces/email/interactive/examples/list @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Subject ignored + +list --status all +-- +Dummy content diff --git a/interfaces/email/interactive/examples/missing_command b/interfaces/email/interactive/examples/missing_command new file mode 100644 index 0000000..bb390fc --- /dev/null +++ b/interfaces/email/interactive/examples/missing_command @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Subject ignored + +abcde +-- +This should elicit a "invalid command 'abcde'" response email. diff --git a/interfaces/email/interactive/examples/multiple_commands b/interfaces/email/interactive/examples/multiple_commands new file mode 100644 index 0000000..41ef730 --- /dev/null +++ b/interfaces/email/interactive/examples/multiple_commands @@ -0,0 +1,14 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Subject ignored + +help +list --status=all +list --status=fixed +show --xml 361 +-- +Goofy tagline ignored. diff --git a/interfaces/email/interactive/examples/new b/interfaces/email/interactive/examples/new new file mode 100644 index 0000000..c64db93 --- /dev/null +++ b/interfaces/email/interactive/examples/new @@ -0,0 +1,19 @@ +From jdoe@example.com Fri Apr 18 12:00:00 2008 +Message-ID: +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug:submit] Need tests for the email interface. + +Version: XYZ +Reporter: Jane Doe +Assign: Dick Tracy +Depend: 00f +Severity: critical +Status: assigned +Tag: topsecret +Target: Law&Order + +-- +Goofy tagline not included, and no comment added. diff --git a/interfaces/email/interactive/examples/new_with_comment b/interfaces/email/interactive/examples/new_with_comment new file mode 100644 index 0000000..1077f0f --- /dev/null +++ b/interfaces/email/interactive/examples/new_with_comment @@ -0,0 +1,13 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug:submit] Need tests for the email interface. + +Version: XYZ + +I think so anyway. +-- +Goofy tagline not included. diff --git a/interfaces/email/interactive/examples/show b/interfaces/email/interactive/examples/show new file mode 100644 index 0000000..c5f8a4d --- /dev/null +++ b/interfaces/email/interactive/examples/show @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Subject ignored + +show --xml 361 +-- +Can we show a bug? diff --git a/interfaces/email/interactive/examples/unicode b/interfaces/email/interactive/examples/unicode new file mode 100644 index 0000000..f0e8001 --- /dev/null +++ b/interfaces/email/interactive/examples/unicode @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Subject ignored + +show --xml f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a +-- +Can we handle unicode output? diff --git a/interfaces/email/interactive/libbe/arch.py b/interfaces/email/interactive/libbe/arch.py new file mode 100644 index 0000000..ab55172 --- /dev/null +++ b/interfaces/email/interactive/libbe/arch.py @@ -0,0 +1,312 @@ +# 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. + +""" +GNU Arch (tla) backend. +""" + +import codecs +import os +import re +import shutil +import sys +import time +import unittest +import doctest + +from beuuid import uuid_gen +import config +import vcs + + + +DEFAULT_CLIENT = "tla" + +client = config.get_val("arch_client", default=DEFAULT_CLIENT) + +def new(): + return Arch() + +class Arch(vcs.VCS): + 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 _vcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _vcs_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 _vcs_init(self, path): + self._create_archive(path) + self._create_project(path) + self._add_project_code(path) + def _create_archive(self, path): + """ + Create a temporary Arch archive in the directory PATH. This + archive will be removed by + __del__->cleanup->_vcs_cleanup->_remove_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->_vcs_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 _vcs_cleanup(self): + if self._tmp_project == True: + self._remove_project() + if self._tmp_archive == True: + self._remove_archive() + + def _vcs_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 _vcs_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 _vcs_set_user_id(self, value): + self._u_invoke_client('my-id', value) + def _vcs_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 _vcs_remove(self, path): + if not '.arch-ids' in path: + self._u_invoke_client("delete-id", path) + def _vcs_update(self, path): + pass + def _vcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return vcs.VCS._vcs_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 _vcs_duplicate_repo(self, directory, revision=None): + if revision == None: + vcs.VCS._vcs_duplicate_repo(self, directory, revision) + else: + status,output,error = \ + self._u_invoke_client("get", revision,directory) + def _vcs_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 vcs.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 + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("logs") + logs = output.splitlines() + first_log = logs.pop(0) + assert first_log == "base-0", first_log + try: + log = logs[index] + except IndexError: + return None + return "%s--%s" % (self._archive_project_name(), log) + +class CantAddFile(Exception): + def __init__(self, file): + self.file = file + Exception.__init__(self, "Can't automatically add file %s" % file) + + + +vcs.make_vcs_testcase_subclasses(Arch, sys.modules[__name__]) + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/email/interactive/libbe/beuuid.py b/interfaces/email/interactive/libbe/beuuid.py new file mode 100644 index 0000000..490ed62 --- /dev/null +++ b/interfaces/email/interactive/libbe/beuuid.py @@ -0,0 +1,63 @@ +# 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/email/interactive/libbe/bug.py b/interfaces/email/interactive/libbe/bug.py new file mode 100644 index 0000000..fd30ff7 --- /dev/null +++ b/interfaces/email/interactive/libbe/bug.py @@ -0,0 +1,580 @@ +# 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. + +""" +Define the Bug class for representing bugs. +""" + +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 + + +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) + +### 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_vcs(self): + if hasattr(self.bugdir, "vcs"): + return self.bugdir.vcs + + @Property + @cached_property(generator=_get_vcs) + @local_property("vcs") + @doc_property(doc="A revision control system instance.") + def vcs(): 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.vcs != None: + self.creator = self.vcs.get_user_id() + self.summary = summary + + def __repr__(self): + return "Bug(uuid=%r)" % self.uuid + + def __str__(self): + return self.string(shortlist=True) + + def __cmp__(self, other): + return cmp_full(self, other) + + # serializing methods + + 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 + + # methods for saving/loading/acessing settings and properties. + + def get_path(self, *args): + dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid) + if len(args) == 0: + return dir + assert args[0] in ["values", "comments"], str(args) + return os.path.join(dir, *args) + + def set_sync_with_disk(self, value): + self.sync_with_disk = value + for comment in self.comments(): + comment.set_sync_with_disk(value) + + def load_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("load settings") + self.settings = mapfile.map_load(self.vcs, self.get_path("values")) + self._setup_saved_settings() + + def save_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("save settings") + assert self.summary != None, "Can't save blank bug" + self.vcs.mkdir(self.get_path()) + path = self.get_path("values") + mapfile.map_save(self.vcs, 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). + """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) + self.save_settings() + if len(self.comment_root) > 0: + comment.saveComments(self) + if sync_with_disk == False: + self.set_sync_with_disk(False) + + def load_comments(self, load_full=True): + if self.sync_with_disk == False: + raise DiskAccessRequired("load comments") + 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 remove(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("remove") + self.comment_root.remove() + path = self.get_path() + self.vcs.recursive_remove(path) + + # methods for managing comments + + 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_uuid = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "uuid") +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") +cmp_target = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "target") +cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter") +cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary") +# 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_reporter, cmp_target, cmp_comments, cmp_summary, cmp_uuid) + +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/email/interactive/libbe/bugdir.py b/interfaces/email/interactive/libbe/bugdir.py new file mode 100644 index 0000000..c4f0f91 --- /dev/null +++ b/interfaces/email/interactive/libbe/bugdir.py @@ -0,0 +1,832 @@ +# 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. + +""" +Define the BugDir class for representing bug comments. +""" + +import copy +import errno +import os +import os.path +import sys +import time +import unittest +import doctest + +import bug +import encoding +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 mapfile +import vcs +import settings_object +import upgrade +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 + +class NoBugMatches(KeyError): + def __init__(self, shortname): + msg = "No bug matches %s" % shortname + KeyError.__init__(self, msg) + self.shortname = shortname + +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) + + +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 memory, a call to save() immediately before the + .set_sync_with_disk(True) call 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 settings/bugs/comments that it doesn't already have in + memory and .sync_with_disk == True. + + Allow VCS initialization + ======================== + + This one is for testing purposes. Setting it to True allows the + BugDir to search for an installed VCS 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.vcs.user_id = user_id + def _guess_user_id(self): + return self.vcs.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 VCS 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="vcs_name", + doc="""The name of the current VCS. Kept seperate to make saving/loading +settings easy. Don't set this attribute. Set .vcs instead, and +.vcs_name will be automatically adjusted.""", + default="None", + allowed=["None", "Arch", "bzr", "darcs", "git", "hg"]) + def vcs_name(): return {} + + def _get_vcs(self, vcs_name=None): + """Get and root a new revision control system""" + if vcs_name == None: + vcs_name = self.vcs_name + new_vcs = vcs.vcs_by_name(vcs_name) + self._change_vcs(None, new_vcs) + return new_vcs + def _change_vcs(self, old_vcs, new_vcs): + new_vcs.encoding = self.encoding + new_vcs.root(self.root) + self.vcs_name = new_vcs.name + + @Property + @change_hook_property(hook=_change_vcs) + @cached_property(generator=_get_vcs) + @local_property("vcs") + @doc_property(doc="A revision control system instance.") + def vcs(): 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_vcs_init=False, + manipulate_encodings=True, from_disk=False, vcs=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 vcs until we've loaded settings + self.sync_with_disk = False + self.vcs = self._guess_vcs() + + 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 vcs == None: + vcs = self._guess_vcs(allow_vcs_init) + self.vcs = vcs + self._setup_user_id(self.user_id) + + def __del__(self): + self.cleanup() + + def cleanup(self): + self.vcs.cleanup() + + # methods for getting the BugDir situated in the filesystem + + def _find_root(self, path): + """ + Search for an existing bug database dir and it's ancestors and + return a BugDir rooted there. Only called by __init__, and + then only if sink_to_existing_root == True. + """ + 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 _guess_vcs(self, allow_vcs_init=False): + """ + Only called by __init__. + """ + deepdir = self.get_path() + if not os.path.exists(deepdir): + deepdir = os.path.dirname(deepdir) + new_vcs = vcs.detect_vcs(deepdir) + install = False + if new_vcs.name == "None": + if allow_vcs_init == True: + new_vcs = vcs.installed_vcs() + new_vcs.init(self.root) + return new_vcs + + # methods for saving/loading/accessing settings and properties. + + def get_path(self, *args): + """ + Return a path relative to .root. + """ + dir = os.path.join(self.root, ".be") + if len(args) == 0: + return dir + assert args[0] in ["version", "settings", "bugs"], str(args) + return os.path.join(dir, *args) + + def _get_settings(self, settings_path, for_duplicate_bugdir=False): + allow_no_vcs = not self.vcs.path_in_root(settings_path) + if allow_no_vcs == True: + assert for_duplicate_bugdir == True + if self.sync_with_disk == False and for_duplicate_bugdir == False: + # duplicates can ignore this bugdir's .sync_with_disk status + raise DiskAccessRequired("_get settings") + try: + settings = mapfile.map_load(self.vcs, settings_path, allow_no_vcs) + except vcs.NoSuchFile: + settings = {"vcs_name": "None"} + return settings + + def _save_settings(self, settings_path, settings, + for_duplicate_bugdir=False): + allow_no_vcs = not self.vcs.path_in_root(settings_path) + if allow_no_vcs == True: + assert for_duplicate_bugdir == True + if self.sync_with_disk == False and for_duplicate_bugdir == False: + # duplicates can ignore this bugdir's .sync_with_disk status + raise DiskAccessRequired("_save settings") + self.vcs.mkdir(self.get_path(), allow_no_vcs) + mapfile.map_save(self.vcs, settings_path, settings, allow_no_vcs) + + 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) + self.vcs = vcs.vcs_by_name(self.vcs_name) + self._setup_user_id(self.user_id) + + def save_settings(self): + settings = self._get_saved_settings() + self._save_settings(self.get_path("settings"), settings) + + def get_version(self, path=None, use_none_vcs=False, + for_duplicate_bugdir=False): + """ + Requires disk access. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("get version") + if use_none_vcs == True: + VCS = vcs.vcs_by_name("None") + VCS.root(self.root) + VCS.encoding = encoding.get_encoding() + else: + VCS = self.vcs + + if path == None: + path = self.get_path("version") + allow_no_vcs = not VCS.path_in_root(path) + if allow_no_vcs == True: + assert for_duplicate_bugdir == True + version = VCS.get_file_contents( + path, allow_no_vcs=allow_no_vcs).rstrip("\n") + return version + + def set_version(self): + """ + Requires disk access. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("set version") + self.vcs.mkdir(self.get_path()) + self.vcs.set_file_contents(self.get_path("version"), + upgrade.BUGDIR_DISK_VERSION+"\n") + + # methods controlling disk access + + def set_sync_with_disk(self, value): + """ + Adjust .sync_with_disk for the BugDir and all it's children. + See the BugDir docstring for a description of the role of + .sync_with_disk. + """ + self.sync_with_disk = value + for bug in self: + bug.set_sync_with_disk(value) + + def load(self): + """ + Reqires disk access + """ + version = self.get_version(use_none_vcs=True) + if version != upgrade.BUGDIR_DISK_VERSION: + upgrade.upgrade(self.root, version) + else: + if not os.path.exists(self.get_path()): + raise NoBugDir(self.get_path()) + self.load_settings() + + def load_all_bugs(self): + """ + Requires disk access. + Warning: this could take a while. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("load all bugs") + self._clear_bugs() + for uuid in self.list_uuids(): + self._load_bug(uuid) + + def save(self): + """ + Note that this command writes to disk _regardless_ of the + status of .sync_with_disk. + + Save any loaded contents to disk. Because of lazy loading of + bugs and comments, this is actually not too inefficient. + + However, if .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). + + Requires disk access. + """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) + self.set_version() + self.save_settings() + for bug in self: + bug.save() + if sync_with_disk == False: + self.set_sync_with_disk(sync_with_disk) + + # methods for managing duplicate BugDirs + + def duplicate_bugdir(self, revision): + duplicate_path = self.vcs.duplicate_repo(revision) + + duplicate_version_path = os.path.join(duplicate_path, ".be", "version") + try: + version = self.get_version(duplicate_version_path, + for_duplicate_bugdir=True) + except DiskAccessRequired: + self.sync_with_disk = True # temporarily allow access + version = self.get_version(duplicate_version_path, + for_duplicate_bugdir=True) + self.sync_with_disk = False + if version != upgrade.BUGDIR_DISK_VERSION: + upgrade.upgrade(duplicate_path, version) + + # setup revision VCS 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, + for_duplicate_bugdir=True) + if "vcs_name" in duplicate_settings: + duplicate_settings["vcs_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, + for_duplicate_bugdir=True) + + return BugDir(duplicate_path, from_disk=True, manipulate_encodings=self._manipulate_encodings) + + def remove_duplicate_bugdir(self): + self.vcs.remove_duplicate_repo() + + # methods for managing bugs + + def list_uuids(self): + uuids = [] + if self.sync_with_disk == True and os.path.exists(self.get_path()): + # list the uuids on disk + if os.path.exists(self.get_path("bugs")): + 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): + if self.sync_with_disk == False: + raise DiskAccessRequired("_load bug") + 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) + if bug.sync_with_disk == True: + 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 = SimpleBugDir(sync_with_disk=False) + >>> bug_a = bd.bug_from_shortname('a') + >>> print type(bug_a) + + >>> print bug_a + a:om: Bug A + >>> bd.cleanup() + """ + 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 NoBugMatches(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 + + +class SimpleBugDir (BugDir): + """ + For testing. Set sync_with_disk==False for a memory-only bugdir. + >>> bugdir = SimpleBugDir() + >>> uuids = list(bugdir.list_uuids()) + >>> uuids.sort() + >>> print uuids + ['a', 'b'] + >>> bugdir.cleanup() + """ + def __init__(self, sync_with_disk=True): + if sync_with_disk == True: + dir = utility.Dir() + assert os.path.exists(dir.path) + root = dir.path + assert_new_BugDir = True + vcs_init = True + else: + root = "/" + assert_new_BugDir = False + vcs_init = False + BugDir.__init__(self, root, sink_to_existing_root=False, + assert_new_BugDir=assert_new_BugDir, + allow_vcs_init=vcs_init, + manipulate_encodings=False) + if sync_with_disk == True: # postpone cleanup since dir.__del__() removes dir. + self._dir_ref = dir + bug_a = self.new_bug("a", summary="Bug A") + bug_a.creator = "John Doe " + bug_a.time = 0 + bug_b = self.new_bug("b", summary="Bug B") + bug_b.creator = "Jane Doe " + bug_b.time = 0 + bug_b.status = "closed" + if sync_with_disk == True: + self.save() + self.set_sync_with_disk(True) + def cleanup(self): + if hasattr(self, "_dir_ref"): + self._dir_ref.cleanup() + BugDir.cleanup(self) + +class BugDirTestCase(unittest.TestCase): + def setUp(self): + self.dir = utility.Dir() + self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, + allow_vcs_init=True) + self.vcs = self.bugdir.vcs + def tearDown(self): + self.bugdir.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.vcs.versioned == False: + return + original = self.bugdir.vcs.commit("Began versioning") + bugA = self.bugdir.bug_from_uuid("a") + bugA.status = "fixed" + self.bugdir.save() + new = self.vcs.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.set_sync_with_disk(True) + self.bugdir._clear_bugs() + bug = self.bugdir.bug_from_uuid("a") + bug.load_comments() + if sync_with_disk == False: + self.bugdir.set_sync_with_disk(False) + 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 == sync_with_disk, + comment.sync_with_disk) + 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) + +class SimpleBugDirTestCase (unittest.TestCase): + def setUp(self): + # create a pre-existing bugdir in a temporary directory + self.dir = utility.Dir() + self.original_working_dir = os.getcwd() + os.chdir(self.dir.path) + self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, + allow_vcs_init=True) + self.bugdir.new_bug("preexisting", summary="Hopefully not imported") + self.bugdir.save() + def tearDown(self): + os.chdir(self.original_working_dir) + self.bugdir.cleanup() + self.dir.cleanup() + def testOnDiskCleanLoad(self): + """SimpleBugDir(sync_with_disk==True) should not import preexisting bugs.""" + bugdir = SimpleBugDir(sync_with_disk=True) + self.failUnless(bugdir.sync_with_disk==True, bugdir.sync_with_disk) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir._clear_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == [], uuids) + bugdir.load_all_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir.cleanup() + def testInMemoryCleanLoad(self): + """SimpleBugDir(sync_with_disk==False) should not import preexisting bugs.""" + bugdir = SimpleBugDir(sync_with_disk=False) + self.failUnless(bugdir.sync_with_disk==False, bugdir.sync_with_disk) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + self.failUnlessRaises(DiskAccessRequired, bugdir.load_all_bugs) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir._clear_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == [], uuids) + bugdir.cleanup() + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/email/interactive/libbe/bzr.py b/interfaces/email/interactive/libbe/bzr.py new file mode 100644 index 0000000..e9e0649 --- /dev/null +++ b/interfaces/email/interactive/libbe/bzr.py @@ -0,0 +1,113 @@ +# 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. + +""" +Bazaar (bzr) backend. +""" + +import os +import re +import sys +import unittest +import doctest + +import vcs + + +def new(): + return Bzr() + +class Bzr(vcs.VCS): + name = "bzr" + client = "bzr" + versioned = True + def _vcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _vcs_detect(self, path): + if self._u_search_parent_directories(path, ".bzr") != None : + return True + return False + def _vcs_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 _vcs_init(self, path): + self._u_invoke_client("init", directory=path) + def _vcs_get_user_id(self): + status,output,error = self._u_invoke_client("whoami") + return output.rstrip('\n') + def _vcs_set_user_id(self, value): + self._u_invoke_client("whoami", value) + def _vcs_add(self, path): + self._u_invoke_client("add", path) + def _vcs_remove(self, path): + # --force to also remove unversioned files. + self._u_invoke_client("remove", "--force", path) + def _vcs_update(self, path): + pass + def _vcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) + else: + status,output,error = \ + self._u_invoke_client("cat","-r",revision,path) + return output + def _vcs_duplicate_repo(self, directory, revision=None): + if revision == None: + vcs.VCS._vcs_duplicate_repo(self, directory, revision) + else: + self._u_invoke_client("branch", "--revision", revision, + ".", directory) + def _vcs_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 vcs.EmptyCommit() + else: + raise vcs.CommandError(args, status, stdout="", stderr=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 + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("revno") + current_revision = int(output) + if index >= current_revision or index < -current_revision: + return None + if index >= 0: + return str(index+1) # bzr commit 0 is the empty tree. + return str(current_revision+index+1) + + +vcs.make_vcs_testcase_subclasses(Bzr, sys.modules[__name__]) + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/email/interactive/libbe/cmdutil.py b/interfaces/email/interactive/libbe/cmdutil.py new file mode 100644 index 0000000..9b64142 --- /dev/null +++ b/interfaces/email/interactive/libbe/cmdutil.py @@ -0,0 +1,233 @@ +# 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. + +""" +Define assorted utilities to make command-line handling easier. +""" + +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 bug_from_shortname(bdir, shortname): + """ + Exception translation for the command-line interface. + """ + try: + bug = bdir.bug_from_shortname(shortname) + except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: + raise UserError(e.message) + return bug + +def _test(): + import doctest + import sys + doctest.testmod() + +if __name__ == "__main__": + _test() + +suite = doctest.DocTestSuite() diff --git a/interfaces/email/interactive/libbe/comment.py b/interfaces/email/interactive/libbe/comment.py new file mode 100644 index 0000000..41bc7e6 --- /dev/null +++ b/interfaces/email/interactive/libbe/comment.py @@ -0,0 +1,744 @@ +# 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. + +""" +Define the Comment class for representing bug comments. +""" + +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 + +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) + +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. + """ + if bug.sync_with_disk == False: + raise DiskAccessRequired("load comments") + path = bug.get_path("comments") + if not os.path.exists(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): + if bug.sync_with_disk == False: + raise DiskAccessRequired("save comments") + 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="Author", + doc="The author of the comment") + def author(): 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 date(): return {} + + def _get_time(self): + if self.date == None: + return None + return utility.str_to_time(self.date) + def _set_time(self, value): + self.date = utility.time_to_str(value) + time = property(fget=_get_time, + fset=_set_time, + doc="An integer version of .date") + + def _get_comment_body(self): + if self.vcs != None and self.sync_with_disk == True: + import vcs + binary = not self.content_type.startswith("text/") + return self.vcs.get_file_contents(self.get_path("body"), binary=binary) + def _set_comment_body(self, old=None, new=None, force=False): + if (self.vcs != 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.vcs.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_vcs(self): + if hasattr(self.bug, "vcs"): + return self.bug.vcs + + @Property + @cached_property(generator=_get_vcs) + @local_property("vcs") + @doc_property(doc="A revision control system instance.") + def vcs(): 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.vcs != None: + self.author = self.vcs.get_user_id() + self.in_reply_to = in_reply_to + self.body = body + + def __cmp__(self, other): + return cmp_full(self, other) + + def __str__(self): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> comm.uuid = "com-1" + >>> comm.date = "Thu, 20 Nov 2008 15:55:11 +0000" + >>> comm.author = "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 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 + + # serializing methods + + 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.date = "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), + ("author", self._setting_attr_string("author")), + ("date", self.date), + ("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.date = "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','author','date','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.author + >>> commB.author + """ + 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','author','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. + 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.date = "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("author"))) + lines.append("Date: %s" % self.date) + 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 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.author) # 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) + + # methods for saving/loading/acessing settings and properties. + + def get_path(self, *args): + dir = os.path.join(self.bug.get_path("comments"), self.uuid) + if len(args) == 0: + return dir + assert args[0] in ["values", "body"], str(args) + return os.path.join(dir, *args) + + def set_sync_with_disk(self, value): + self.sync_with_disk = value + + def load_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("load settings") + self.settings = mapfile.map_load(self.vcs, self.get_path("values")) + self._setup_saved_settings() + + def save_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("save settings") + self.vcs.mkdir(self.get_path()) + path = self.get_path("values") + mapfile.map_save(self.vcs, 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). + """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) + assert self.body != None, "Can't save blank comment" + self.save_settings() + self._set_comment_body(new=self.body, force=True) + if sync_with_disk == False: + self.set_sync_with_disk(False) + + def remove(self): + if self.sync_with_disk == False and self.uuid != INVALID_UUID: + raise DiskAccessRequired("remove") + for comment in self.traverse(): + path = comment.get_path() + self.vcs.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) + + 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 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) + +def cmp_attr(comment_1, comment_2, attr, invert=False): + """ + Compare a general attribute between two comments using the conventional + comparison rule for that attribute type. If invert == True, sort + *against* that convention. + >>> attr="author" + >>> commentA = Comment() + >>> commentB = Comment() + >>> commentA.author = "John Doe" + >>> commentB.author = "Jane Doe" + >>> cmp_attr(commentA, commentB, attr) > 0 + True + >>> cmp_attr(commentA, commentB, attr, invert=True) < 0 + True + >>> commentB.author = "John Doe" + >>> cmp_attr(commentA, commentB, attr) == 0 + True + """ + if not hasattr(comment_2, attr) : + return 1 + val_1 = getattr(comment_1, attr) + val_2 = getattr(comment_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_uuid = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "uuid") +cmp_author = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "author") +cmp_in_reply_to = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "in_reply_to") +cmp_content_type = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "content_type") +cmp_body = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "body") +# chronological rankings (newer < older) +cmp_time = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "time", invert=True) + +DEFAULT_CMP_FULL_CMP_LIST = \ + (cmp_time, cmp_author, cmp_content_type, cmp_body, cmp_in_reply_to, + cmp_uuid) + +class CommentCompoundComparator (object): + def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): + self.cmp_list = cmp_list + def __call__(self, comment_1, comment_2): + for comparison in self.cmp_list : + val = comparison(comment_1, comment_2) + if val != 0 : + return val + return 0 + +cmp_full = CommentCompoundComparator() + +suite = doctest.DocTestSuite() diff --git a/interfaces/email/interactive/libbe/config.py b/interfaces/email/interactive/libbe/config.py new file mode 100644 index 0000000..fb5a028 --- /dev/null +++ b/interfaces/email/interactive/libbe/config.py @@ -0,0 +1,89 @@ +# 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. + +""" +Create, save, and load the per-user config file at path(). +""" + +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/email/interactive/libbe/darcs.py b/interfaces/email/interactive/libbe/darcs.py new file mode 100644 index 0000000..16005f2 --- /dev/null +++ b/interfaces/email/interactive/libbe/darcs.py @@ -0,0 +1,184 @@ +# 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. + +""" +Darcs backend. +""" + +import codecs +import os +import re +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 +from xml.sax.saxutils import unescape +import doctest +import unittest + +import vcs + + +def new(): + return Darcs() + +class Darcs(vcs.VCS): + name="darcs" + client="darcs" + versioned=True + def _vcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _vcs_detect(self, path): + if self._u_search_parent_directories(path, "_darcs") != None : + return True + return False + def _vcs_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 _vcs_init(self, path): + self._u_invoke_client("init", directory=path) + def _vcs_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 _vcs_set_user_id(self, value): + if self.rootdir == None: + self.root(".") + if self.rootdir == None: + raise vcs.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 _vcs_add(self, path): + if os.path.isdir(path): + return + self._u_invoke_client("add", path) + def _vcs_remove(self, path): + if not os.path.isdir(self._u_abspath(path)): + os.remove(os.path.join(self.rootdir, path)) # darcs notices removal + def _vcs_update(self, path): + pass # darcs notices changes + def _vcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return vcs.VCS._vcs_get_file_contents(self, path, revision, + binary=binary) + else: + try: + return self._u_invoke_client("show", "contents", "--patch", revision, path) + except vcs.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 = vcs.VCS._vcs_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 = vcs.VCS._vcs_get_file_contents(self, path, + binary=binary) + return contents + def _vcs_duplicate_repo(self, directory, revision=None): + if revision==None: + vcs.VCS._vcs_duplicate_repo(self, directory, revision) + else: + self._u_invoke_client("put", "--to-patch", revision, directory) + def _vcs_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!"] + if self._u_any_in_string(empty_strings, output) == True: + if allow_empty == False: + raise vcs.EmptyCommit() + # note that darcs does _not_ make an empty revision. + # this returns the last non-empty revision id... + revision = self._vcs_revision_id(-1) + 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 + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("changes", "--xml") + revisions = [] + xml_str = output.encode("unicode_escape").replace(r"\n", "\n") + element = ElementTree.XML(xml_str) + assert element.tag == "changelog", element.tag + for patch in element.getchildren(): + assert patch.tag == "patch", patch.tag + for child in patch.getchildren(): + if child.tag == "name": + text = unescape(unicode(child.text).decode("unicode_escape").strip()) + revisions.append(text) + revisions.reverse() + try: + return revisions[index] + except IndexError: + return None + +vcs.make_vcs_testcase_subclasses(Darcs, sys.modules[__name__]) + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/email/interactive/libbe/diff.py b/interfaces/email/interactive/libbe/diff.py new file mode 100644 index 0000000..9253a23 --- /dev/null +++ b/interfaces/email/interactive/libbe/diff.py @@ -0,0 +1,419 @@ +# 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.""" + +import difflib +import doctest + +from libbe import bugdir, bug, settings_object, tree +from libbe.utility import time_to_str + + +class DiffTree (tree.Tree): + """ + A tree holding difference data for easy report generation. + >>> bugdir = DiffTree("bugdir") + >>> bdsettings = DiffTree("settings", data="target: None -> 1.0") + >>> bugdir.append(bdsettings) + >>> bugs = DiffTree("bugs", "bug-count: 5 -> 6") + >>> bugdir.append(bugs) + >>> new = DiffTree("new", "new bugs: ABC, DEF") + >>> bugs.append(new) + >>> rem = DiffTree("rem", "removed bugs: RST, UVW") + >>> bugs.append(rem) + >>> print bugdir.report_string() + target: None -> 1.0 + bug-count: 5 -> 6 + new bugs: ABC, DEF + removed bugs: RST, UVW + >>> print "\\n".join(bugdir.paths()) + bugdir + bugdir/settings + bugdir/bugs + bugdir/bugs/new + bugdir/bugs/rem + >>> bugdir.child_by_path("/") == bugdir + True + >>> bugdir.child_by_path("/bugs") == bugs + True + >>> bugdir.child_by_path("/bugs/rem") == rem + True + >>> bugdir.child_by_path("bugdir") == bugdir + True + >>> bugdir.child_by_path("bugdir/") == bugdir + True + >>> bugdir.child_by_path("bugdir/bugs") == bugs + True + >>> bugdir.child_by_path("/bugs").masked = True + >>> print bugdir.report_string() + target: None -> 1.0 + """ + def __init__(self, name, data=None, data_part_fn=str, + requires_children=False, masked=False): + tree.Tree.__init__(self) + self.name = name + self.data = data + self.data_part_fn = data_part_fn + self.requires_children = requires_children + self.masked = masked + def paths(self, parent_path=None): + paths = [] + if parent_path == None: + path = self.name + else: + path = "%s/%s" % (parent_path, self.name) + paths.append(path) + for child in self: + paths.extend(child.paths(path)) + return paths + def child_by_path(self, path): + if hasattr(path, "split"): # convert string path to a list of names + names = path.split("/") + if names[0] == "": + names[0] = self.name # replace root with self + if len(names) > 1 and names[-1] == "": + names = names[:-1] # strip empty tail + else: # it was already an array + names = path + assert len(names) > 0, path + if names[0] == self.name: + if len(names) == 1: + return self + for child in self: + if names[1] == child.name: + return child.child_by_path(names[1:]) + if len(names) == 1: + raise KeyError, "%s doesn't match '%s'" % (names, self.name) + raise KeyError, "%s points to child not in %s" % (names, [c.name for c in self]) + def report_string(self): + return "\n".join(self.report()) + def report(self, root=None, parent=None, depth=0): + if root == None: + root = self.make_root() + if self.masked == True: + return None + data_part = self.data_part(depth) + if self.requires_children == True and len(self) == 0: + pass + else: + self.join(root, parent, data_part) + if data_part != None: + depth += 1 + for child in self: + child.report(root, self, depth) + return root + def make_root(self): + return [] + def join(self, root, parent, data_part): + if data_part != None: + root.append(data_part) + def data_part(self, depth, indent=True): + if self.data == None: + return None + if hasattr(self, "_cached_data_part"): + return self._cached_data_part + data_part = self.data_part_fn(self.data) + if indent == True: + data_part_lines = data_part.splitlines() + indent = " "*(depth) + line_sep = "\n"+indent + data_part = indent+line_sep.join(data_part_lines) + self._cached_data_part = data_part + return data_part + +class Diff (object): + """ + Difference tree generator for BugDirs. + >>> import copy + >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) + >>> bd.user_id = "John Doe " + >>> bd_new = copy.deepcopy(bd) + >>> bd_new.target = "1.0" + >>> a = bd_new.bug_from_uuid("a") + >>> rep = a.comment_root.new_reply("I'm closing this bug") + >>> rep.uuid = "acom" + >>> rep.date = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> a.status = "closed" + >>> b = bd_new.bug_from_uuid("b") + >>> bd_new.remove_bug(b) + >>> c = bd_new.new_bug("c", "Bug C") + >>> d = Diff(bd, bd_new) + >>> r = d.report_tree() + >>> print "\\n".join(r.paths()) + bugdir + bugdir/settings + bugdir/bugs + bugdir/bugs/new + bugdir/bugs/new/c + bugdir/bugs/rem + bugdir/bugs/rem/b + bugdir/bugs/mod + bugdir/bugs/mod/a + bugdir/bugs/mod/a/settings + bugdir/bugs/mod/a/comments + bugdir/bugs/mod/a/comments/new + bugdir/bugs/mod/a/comments/new/acom + bugdir/bugs/mod/a/comments/rem + bugdir/bugs/mod/a/comments/mod + >>> print r.report_string() + Changed bug directory settings: + target: None -> 1.0 + New bugs: + c:om: Bug C + Removed bugs: + b:cm: Bug B + Modified bugs: + a:cm: Bug A + Changed bug settings: + status: open -> closed + New comments: + from John Doe on Thu, 01 Jan 1970 00:00:00 +0000 + I'm closing this bug... + >>> bd.cleanup() + """ + def __init__(self, old_bugdir, new_bugdir): + self.old_bugdir = old_bugdir + self.new_bugdir = new_bugdir + + # data assembly methods + + def _changed_bugs(self): + """ + Search for differences in all bugs between .old_bugdir and + .new_bugdir. Returns + (added_bugs, modified_bugs, removed_bugs) + where added_bugs and removed_bugs are lists of added and + removed bugs respectively. modified_bugs is a list of + (old_bug,new_bug) pairs. + """ + if hasattr(self, "__changed_bugs"): + return self.__changed_bugs + added = [] + removed = [] + modified = [] + for uuid in self.new_bugdir.list_uuids(): + new_bug = self.new_bugdir.bug_from_uuid(uuid) + try: + old_bug = self.old_bugdir.bug_from_uuid(uuid) + except KeyError: + added.append(new_bug) + else: + if old_bug.sync_with_disk == True: + old_bug.load_comments() + if new_bug.sync_with_disk == True: + new_bug.load_comments() + if old_bug != new_bug: + modified.append((old_bug, new_bug)) + for uuid in self.old_bugdir.list_uuids(): + if not self.new_bugdir.has_bug(uuid): + old_bug = self.old_bugdir.bug_from_uuid(uuid) + removed.append(old_bug) + added.sort() + removed.sort() + modified.sort(self._bug_modified_cmp) + self.__changed_bugs = (added, modified, removed) + return self.__changed_bugs + def _bug_modified_cmp(self, left, right): + return cmp(left[1], right[1]) + def _changed_comments(self, old, new): + """ + Search for differences in all loaded comments between the bugs + old and new. Returns + (added_comments, modified_comments, removed_comments) + analogous to ._changed_bugs. + """ + if hasattr(self, "__changed_comments"): + if new.uuid in self.__changed_comments: + return self.__changed_comments[new.uuid] + else: + self.__changed_comments = {} + added = [] + removed = [] + modified = [] + old.comment_root.sort(key=lambda comm : comm.time) + new.comment_root.sort(key=lambda comm : comm.time) + old_comment_ids = [c.uuid for c in old.comments()] + new_comment_ids = [c.uuid for c in new.comments()] + for uuid in new_comment_ids: + new_comment = new.comment_from_uuid(uuid) + try: + old_comment = old.comment_from_uuid(uuid) + except KeyError: + added.append(new_comment) + else: + if old_comment != new_comment: + modified.append((old_comment, new_comment)) + for uuid in old_comment_ids: + if uuid not in new_comment_ids: + new_comment = new.comment_from_uuid(uuid) + removed.append(new_comment) + self.__changed_comments[new.uuid] = (added, modified, removed) + return self.__changed_comments[new.uuid] + def _attribute_changes(self, old, new, attributes): + """ + Take two objects old and new, and compare the value of *.attr + for attr in the list attribute names. Returns a list of + (attr_name, old_value, new_value) + tuples. + """ + change_list = [] + for attr in attributes: + old_value = getattr(old, attr) + new_value = getattr(new, attr) + if old_value != new_value: + change_list.append((attr, old_value, new_value)) + if len(change_list) >= 0: + return change_list + return None + def _settings_properties_attribute_changes(self, old, new, + hidden_properties=[]): + properties = sorted(new.settings_properties) + for p in hidden_properties: + properties.remove(p) + attributes = [settings_object.setting_name_to_attr_name(None, p) + for p in properties] + return self._attribute_changes(old, new, attributes) + def _bugdir_attribute_changes(self): + return self._settings_properties_attribute_changes( \ + self.old_bugdir, self.new_bugdir, + ["vcs_name"]) # tweaked by bugdir.duplicate_bugdir + def _bug_attribute_changes(self, old, new): + return self._settings_properties_attribute_changes(old, new) + def _comment_attribute_changes(self, old, new): + return self._settings_properties_attribute_changes(old, new) + + # report generation methods + + def report_tree(self, diff_tree=DiffTree): + """ + Pretty bare to make it easy to adjust to specific cases. You + can pass in a DiffTree subclass via diff_tree to override the + default report assembly process. + """ + if hasattr(self, "__report_tree"): + return self.__report_tree + bugdir_settings = sorted(self.new_bugdir.settings_properties) + bugdir_settings.remove("vcs_name") # tweaked by bugdir.duplicate_bugdir + root = diff_tree("bugdir") + bugdir_attribute_changes = self._bugdir_attribute_changes() + if len(bugdir_attribute_changes) > 0: + bugdir = diff_tree("settings", bugdir_attribute_changes, + self.bugdir_attribute_change_string) + root.append(bugdir) + bug_root = diff_tree("bugs") + root.append(bug_root) + add,mod,rem = self._changed_bugs() + bnew = diff_tree("new", "New bugs:", requires_children=True) + bug_root.append(bnew) + for bug in add: + b = diff_tree(bug.uuid, bug, self.bug_add_string) + bnew.append(b) + brem = diff_tree("rem", "Removed bugs:", requires_children=True) + bug_root.append(brem) + for bug in rem: + b = diff_tree(bug.uuid, bug, self.bug_rem_string) + brem.append(b) + bmod = diff_tree("mod", "Modified bugs:", requires_children=True) + bug_root.append(bmod) + for old,new in mod: + b = diff_tree(new.uuid, (old,new), self.bug_mod_string) + bmod.append(b) + bug_attribute_changes = self._bug_attribute_changes(old, new) + if len(bug_attribute_changes) > 0: + bset = diff_tree("settings", bug_attribute_changes, + self.bug_attribute_change_string) + b.append(bset) + if old.summary != new.summary: + data = (old.summary, new.summary) + bsum = diff_tree("summary", data, self.bug_summary_change_string) + b.append(bsum) + cr = diff_tree("comments") + b.append(cr) + a,m,d = self._changed_comments(old, new) + cnew = diff_tree("new", "New comments:", requires_children=True) + for comment in a: + c = diff_tree(comment.uuid, comment, self.comment_add_string) + cnew.append(c) + crem = diff_tree("rem", "Removed comments:",requires_children=True) + for comment in d: + c = diff_tree(comment.uuid, comment, self.comment_rem_string) + crem.append(c) + cmod = diff_tree("mod","Modified comments:",requires_children=True) + for o,n in m: + c = diff_tree(n.uuid, (o,n), self.comment_mod_string) + cmod.append(c) + comm_attribute_changes = self._comment_attribute_changes(o, n) + if len(comm_attribute_changes) > 0: + cset = diff_tree("settings", comm_attribute_changes, + self.comment_attribute_change_string) + if o.body != n.body: + data = (o.body, n.body) + cbody = diff_tree("cbody", data, + self.comment_body_change_string) + c.append(cbody) + cr.extend([cnew, crem, cmod]) + self.__report_tree = root + return self.__report_tree + + # change data -> string methods. + # Feel free to play with these in subclasses. + + def attribute_change_string(self, attribute_changes, indent=0): + indent_string = " "*indent + change_strings = [u"%s: %s -> %s" % f for f in attribute_changes] + for i,change_string in enumerate(change_strings): + change_strings[i] = indent_string+change_string + return u"\n".join(change_strings) + def bugdir_attribute_change_string(self, attribute_changes): + return "Changed bug directory settings:\n%s" % \ + self.attribute_change_string(attribute_changes, indent=1) + def bug_attribute_change_string(self, attribute_changes): + return "Changed bug settings:\n%s" % \ + self.attribute_change_string(attribute_changes, indent=1) + def comment_attribute_change_string(self, attribute_changes): + return "Changed comment settings:\n%s" % \ + self.attribute_change_string(attribute_changes, indent=1) + def bug_add_string(self, bug): + return bug.string(shortlist=True) + def bug_rem_string(self, bug): + return bug.string(shortlist=True) + def bug_mod_string(self, bugs): + old_bug,new_bug = bugs + return new_bug.string(shortlist=True) + def bug_summary_change_string(self, summaries): + old_summary,new_summary = summaries + return "summary changed:\n %s\n %s" % (old_summary, new_summary) + def _comment_summary_string(self, comment): + return "from %s on %s" % (comment.author, time_to_str(comment.time)) + def comment_add_string(self, comment): + summary = self._comment_summary_string(comment) + first_line = comment.body.splitlines()[0] + return "%s\n %s..." % (summary, first_line) + def comment_rem_string(self, comment): + summary = self._comment_summary_string(comment) + first_line = comment.body.splitlines()[0] + return "%s\n %s..." % (summary, first_line) + def comment_mod_string(self, comments): + old_comment,new_comment = comments + return self._comment_summary_string(new_comment) + def comment_body_change_string(self, bodies): + old_body,new_body = bodies + return difflib.unified_diff(old_body, new_body) + + +suite = doctest.DocTestSuite() diff --git a/interfaces/email/interactive/libbe/editor.py b/interfaces/email/interactive/libbe/editor.py new file mode 100644 index 0000000..ec41006 --- /dev/null +++ b/interfaces/email/interactive/libbe/editor.py @@ -0,0 +1,108 @@ +# 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. + +""" +Define editor_string(), a function that invokes an editor to accept +user-produced text as a string. +""" + +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: + cstring = u'\n'+comment_string(comment) + os.write(fhandle, cstring.encode(encoding)) + 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/email/interactive/libbe/encoding.py b/interfaces/email/interactive/libbe/encoding.py new file mode 100644 index 0000000..fd513b5 --- /dev/null +++ b/interfaces/email/interactive/libbe/encoding.py @@ -0,0 +1,61 @@ +# 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. + +""" +Support input/output/filesystem encodings (e.g. UTF-8). +""" + +import codecs +import locale +import sys +import doctest + + +ENCODING = None # override get_encoding() output by setting this + +def get_encoding(): + """ + Guess a useful input/output/filesystem encoding... Maybe we need + seperate encodings for input/output and filesystem? Hmm... + """ + if ENCODING != None: + return ENCODING + 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/email/interactive/libbe/git.py b/interfaces/email/interactive/libbe/git.py new file mode 100644 index 0000000..3abe3b8 --- /dev/null +++ b/interfaces/email/interactive/libbe/git.py @@ -0,0 +1,148 @@ +# 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. + +""" +Git backend. +""" + +import os +import re +import sys +import unittest +import doctest + +import vcs + + +def new(): + return Git() + +class Git(vcs.VCS): + name="git" + client="git" + versioned=True + def _vcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _vcs_detect(self, path): + if self._u_search_parent_directories(path, ".git") != None : + return True + return False + def _vcs_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 _vcs_init(self, path): + self._u_invoke_client("init", directory=path) + def _vcs_get_user_id(self): + status,output,error = \ + self._u_invoke_client("config", "user.name", expect=(0,1)) + if status == 0: + name = output.rstrip('\n') + else: + name = "" + status,output,error = \ + self._u_invoke_client("config", "user.email", expect=(0,1)) + if status == 0: + email = output.rstrip('\n') + else: + email = "" + 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 _vcs_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 _vcs_add(self, path): + if os.path.isdir(path): + return + self._u_invoke_client("add", path) + def _vcs_remove(self, path): + if not os.path.isdir(self._u_abspath(path)): + self._u_invoke_client("rm", "-f", path) + def _vcs_update(self, path): + self._vcs_add(path) + def _vcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return vcs.VCS._vcs_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 _vcs_duplicate_repo(self, directory, revision=None): + if revision==None: + vcs.VCS._vcs_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 _vcs_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 vcs.EmptyCommit() + revision = None + revline = re.compile("(.*) (.*)[:\]] (.*)") + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 3 + revision = match.groups()[1] + full_revision = self._vcs_revision_id(-1) + assert full_revision.startswith(revision), \ + "Mismatched revisions:\n%s\n%s" % (revision, full_revision) + return full_revision + def _vcs_revision_id(self, index): + args = ["rev-list", "--first-parent", "--reverse", "HEAD"] + kwargs = {"expect":(0,128)} + status,output,error = self._u_invoke_client(*args, **kwargs) + if status == 128: + if error.startswith("fatal: ambiguous argument 'HEAD': unknown "): + return None + raise vcs.CommandError(args, status, stdout="", stderr=error) + commits = output.splitlines() + try: + return commits[index] + except IndexError: + return None + + +vcs.make_vcs_testcase_subclasses(Git, sys.modules[__name__]) + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/email/interactive/libbe/hg.py b/interfaces/email/interactive/libbe/hg.py new file mode 100644 index 0000000..f8f8121 --- /dev/null +++ b/interfaces/email/interactive/libbe/hg.py @@ -0,0 +1,103 @@ +# 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. + +""" +Mercurial (hg) backend. +""" + +import os +import re +import sys +import unittest +import doctest + +import vcs + + +def new(): + return Hg() + +class Hg(vcs.VCS): + name="hg" + client="hg" + versioned=True + def _vcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _vcs_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 _vcs_root(self, path): + status,output,error = self._u_invoke_client("root", directory=path) + return output.rstrip('\n') + def _vcs_init(self, path): + self._u_invoke_client("init", directory=path) + def _vcs_get_user_id(self): + status,output,error = self._u_invoke_client("showconfig","ui.username") + return output.rstrip('\n') + def _vcs_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 vcs.SettingIDnotSupported + def _vcs_add(self, path): + self._u_invoke_client("add", path) + def _vcs_remove(self, path): + self._u_invoke_client("rm", "--force", path) + def _vcs_update(self, path): + pass + def _vcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) + else: + status,output,error = \ + self._u_invoke_client("cat","-r",revision,path) + return output + def _vcs_duplicate_repo(self, directory, revision=None): + if revision == None: + return vcs.VCS._vcs_duplicate_repo(self, directory, revision) + else: + self._u_invoke_client("archive", "--rev", revision, directory) + def _vcs_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 vcs.EmptyCommit() + return self._vcs_revision_id(-1) + def _vcs_revision_id(self, index, style="id"): + args = ["identify", "--rev", str(int(index)), "--%s" % style] + kwargs = {"expect": (0,255)} + status,output,error = self._u_invoke_client(*args, **kwargs) + if status == 0: + id = output.strip() + if id == '000000000000': + return None # before initial commit. + return id + return None + + +vcs.make_vcs_testcase_subclasses(Hg, sys.modules[__name__]) + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/email/interactive/libbe/mapfile.py b/interfaces/email/interactive/libbe/mapfile.py new file mode 100644 index 0000000..4d69601 --- /dev/null +++ b/interfaces/email/interactive/libbe/mapfile.py @@ -0,0 +1,116 @@ +# 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. + +""" +Provide a means of saving and loading dictionaries of parameters. The +saved "mapfiles" should be clear, flat-text files, and allow easy merging of +independent/conflicting changes. +""" + +import errno +import os.path +import yaml +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' + """ + return yaml.load(contents) or {} + +def map_save(vcs, path, map, allow_no_vcs=False): + """Save the map as a mapfile to the specified path""" + contents = generate(map) + vcs.set_file_contents(path, contents, allow_no_vcs) + +def map_load(vcs, path, allow_no_vcs=False): + contents = vcs.get_file_contents(path, allow_no_vcs=allow_no_vcs) + return parse(contents) + +suite = doctest.DocTestSuite() diff --git a/interfaces/email/interactive/libbe/plugin.py b/interfaces/email/interactive/libbe/plugin.py new file mode 100644 index 0000000..d593d69 --- /dev/null +++ b/interfaces/email/interactive/libbe/plugin.py @@ -0,0 +1,77 @@ +# 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. + +""" +Allow simple listing and loading of the various becommands and libbe +submodules (i.e. "plugins"). +""" + +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, default=None): + _init_mutable_property_cache(self) + if (cacher_name, property_name) not in self._mutable_property_cache_hash: + _set_cached_mutable_property(self, cacher_name, property_name, default) + 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, default=None): + """ + 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, default) != 0: + # there has been a change, cache new value + old_value = _get_cached_mutable_property(self, "change hook property", name, default) + _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/email/interactive/libbe/settings_object.py b/interfaces/email/interactive/libbe/settings_object.py new file mode 100644 index 0000000..ceea9d5 --- /dev/null +++ b/interfaces/email/interactive/libbe/settings_object.py @@ -0,0 +1,412 @@ +# 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, + default=EMPTY) + 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 == [], 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 == [ + "'' -> '[]'" + ], SAVES) + t.list_type.append(5) + self.failUnless(SAVES == [ + "'' -> '[]'", + ], SAVES) + self.failUnless(t.settings["List-type"] == [5],t.settings["List-type"]) + self.failUnless(SAVES == [ # the append(5) has not yet been saved + "'' -> '[]'", + ], SAVES) + self.failUnless(t.list_type == [5], t.list_type) # <-get triggers saved + + self.failUnless(SAVES == [ # now the append(5) has been saved. + "'' -> '[]'", + "'[]' -> '[5]'" + ], SAVES) + +unitsuite=unittest.TestLoader().loadTestsFromTestCase(SavedSettingsObjectTests) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/interfaces/email/interactive/libbe/tree.py b/interfaces/email/interactive/libbe/tree.py new file mode 100644 index 0000000..06d09e5 --- /dev/null +++ b/interfaces/email/interactive/libbe/tree.py @@ -0,0 +1,183 @@ +# 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. + +""" +Define a traversable tree structure. +""" + +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/email/interactive/libbe/upgrade.py b/interfaces/email/interactive/libbe/upgrade.py new file mode 100644 index 0000000..4123c72 --- /dev/null +++ b/interfaces/email/interactive/libbe/upgrade.py @@ -0,0 +1,187 @@ +# 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. + +""" +Handle conversion between the various on-disk images. +""" + +import os, os.path +import sys +import doctest + +import encoding +import mapfile +import vcs + +# a list of all past versions +BUGDIR_DISK_VERSIONS = ["Bugs Everywhere Tree 1 0", + "Bugs Everywhere Directory v1.1", + "Bugs Everywhere Directory v1.2"] + +# the current version +BUGDIR_DISK_VERSION = BUGDIR_DISK_VERSIONS[-1] + +class Upgrader (object): + "Class for converting " + initial_version = None + final_version = None + def __init__(self, root): + self.root = root + # use the "None" VCS to ensure proper encoding/decoding and + # simplify path construction. + self.vcs = vcs.vcs_by_name("None") + self.vcs.root(self.root) + self.vcs.encoding = encoding.get_encoding() + + def get_path(self, *args): + """ + Return a path relative to .root. + """ + dir = os.path.join(self.root, ".be") + if len(args) == 0: + return dir + assert args[0] in ["version", "settings", "bugs"], str(args) + return os.path.join(dir, *args) + + def check_initial_version(self): + path = self.get_path("version") + version = self.vcs.get_file_contents(path).rstrip("\n") + assert version == self.initial_version, version + + def set_version(self): + path = self.get_path("version") + self.vcs.set_file_contents(path, self.final_version+"\n") + + def upgrade(self): + print >> sys.stderr, "upgrading bugdir from '%s' to '%s'" \ + % (self.initial_version, self.final_version) + self.check_initial_version() + self.set_version() + self._upgrade() + + def _upgrade(self): + raise NotImplementedError + + +class Upgrade_1_0_to_1_1 (Upgrader): + initial_version = "Bugs Everywhere Tree 1 0" + final_version = "Bugs Everywhere Directory v1.1" + def _upgrade_mapfile(self, path): + contents = self.vcs.get_file_contents(path) + old_format = False + for line in contents.splitlines(): + if len(line.split("=")) == 2: + old_format = True + break + if old_format == True: + # translate to YAML. + 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) + # load the YAML and save + map = mapfile.parse(contents) + mapfile.map_save(self.vcs, path, map) + + def _upgrade(self): + """ + Comment value field "From" -> "Author". + Homegrown mapfile -> YAML. + """ + path = self.get_path("settings") + self._upgrade_mapfile(path) + for bug_uuid in os.listdir(self.get_path("bugs")): + path = self.get_path("bugs", bug_uuid, "values") + self._upgrade_mapfile(path) + c_path = ["bugs", bug_uuid, "comments"] + if not os.path.exists(self.get_path(*c_path)): + continue # no comments for this bug + for comment_uuid in os.listdir(self.get_path(*c_path)): + path_list = c_path + [comment_uuid, "values"] + path = self.get_path(*path_list) + self._upgrade_mapfile(path) + settings = mapfile.map_load(self.vcs, path) + if "From" in settings: + settings["Author"] = settings.pop("From") + mapfile.map_save(self.vcs, path, settings) + + +class Upgrade_1_1_to_1_2 (Upgrader): + initial_version = "Bugs Everywhere Directory v1.1" + final_version = "Bugs Everywhere Directory v1.2" + def _upgrade(self): + """ + BugDir settings field "rcs_name" -> "vcs_name". + """ + path = self.get_path("settings") + settings = mapfile.map_load(self.vcs, path) + if "rcs_name" in settings: + settings["vcs_name"] = settings.pop("rcs_name") + mapfile.map_save(self.vcs, path, settings) + + +upgraders = [Upgrade_1_0_to_1_1, + Upgrade_1_1_to_1_2] +upgrade_classes = {} +for upgrader in upgraders: + upgrade_classes[(upgrader.initial_version,upgrader.final_version)]=upgrader + +def upgrade(path, current_version, + target_version=BUGDIR_DISK_VERSION): + """ + Call the appropriate upgrade function to convert current_version + to target_version. If a direct conversion function does not exist, + use consecutive conversion functions. + """ + if current_version not in BUGDIR_DISK_VERSIONS: + raise NotImplementedError, \ + "Cannot handle version '%s' yet." % version + if target_version not in BUGDIR_DISK_VERSIONS: + raise NotImplementedError, \ + "Cannot handle version '%s' yet." % version + + if (current_version, target_version) in upgrade_classes: + # direct conversion + upgrade_class = upgrade_classes[(current_version, target_version)] + u = upgrade_class(path) + u.upgrade() + else: + # consecutive single-step conversion + i = BUGDIR_DISK_VERSIONS.index(current_version) + while True: + version_a = BUGDIR_DISK_VERSIONS[i] + version_b = BUGDIR_DISK_VERSIONS[i+1] + try: + upgrade_class = upgrade_classes[(version_a, version_b)] + except KeyError: + raise NotImplementedError, \ + "Cannot convert version '%s' to '%s' yet." \ + % (version_a, version_b) + u = upgrade_class(path) + u.upgrade() + if version_b == target_version: + break + i += 1 + +suite = doctest.DocTestSuite() diff --git a/interfaces/email/interactive/libbe/utility.py b/interfaces/email/interactive/libbe/utility.py new file mode 100644 index 0000000..aafbf8d --- /dev/null +++ b/interfaces/email/interactive/libbe/utility.py @@ -0,0 +1,134 @@ +# 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. + +""" +Assorted utility functions that don't fit in anywhere else. +""" + +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/interfaces/email/interactive/libbe/vcs.py b/interfaces/email/interactive/libbe/vcs.py new file mode 100644 index 0000000..a1d3022 --- /dev/null +++ b/interfaces/email/interactive/libbe/vcs.py @@ -0,0 +1,938 @@ +# 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. + +""" +Define the base VCS (Version Control System) class, which should be +subclassed by other Version Control System backends. The base class +implements a "do not version" VCS. +""" + +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_vcs(matchfn): + """Return the first module for which matchfn(VCS_instance) is true""" + import arch + import bzr + import darcs + import git + import hg + for module in [arch, bzr, darcs, git, hg]: + vcs = module.new() + if matchfn(vcs) == True: + return vcs + del(vcs) + return VCS() + +def vcs_by_name(vcs_name): + """Return the module for the VCS with the given name""" + return _get_matching_vcs(lambda vcs: vcs.name == vcs_name) + +def detect_vcs(dir): + """Return an VCS instance for the vcs being used in this directory""" + return _get_matching_vcs(lambda vcs: vcs.detect(dir)) + +def installed_vcs(): + """Return an instance of an installed VCS""" + return _get_matching_vcs(lambda vcs: vcs.installed()) + + +class CommandError(Exception): + def __init__(self, command, status, stdout, stderr): + strerror = ["Command failed (%d):\n %s\n" % (status, stderr), + "while executing\n %s" % command] + Exception.__init__(self, "\n".join(strerror)) + self.command = command + self.status = status + self.stdout = stdout + self.stderr = stderr + +class SettingIDnotSupported(NotImplementedError): + pass + +class VCSnotRooted(Exception): + def __init__(self): + msg = "VCS 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 VCS() + +class VCS(object): + """ + This class implements a 'no-vcs' interface. + + Support for other VCSs can be added by subclassing this class, and + overriding methods _vcs_*() with code appropriate for your VCS. + + The methods _u_*() are utility methods available to the _vcs_*() + 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 _vcs_help(self): + """ + Return the command help string. + (Allows a simple test to see if the client is installed.) + """ + pass + def _vcs_detect(self, path=None): + """ + Detect whether a directory is revision controlled with this VCS. + """ + return True + def _vcs_root(self, path): + """ + Get the VCS root. This is the default working directory for + future invocations. You would normally set this to the root + directory for your VCS. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + if path == "": + path = os.path.abspath(".") + return path + def _vcs_init(self, path): + """ + Begin versioning the tree based at path. + """ + pass + def _vcs_cleanup(self): + """ + Remove any cruft that _vcs_init() created outside of the + versioned tree. + """ + pass + def _vcs_get_user_id(self): + """ + Get the VCS's suggested user id (e.g. "John Doe "). + If the VCS has not been configured with a username, return None. + """ + return None + def _vcs_set_user_id(self, value): + """ + Set the VCS's suggested user id (e.g "John Doe "). + This is run if the VCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + raise SettingIDnotSupported + def _vcs_add(self, path): + """ + Add the already created file at path to version control. + """ + pass + def _vcs_remove(self, path): + """ + Remove the file at path from version control. Optionally + remove the file from the filesystem as well. + """ + pass + def _vcs_update(self, path): + """ + Notify the versioning system of changes to the versioned file + at path. + """ + pass + def _vcs_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 VCS 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 _vcs_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 _vcs_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 _vcs_revision_id(self, index): + """ + Return the name of the th revision. Index will be an + integer (possibly <= 0). The choice of which branch to follow + when crossing branches/merges is not defined. + + Return None if revision IDs are not supported, or if the + specified revision does not exist. + """ + return None + def installed(self): + try: + self._vcs_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 VCS. + """ + return self._vcs_detect(path) + def root(self, path): + """ + Set the root directory to the path's VCS root. This is the + default working directory for future invocations. + """ + self.rootdir = self._vcs_root(path) + def init(self, path): + """ + Begin versioning the tree based at path. + Also roots the vcs at path. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + self._vcs_init(path) + self.root(path) + def cleanup(self): + self._vcs_cleanup() + def get_user_id(self): + """ + Get the VCS's suggested user id (e.g. "John Doe "). + If the VCS has not been configured with a username, return the user's + id. You can override the automatic lookup procedure by setting the + VCS.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._vcs_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 VCS's suggested user id (e.g "John Doe "). + This is run if the VCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + self._vcs_set_user_id(value) + def add(self, path): + """ + Add the already created file at path to version control. + """ + self._vcs_add(self._u_rel_path(path)) + def remove(self, path): + """ + Remove a file from both version control and the filesystem. + """ + self._vcs_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._vcs_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._vcs_update(self._u_rel_path(path)) + def get_file_contents(self, path, revision=None, allow_no_vcs=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_vcs(path, allow_no_vcs): + relpath = self._u_rel_path(path) + contents = self._vcs_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_vcs=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_vcs(path, allow_no_vcs): + if add: + self.add(path) + else: + self.update(path) + def mkdir(self, path, allow_no_vcs=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_vcs, check_parents) + if not os.path.exists(path): + os.mkdir(path) + if self._use_vcs(path, allow_no_vcs): + self.add(path) + else: + assert os.path.isdir(path) + if self._use_vcs(path, allow_no_vcs): + #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='BEvcs') + self._duplicateDirname = \ + os.path.join(self._duplicateBasedir, "duplicate") + self._vcs_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._vcs_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 revision_id(self, index=None): + """ + Return the name of the th revision. The choice of + which branch to follow when crossing branches/merges is not + defined. + + Return None if index==None, revision IDs are not supported, or + if the specified revision does not exist. + """ + if index == None: + return None + return self._vcs_revision_id(index) + 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, status=e.args[0], stdout="", stderr=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, output, 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_vcs(self, path, allow_no_vcs): + """ + Try and decide if _vcs_add/update/mkdir/etc calls will + succeed. Returns True is we think the vcs_call would + succeeed, and False otherwise. + """ + use_vcs = True + exception = None + if self.rootdir != None: + if self.path_in_root(path) == False: + use_vcs = False + exception = PathNotInRoot(path, self.rootdir) + else: + use_vcs = False + exception = VCSnotRooted + if use_vcs == False and allow_no_vcs==False: + raise exception + return use_vcs + def path_in_root(self, path, root=None): + """ + Return the relative path to path from root. + >>> vcs = new() + >>> vcs.path_in_root("/a.b/c/.be", "/a.b/c") + True + >>> vcs.path_in_root("/a.b/.be", "/a.b/c") + False + """ + if root == None: + if self.rootdir == None: + raise VCSnotRooted + 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. + >>> vcs = new() + >>> vcs._u_rel_path("/a.b/c/.be", "/a.b/c") + '.be' + """ + if root == None: + if self.rootdir == None: + raise VCSnotRooted + 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. + >>> vcs = new() + >>> vcs._u_abspath(".be", "/a.b/c") + '/a.b/c/.be' + """ + if root == None: + assert self.rootdir != None, "VCS not rooted" + root = self.rootdir + return os.path.abspath(os.path.join(root, path)) + def _u_create_id(self, name, email=None): + """ + >>> vcs = new() + >>> vcs._u_create_id("John Doe", "jdoe@example.com") + 'John Doe ' + >>> vcs._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): + """ + >>> vcs = new() + >>> vcs._u_parse_id("John Doe ") + ('John Doe', 'jdoe@example.com') + >>> vcs._u_parse_id("John Doe") + ('John Doe', None) + >>> try: + ... vcs._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_vcs_test_fixtures(testcase): + """Set up test fixtures for VCS test case.""" + testcase.vcs = testcase.Class() + testcase.dir = Dir() + testcase.dirname = testcase.dir.path + + vcs_not_supporting_uninitialized_user_id = [] + vcs_not_supporting_set_user_id = ["None", "hg"] + testcase.vcs_supports_uninitialized_user_id = ( + testcase.vcs.name not in vcs_not_supporting_uninitialized_user_id) + testcase.vcs_supports_set_user_id = ( + testcase.vcs.name not in vcs_not_supporting_set_user_id) + + if not testcase.vcs.installed(): + testcase.fail( + "%(name)s VCS not found" % vars(testcase.Class)) + + if testcase.Class.name != "None": + testcase.failIf( + testcase.vcs.detect(testcase.dirname), + "Detected %(name)s VCS before initialising" + % vars(testcase.Class)) + + testcase.vcs.init(testcase.dirname) + + +class VCSTestCase(unittest.TestCase): + """Test cases for base VCS class.""" + + Class = VCS + + def __init__(self, *args, **kwargs): + super(VCSTestCase, self).__init__(*args, **kwargs) + self.dirname = None + + def setUp(self): + super(VCSTestCase, self).setUp() + setup_vcs_test_fixtures(self) + + def tearDown(self): + del(self.vcs) + super(VCSTestCase, self).tearDown() + + def full_path(self, rel_path): + return os.path.join(self.dirname, rel_path) + + +class VCS_init_TestCase(VCSTestCase): + """Test cases for VCS.init method.""" + + def test_detect_should_succeed_after_init(self): + """Should detect VCS in directory after initialization.""" + self.failUnless( + self.vcs.detect(self.dirname), + "Did not detect %(name)s VCS after initialising" + % vars(self.Class)) + + def test_vcs_rootdir_in_specified_root_path(self): + """VCS root directory should be in specified root path.""" + rp = os.path.realpath(self.vcs.rootdir) + dp = os.path.realpath(self.dirname) + vcs_name = self.Class.name + self.failUnless( + dp == rp or rp == None, + "%(vcs_name)s VCS root in wrong dir (%(dp)s %(rp)s)" % vars()) + + +class VCS_get_user_id_TestCase(VCSTestCase): + """Test cases for VCS.get_user_id method.""" + + def test_gets_existing_user_id(self): + """Should get the existing user ID.""" + if not self.vcs_supports_uninitialized_user_id: + return + + user_id = self.vcs.get_user_id() + self.failUnless( + user_id is not None, + "unable to get a user id") + + +class VCS_set_user_id_TestCase(VCSTestCase): + """Test cases for VCS.set_user_id method.""" + + def setUp(self): + super(VCS_set_user_id_TestCase, self).setUp() + + if self.vcs_supports_uninitialized_user_id: + self.prev_user_id = self.vcs.get_user_id() + else: + self.prev_user_id = "Uninitialized identity " + + if self.vcs_supports_set_user_id: + self.test_new_user_id = "John Doe " + self.vcs.set_user_id(self.test_new_user_id) + + def tearDown(self): + if self.vcs_supports_set_user_id: + self.vcs.set_user_id(self.prev_user_id) + super(VCS_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.vcs_supports_set_user_id: + return + self.assertRaises( + SettingIDnotSupported, + self.vcs.set_user_id, "foo") + + def test_updates_user_id_in_supporting_vcs(self): + """Should update the user ID in an VCS that supports it.""" + if not self.vcs_supports_set_user_id: + return + user_id = self.vcs.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_vcs_revision_test_fixtures(testcase): + """Set up revision test fixtures for VCS test case.""" + testcase.test_dirs = ['a', 'a/b', 'c'] + for path in testcase.test_dirs: + testcase.vcs.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 VCS_mkdir_TestCase(VCSTestCase): + """Test cases for VCS.mkdir method.""" + + def setUp(self): + super(VCS_mkdir_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_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 VCS_commit_TestCase(VCSTestCase): + """Test cases for VCS.commit method.""" + + def setUp(self): + super(VCS_commit_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_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.vcs.set_file_contents(full_path, test_contents) + current_contents = self.vcs.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.vcs.set_file_contents(full_path, test_contents) + revision = self.vcs.commit("Initial file contents.") + current_contents = self.vcs.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.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial file contents.") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + current_contents = self.vcs.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.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial file contents.") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + committed_contents = self.vcs.get_file_contents( + full_path, revision) + self.failUnlessEqual( + self.test_contents['rev_1'], committed_contents) + + def test_revision_id_as_committed(self): + """Check for compatibility between .commit() and .revision_id()""" + if not self.vcs.versioned: + self.failUnlessEqual(self.vcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial %s contents." % path) + committed_revisions.append(revision) + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + revision = self.vcs.commit("Altered %s contents." % path) + committed_revisions.append(revision) + for i,revision in enumerate(committed_revisions): + self.failUnlessEqual(self.vcs.revision_id(i), revision) + i += -len(committed_revisions) # check negative indices + self.failUnlessEqual(self.vcs.revision_id(i), revision) + i = len(committed_revisions) + self.failUnlessEqual(self.vcs.revision_id(i), None) + self.failUnlessEqual(self.vcs.revision_id(-i-1), None) + + def test_revision_id_as_committed(self): + """Check revision id before first commit""" + if not self.vcs.versioned: + self.failUnlessEqual(self.vcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + self.failUnlessEqual(self.vcs.revision_id(0), None) + + +class VCS_duplicate_repo_TestCase(VCSTestCase): + """Test cases for VCS.duplicate_repo method.""" + + def setUp(self): + super(VCS_duplicate_repo_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + self.vcs.remove_duplicate_repo() + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_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.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Commit current status") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + dup_repo_path = self.vcs.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.vcs.remove_duplicate_repo() + + +def make_vcs_testcase_subclasses(vcs_class, namespace): + """Make VCSTestCase subclasses for vcs_class in the namespace.""" + vcs_testcase_classes = [ + c for c in ( + ob for ob in globals().values() if isinstance(ob, type)) + if issubclass(c, VCSTestCase)] + + for base_class in vcs_testcase_classes: + testcase_class_name = vcs_class.__name__ + base_class.__name__ + testcase_class_bases = (base_class,) + testcase_class_dict = dict(base_class.__dict__) + testcase_class_dict['Class'] = vcs_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/email/interactive/libbe/version.py b/interfaces/email/interactive/libbe/version.py new file mode 100644 index 0000000..f8eebbd --- /dev/null +++ b/interfaces/email/interactive/libbe/version.py @@ -0,0 +1,50 @@ +#!/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. + +""" +Store version info for this BE installation. By default, use the +bzr-generated information in _version.py, but allow manual overriding +by setting _VERSION. This allows support of both the "I don't want to +be bothered setting version strings" and the "I want complete control +over the version strings" workflows. +""" + +import libbe._version as _version + +# Manually set a version string (optional, defaults to bzr revision id) +#_VERSION = "1.2.3" + +def version(verbose=False): + """ + Returns the version string for this BE installation. If + verbose==True, the string will include extra lines with more + detail (e.g. bzr branch nickname, etc.). + """ + if "_VERSION" in globals(): + string = _VERSION + else: + string = _version.version_info["revision_id"] + if verbose == True: + string += ("\n" + "revision: %(revno)d\n" + "nick: %(branch_nick)s\n" + "revision id: %(revision_id)s" + % _version.version_info) + return string + +if __name__ == "__main__": + print version(verbose=True) diff --git a/interfaces/email/interactive/send_pgp_mime.py b/interfaces/email/interactive/send_pgp_mime.py new file mode 100644 index 0000000..c19483e --- /dev/null +++ b/interfaces/email/interactive/send_pgp_mime.py @@ -0,0 +1,611 @@ +#!/usr/bin/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. +""" +Python module and command line tool for sending pgp/mime email. + +Mostly uses subprocess to call gpg and a sendmail-compatible mailer. +If you lack gpg, either don't use the encryption functions or adjust +the pgp_* commands. You may need to adjust the sendmail command to +point to whichever sendmail-compatible mailer you have on your system. +""" + +from cStringIO import StringIO +import os +import re +#import GnuPGInterface # Maybe should use this instead of subprocess +import smtplib +import subprocess +import sys +import tempfile +import types + +try: + from email import Message + from email.mime.text import MIMEText + from email.mime.multipart import MIMEMultipart + from email.mime.application import MIMEApplication + from email.encoders import encode_7or8bit + from email.generator import Generator + from email.parser import Parser + from email.utils import getaddress +except ImportError: + # adjust to old python 2.4 + from email import Message + from email.MIMEText import MIMEText + from email.MIMEMultipart import MIMEMultipart + from email.MIMENonMultipart import MIMENonMultipart + from email.Encoders import encode_7or8bit + from email.Generator import Generator + from email.Parser import Parser + from email.Utils import getaddresses + + getaddress = getaddresses + class MIMEApplication (MIMENonMultipart): + def __init__(self, _data, _subtype, _encoder, **params): + MIMENonMultipart.__init__(self, 'application', _subtype, **params) + self.set_payload(_data) + _encoder(self) + +usage="""usage: %prog [options] + +Scriptable PGP MIME email using gpg. + +You can use gpg-agent for passphrase caching if your key requires a +passphrase (it better!). Example usage would be to install gpg-agent, +and then run + export GPG_TTY=`tty` + eval $(gpg-agent --daemon) +in your shell before invoking this script. See gpg-agent(1) for more +details. Alternatively, you can send your passphrase in on stdin + echo 'passphrase' | %prog [options] +or use the --passphrase-file option + %prog [options] --passphrase-file FILE [more options] +Both of these alternatives are much less secure than gpg-agent. You +have been warned. +""" + +verboseInvoke = False +PGP_SIGN_AS = None +PASSPHRASE = None + +# The following commands are adapted from my .mutt/pgp configuration +# +# Printf-like sequences: +# %a The value of PGP_SIGN_AS. +# %f Expands to the name of a file with text to be signed/encrypted. +# %p Expands to the passphrase argument. +# %R A string with some number (0 on up) of pgp_reciepient_arg +# strings. +# %r One key ID (e.g. recipient email address) to build a +# pgp_reciepient_arg string. +# +# The above sequences can be used to optionally print a string if +# their length is nonzero. For example, you may only want to pass the +# -u/--local-user argument to gpg if PGP_SIGN_AS is defined. To +# optionally print a string based upon one of the above sequences, the +# following construct is used +# %??? +# where sequence_char is a character from the table above, and +# optional_string is the string you would like printed if status_char +# is nonzero. optional_string may contain other sequence as well as +# normal text, but it may not contain any question marks. +# +# see http://codesorcery.net/old/mutt/mutt-gnupg-howto +# http://www.mutt.org/doc/manual/manual-6.html#pgp_autosign +# http://tldp.org/HOWTO/Mutt-GnuPG-PGP-HOWTO-8.html +# for more details + +pgp_recipient_arg='-r "%r"' +pgp_stdin_passphrase_arg='--passphrase-fd 0' +pgp_sign_command='/usr/bin/gpg --no-verbose --quiet --batch %p --output - --detach-sign --armor --textmode %?a?-u "%a"? %f' +pgp_encrypt_only_command='/usr/bin/gpg --no-verbose --quiet --batch --output - --encrypt --armor --textmode --always-trust --encrypt-to "%a" %R -- %f' +pgp_encrypt_sign_command='/usr/bin/gpg --no-verbose --quiet --batch %p --output - --encrypt --sign %?a?-u "%a"? --armor --textmode --always-trust --encrypt-to "%a" %R -- %f' +sendmail='/usr/sbin/sendmail -t' + +def mail(msg, sendmail=None): + """ + Send an email Message instance on its merry way. + + We can shell out to the user specified sendmail in case + the local host doesn't have an SMTP server set up + for easy smtplib usage. + """ + if sendmail != None: + execute(sendmail, stdin=flatten(msg)) + return None + s = smtplib.SMTP() + s.connect() + s.sendmail(from_addr=source_email(msg), + to_addrs=target_emails(msg), + msg=flatten(msg)) + s.close() + +def header_from_text(text, encoding="us-ascii"): + """ + Simple wrapper for instantiating an email.Message from text. + >>> header = header_from_text('\\n'.join(['From: me@big.edu','To: you@big.edu','Subject: testing'])) + >>> print flatten(header) + From: me@big.edu + To: you@big.edu + Subject: testing + + + """ + text = text.strip() + if type(text) == types.UnicodeType: + text = text.encode(encoding) + # assume StringType arguments are already encoded + p = Parser() + return p.parsestr(text, headersonly=True) + +def guess_encoding(text): + if type(text) == types.StringType: + encoding = "us-ascii" + elif type(text) == types.UnicodeType: + for encoding in ["us-ascii", "iso-8859-1", "utf-8"]: + try: + text.encode(encoding) + except UnicodeError: + pass + else: + break + assert encoding != None + return encoding + +def encodedMIMEText(body, encoding=None): + if encoding == None: + encoding = guess_encoding(body) + if encoding == "us-ascii": + return MIMEText(body) + else: + # Create the message ('plain' stands for Content-Type: text/plain) + return MIMEText(body.encode(encoding), 'plain', encoding) + +def append_text(text_part, new_text): + original_payload = text_part.get_payload(decode=True) + new_payload = u"%s%s" % (original_payload, new_text) + new_encoding = guess_encoding(new_payload) + text_part.set_payload(new_payload.encode(new_encoding), new_encoding) + +def attach_root(header, root_part): + """ + Attach the email.Message root_part to the email.Message header + without generating a multi-part message. + """ + for k,v in header.items(): + root_part[k] = v + return root_part + +def execute(args, stdin=None, expect=(0,)): + """ + Execute a command (allows us to drive gpg). + """ + if verboseInvoke == True: + print >> sys.stderr, '$ '+args + try: + p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, close_fds=True) + except OSError, e: + strerror = '%s\nwhile executing %s' % (e.args[1], args) + raise Exception, strerror + output, error = p.communicate(input=stdin) + status = p.wait() + if verboseInvoke == True: + print >> sys.stderr, '(status: %d)\n%s%s' % (status, output, error) + if status not in expect: + strerror = '%s\nwhile executing %s\n%s\n%d' % (args[1], args, error, status) + raise Exception, strerror + return status, output, error + +def replace(template, format_char, replacement_text): + """ + >>> replace('--textmode %?a?-u %a? %f', 'f', 'file.in') + '--textmode %?a?-u %a? file.in' + >>> replace('--textmode %?a?-u %a? %f', 'a', '0xHEXKEY') + '--textmode -u 0xHEXKEY %f' + >>> replace('--textmode %?a?-u %a? %f', 'a', '') + '--textmode %f' + """ + if replacement_text == None: + replacement_text = "" + regexp = re.compile('%[?]'+format_char+'[?]([^?]*)[?]') + if len(replacement_text) > 0: + str = regexp.sub('\g<1>', template) + else: + str = regexp.sub('', template) + regexp = re.compile('%'+format_char) + str = regexp.sub(replacement_text, str) + return str + +def flatten(msg, to_unicode=False): + """ + Produce flat text output from an email Message instance. + """ + assert msg != None + fp = StringIO() + g = Generator(fp, mangle_from_=False) + g.flatten(msg) + text = fp.getvalue() + if to_unicode == True: + encoding = msg.get_content_charset() or "utf-8" + text = unicode(text, encoding=encoding) + return text + +def source_email(msg, return_realname=False): + """ + Search the header of an email Message instance to find the + sender's email address. + """ + froms = msg.get_all('from', []) + from_tuples = getaddresses(froms) # [(realname, email_address), ...] + assert len(from_tuples) == 1 + if return_realname == True: + return from_tuples[0] # (realname, email_address) + return from_tuples[0][1] # email_address + +def target_emails(msg): + """ + Search the header of an email Message instance to find a + list of recipient's email addresses. + """ + tos = msg.get_all('to', []) + ccs = msg.get_all('cc', []) + bccs = msg.get_all('bcc', []) + resent_tos = msg.get_all('resent-to', []) + resent_ccs = msg.get_all('resent-cc', []) + resent_bccs = msg.get_all('resent-bcc', []) + all_recipients = getaddresses(tos + ccs + bccs + resent_tos + + resent_ccs + resent_bccs) + return [addr[1] for addr in all_recipients] + +class PGPMimeMessageFactory (object): + """ + See http://www.ietf.org/rfc/rfc3156.txt for specification details. + >>> from_addr = "me@big.edu" + >>> to_addr = "you@you.edu" + >>> header = header_from_text('\\n'.join(['From: %s'%from_addr,'To: %s'%to_addr,'Subject: testing'])) + >>> source_email(header) == from_addr + True + >>> target_emails(header) == [to_addr] + True + >>> m = PGPMimeMessageFactory('check 1 2\\ncheck 1 2\\n') + >>> print flatten(m.clearBodyPart()) + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Disposition: inline + + check 1 2 + check 1 2 + + >>> print flatten(m.plain()) + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + + check 1 2 + check 1 2 + + >>> signed = m.sign(header) + >>> signed.set_boundary('boundsep') + >>> print flatten(signed).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + Content-Type: multipart/signed; protocol="application/pgp-signature"; + micalg="pgp-sha1"; boundary="boundsep" + MIME-Version: 1.0 + Content-Disposition: inline + + --boundsep + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Disposition: inline + + check 1 2 + check 1 2 + + --boundsep + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Description: signature + Content-Type: application/pgp-signature; name="signature.asc"; + charset="us-ascii" + + -----BEGIN PGP SIGNATURE----- + ... + -----END PGP SIGNATURE----- + + --boundsep-- + >>> encrypted = m.encrypt(header) + >>> encrypted.set_boundary('boundsep') + >>> print flatten(encrypted).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + micalg="pgp-sha1"; boundary="boundsep" + MIME-Version: 1.0 + Content-Disposition: inline + + --boundsep + Content-Type: application/pgp-encrypted + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + + Version: 1 + + --boundsep + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Type: application/octet-stream; charset="us-ascii" + + -----BEGIN PGP MESSAGE----- + ... + -----END PGP MESSAGE----- + + --boundsep-- + >>> signedAndEncrypted = m.signAndEncrypt(header) + >>> signedAndEncrypted.set_boundary('boundsep') + >>> print flatten(signedAndEncrypted).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + micalg="pgp-sha1"; boundary="boundsep" + MIME-Version: 1.0 + Content-Disposition: inline + + --boundsep + Content-Type: application/pgp-encrypted + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + + Version: 1 + + --boundsep + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Type: application/octet-stream; charset="us-ascii" + + -----BEGIN PGP MESSAGE----- + ... + -----END PGP MESSAGE----- + + --boundsep-- + """ + def __init__(self, body): + self.body = body + def clearBodyPart(self): + body = encodedMIMEText(self.body) + body.add_header('Content-Disposition', 'inline') + return body + def passphrase_arg(self, passphrase=None): + if passphrase == None and PASSPHRASE != None: + passphrase = PASSPHRASE + if passphrase == None: + return (None,'') + return (passphrase, pgp_stdin_passphrase_arg) + def plain(self): + """ + text/plain + """ + return encodedMIMEText(self.body) + def sign(self, header, passphrase=None): + """ + multipart/signed + +-> text/plain (body) + +-> application/pgp-signature (signature) + """ + passphrase,pass_arg = self.passphrase_arg(passphrase) + body = self.clearBodyPart() + bfile = tempfile.NamedTemporaryFile() + bfile.write(flatten(body)) + bfile.flush() + + args = replace(pgp_sign_command, 'f', bfile.name) + if PGP_SIGN_AS == None: + pgp_sign_as = '<%s>' % source_email(header) + else: + pgp_sign_as = PGP_SIGN_AS + args = replace(args, 'a', pgp_sign_as) + args = replace(args, 'p', pass_arg) + status,output,error = execute(args, stdin=passphrase) + signature = output + + sig = MIMEApplication(_data=signature, + _subtype='pgp-signature; name="signature.asc"', + _encoder=encode_7or8bit) + sig['Content-Description'] = 'signature' + sig.set_charset('us-ascii') + + msg = MIMEMultipart('signed', micalg='pgp-sha1', + protocol='application/pgp-signature') + msg.attach(body) + msg.attach(sig) + + msg['Content-Disposition'] = 'inline' + return msg + def encrypt(self, header, passphrase=None): + """ + multipart/encrypted + +-> application/pgp-encrypted (control information) + +-> application/octet-stream (body) + """ + body = self.clearBodyPart() + bfile = tempfile.NamedTemporaryFile() + bfile.write(flatten(body)) + bfile.flush() + + recipients = [replace(pgp_recipient_arg, 'r', recipient) + for recipient in target_emails(header)] + recipient_string = ' '.join(recipients) + args = replace(pgp_encrypt_only_command, 'R', recipient_string) + args = replace(args, 'f', bfile.name) + if PGP_SIGN_AS == None: + pgp_sign_as = '<%s>' % source_email(header) + else: + pgp_sign_as = PGP_SIGN_AS + args = replace(args, 'a', pgp_sign_as) + status,output,error = execute(args) + encrypted = output + + enc = MIMEApplication(_data=encrypted, _subtype='octet-stream', + _encoder=encode_7or8bit) + enc.set_charset('us-ascii') + + control = MIMEApplication(_data='Version: 1\n', _subtype='pgp-encrypted', + _encoder=encode_7or8bit) + + msg = MIMEMultipart('encrypted', micalg='pgp-sha1', + protocol='application/pgp-encrypted') + msg.attach(control) + msg.attach(enc) + + msg['Content-Disposition'] = 'inline' + return msg + def signAndEncrypt(self, header, passphrase=None): + """ + multipart/encrypted + +-> application/pgp-encrypted (control information) + +-> application/octet-stream (body) + """ + passphrase,pass_arg = self.passphrase_arg(passphrase) + body = self.sign(header, passphrase) + body.__delitem__('Bcc') + bfile = tempfile.NamedTemporaryFile() + bfile.write(flatten(body)) + bfile.flush() + + recipients = [replace(pgp_recipient_arg, 'r', recipient) + for recipient in target_emails(header)] + recipient_string = ' '.join(recipients) + args = replace(pgp_encrypt_only_command, 'R', recipient_string) + args = replace(args, 'f', bfile.name) + if PGP_SIGN_AS == None: + pgp_sign_as = '<%s>' % source_email(header) + else: + pgp_sign_as = PGP_SIGN_AS + args = replace(args, 'a', pgp_sign_as) + args = replace(args, 'p', pass_arg) + status,output,error = execute(args, stdin=passphrase) + encrypted = output + + enc = MIMEApplication(_data=encrypted, _subtype='octet-stream', + _encoder=encode_7or8bit) + enc.set_charset('us-ascii') + + control = MIMEApplication(_data='Version: 1\n', + _subtype='pgp-encrypted', + _encoder=encode_7or8bit) + + msg = MIMEMultipart('encrypted', micalg='pgp-sha1', + protocol='application/pgp-encrypted') + msg.attach(control) + msg.attach(enc) + + msg['Content-Disposition'] = 'inline' + return msg + +def test(): + import doctest + doctest.testmod() + + +if __name__ == '__main__': + from optparse import OptionParser + + parser = OptionParser(usage=usage) + parser.add_option('-t', '--test', dest='test', action='store_true', + help='Run doctests and exit') + + parser.add_option('-H', '--header-file', dest='header_filename', + help='file containing email header', metavar='FILE') + parser.add_option('-B', '--body-file', dest='body_filename', + help='file containing email body', metavar='FILE') + + parser.add_option('-P', '--passphrase-file', dest='passphrase_file', + help='file containing gpg passphrase', metavar='FILE') + parser.add_option('-p', '--passphrase-fd', dest='passphrase_fd', + help='file descriptor from which to read gpg passphrase (0 for stdin)', + type="int", metavar='DESCRIPTOR') + + parser.add_option('--mode', dest='mode', default='sign', + help="One of 'sign', 'encrypt', 'sign-encrypt', or 'plain'. Defaults to %default.", + metavar='MODE') + + parser.add_option('-a', '--sign-as', dest='sign_as', + help="The gpg key to sign with (gpg's -u/--local-user)", + metavar='KEY') + + parser.add_option('--output', dest='output', action='store_true', + help="Don't mail the generated message, print it to stdout instead.") + + (options, args) = parser.parse_args() + + stdin_used = False + + if options.passphrase_file != None: + PASSPHRASE = file(options.passphrase_file, 'r').read() + elif options.passphrase_fd != None: + if options.passphrase_fd == 0: + stdin_used = True + PASSPHRASE = sys.stdin.read() + else: + PASSPHRASE = os.read(options.passphrase_fd) + + if options.sign_as: + PGP_SIGN_AS = options.sign_as + + if options.test == True: + test() + sys.exit(0) + + header = None + if options.header_filename != None: + if options.header_filename == '-': + assert stdin_used == False + stdin_used = True + header = sys.stdin.read() + else: + header = file(options.header_filename, 'r').read() + if header == None: + raise Exception, "missing header" + headermsg = header_from_text(header) + body = None + if options.body_filename != None: + if options.body_filename == '-': + assert stdin_used == False + stdin_used = True + body = sys.stdin.read() + else: + body = file(options.body_filename, 'r').read() + if body == None: + raise Exception, "missing body" + + m = PGPMimeMessageFactory(body) + if options.mode == "sign": + bodymsg = m.sign(header) + elif options.mode == "encrypt": + bodymsg = m.encrypt(header) + elif options.mode == "sign-encrypt": + bodymsg = m.signAndEncrypt(header) + elif options.mode == "plain": + bodymsg = m.plain() + else: + print "Unrecognized mode '%s'" % options.mode + + message = attach_root(headermsg, bodymsg) + if options.output == True: + message = flatten(message) + print message + else: + mail(message, sendmail) diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py b/interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py index a0d0ff9..50cc754 100644 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py +++ b/interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py @@ -119,9 +119,9 @@ class Bug(PrestHandler): assigned = None bug.assigned = assigned bug.save() -# bug.rcs.precommit(bug.path) -# bug.rcs.commit(bug.path, "Auto-commit") -# bug.rcs.postcommit(bug.path) +# bug.vcs.precommit(bug.path) +# bug.vcs.commit(bug.path, "Auto-commit") +# bug.vcs.postcommit(bug.path) raise cherrypy.HTTPRedirect(bug_list_url(bug_data["project"])) def instantiate(self, project, bug): diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/arch.py b/interfaces/web/Bugs-Everywhere-Web/libbe/arch.py index 2f45aa9..ab55172 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/arch.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/arch.py @@ -17,6 +17,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +GNU Arch (tla) backend. +""" + import codecs import os import re @@ -26,10 +30,11 @@ import time import unittest import doctest -import config from beuuid import uuid_gen -import rcs -from rcs import RCS +import config +import vcs + + DEFAULT_CLIENT = "tla" @@ -38,7 +43,7 @@ client = config.get_val("arch_client", default=DEFAULT_CLIENT) def new(): return Arch() -class Arch(RCS): +class Arch(vcs.VCS): name = "Arch" client = client versioned = True @@ -48,21 +53,25 @@ class Arch(RCS): _project_name = None _tmp_project = False _arch_paramdir = os.path.expanduser("~/.arch-params") - def _rcs_help(self): + def _vcs_help(self): status,output,error = self._u_invoke_client("--help") return output - def _rcs_detect(self, path): + def _vcs_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): + def _vcs_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 + """ + Create a temporary Arch archive in the directory PATH. This + archive will be removed by + __del__->cleanup->_vcs_cleanup->_remove_archive + """ # http://regexps.srparish.net/tutorial-tla/new-archive.html#Creating_a_New_Archive assert self._archive_name == None id = self.get_user_id() @@ -103,7 +112,7 @@ class Arch(RCS): """ Create a temporary Arch project in the directory PATH. This project will be removed by - __del__->cleanup->_rcs_cleanup->_remove_project + __del__->cleanup->_vcs_cleanup->_remove_project """ # http://mwolson.org/projects/GettingStartedWithArch.html # http://regexps.srparish.net/tutorial-tla/new-project.html#Starting_a_New_Project @@ -159,13 +168,13 @@ class Arch(RCS): self._adjust_naming_conventions(path) self._invoke_client("import", "--summary", "Began versioning", directory=path) - def _rcs_cleanup(self): + def _vcs_cleanup(self): if self._tmp_project == True: self._remove_project() if self._tmp_archive == True: self._remove_archive() - def _rcs_root(self, path): + def _vcs_root(self, path): if not os.path.isdir(path): dirname = os.path.dirname(path) else: @@ -176,7 +185,6 @@ class Arch(RCS): 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') @@ -188,7 +196,6 @@ class Arch(RCS): 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) @@ -197,7 +204,7 @@ class Arch(RCS): archive_name,project_name = output.rstrip('\n').split('/') self._archive_name = archive_name self._project_name = project_name - def _rcs_get_user_id(self): + def _vcs_get_user_id(self): try: status,output,error = self._u_invoke_client('my-id') return output.rstrip('\n') @@ -206,9 +213,9 @@ class Arch(RCS): return None else: raise - def _rcs_set_user_id(self, value): + def _vcs_set_user_id(self, value): self._u_invoke_client('my-id', value) - def _rcs_add(self, path): + def _vcs_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) @@ -237,14 +244,14 @@ class Arch(RCS): 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): + def _vcs_remove(self, path): if not '.arch-ids' in path: self._u_invoke_client("delete-id", path) - def _rcs_update(self, path): + def _vcs_update(self, path): pass - def _rcs_get_file_contents(self, path, revision=None, binary=False): + def _vcs_get_file_contents(self, path, revision=None, binary=False): if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) else: status,output,error = \ self._invoke_client("file-find", path, revision) @@ -254,18 +261,18 @@ class Arch(RCS): contents = f.read() f.close() return contents - def _rcs_duplicate_repo(self, directory, revision=None): + def _vcs_duplicate_repo(self, directory, revision=None): if revision == None: - RCS._rcs_duplicate_repo(self, directory, revision) + vcs.VCS._vcs_duplicate_repo(self, directory, revision) else: status,output,error = \ self._u_invoke_client("get", revision,directory) - def _rcs_commit(self, commitfile, allow_empty=False): + def _vcs_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() + raise vcs.EmptyCommit() summary,body = self._u_parse_commitfile(commitfile) args = ["commit", "--summary", summary] if body != None: @@ -281,6 +288,16 @@ class Arch(RCS): assert revpath.startswith(self._archive_project_name()+'--') revision = revpath[len(self._archive_project_name()+'--'):] return revpath + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("logs") + logs = output.splitlines() + first_log = logs.pop(0) + assert first_log == "base-0", first_log + try: + log = logs[index] + except IndexError: + return None + return "%s--%s" % (self._archive_project_name(), log) class CantAddFile(Exception): def __init__(self, file): @@ -289,7 +306,7 @@ class CantAddFile(Exception): -rcs.make_rcs_testcase_subclasses(Arch, sys.modules[__name__]) +vcs.make_vcs_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 index bc47208..490ed62 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/beuuid.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/beuuid.py @@ -13,6 +13,7 @@ # 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 @@ -20,6 +21,7 @@ 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(): diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/bug.py b/interfaces/web/Bugs-Everywhere-Web/libbe/bug.py index c1e5481..fd30ff7 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/bug.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/bug.py @@ -15,6 +15,11 @@ # 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. + +""" +Define the Bug class for representing bugs. +""" + import os import os.path import errno @@ -33,6 +38,11 @@ import comment import utility +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) + ### 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/ @@ -216,15 +226,15 @@ class Bug(settings_object.SavedSettingsObject): @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 + def _get_vcs(self): + if hasattr(self.bugdir, "vcs"): + return self.bugdir.vcs @Property - @cached_property(generator=_get_rcs) - @local_property("rcs") + @cached_property(generator=_get_vcs) + @local_property("vcs") @doc_property(doc="A revision control system instance.") - def rcs(): return {} + def vcs(): return {} def __init__(self, bugdir=None, uuid=None, from_disk=False, load_comments=False, summary=None): @@ -238,17 +248,20 @@ class Bug(settings_object.SavedSettingsObject): 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() + if self.vcs != None: + self.creator = self.vcs.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 __str__(self): + return self.string(shortlist=True) + + def __cmp__(self, other): + return cmp_full(self, other) + + # serializing methods def _setting_attr_string(self, setting): value = getattr(self, setting) @@ -331,43 +344,34 @@ class Bug(settings_object.SavedSettingsObject): output = bugout return output - def __str__(self): - return self.string(shortlist=True) + # methods for saving/loading/acessing settings and properties. - def __cmp__(self, other): - return cmp_full(self, other) + def get_path(self, *args): + dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid) + if len(args) == 0: + return dir + assert args[0] in ["values", "comments"], str(args) + return os.path.join(dir, *args) - 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 set_sync_with_disk(self, value): + self.sync_with_disk = value + for comment in self.comments(): + comment.set_sync_with_disk(value) def load_settings(self): - self.settings = mapfile.map_load(self.rcs, self.get_path("values")) + if self.sync_with_disk == False: + raise DiskAccessRequired("load settings") + self.settings = mapfile.map_load(self.vcs, 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): + if self.sync_with_disk == False: + raise DiskAccessRequired("save settings") assert self.summary != None, "Can't save blank bug" - - self.rcs.mkdir(self.get_path()) + self.vcs.mkdir(self.get_path()) path = self.get_path("values") - mapfile.map_save(self.rcs, path, self._get_saved_settings()) - + mapfile.map_save(self.vcs, path, self._get_saved_settings()) + def save(self): """ Save any loaded contents to disk. Because of lazy loading of @@ -378,15 +382,39 @@ class Bug(settings_object.SavedSettingsObject): calling this method will just waste time (unless something else has been messing with your on-disk files). """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) self.save_settings() if len(self.comment_root) > 0: comment.saveComments(self) + if sync_with_disk == False: + self.set_sync_with_disk(False) + + def load_comments(self, load_full=True): + if self.sync_with_disk == False: + raise DiskAccessRequired("load comments") + 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 remove(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("remove") self.comment_root.remove() path = self.get_path() - self.rcs.recursive_remove(path) + self.vcs.recursive_remove(path) + # methods for managing comments + def comments(self): for comment in self.comment_root.traverse(): yield comment @@ -489,8 +517,12 @@ def cmp_attr(bug_1, bug_2, attr, invert=False): return cmp(val_1, val_2) # alphabetical rankings (a < z) +cmp_uuid = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "uuid") 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") +cmp_target = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "target") +cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter") +cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary") # chronological rankings (newer < older) cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True) @@ -512,7 +544,8 @@ def cmp_comments(bug_1, bug_2): return 0 DEFAULT_CMP_FULL_CMP_LIST = \ - (cmp_status,cmp_severity,cmp_assigned,cmp_time,cmp_creator,cmp_comments) + (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator, + cmp_reporter, cmp_target, cmp_comments, cmp_summary, cmp_uuid) class BugCompoundComparator (object): def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/bugdir.py b/interfaces/web/Bugs-Everywhere-Web/libbe/bugdir.py index 6e020ee..c4f0f91 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/bugdir.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/bugdir.py @@ -17,23 +17,30 @@ # 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. + +""" +Define the BugDir class for representing bug comments. +""" + +import copy +import errno import os import os.path -import errno +import sys import time -import copy import unittest import doctest +import bug +import encoding 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 vcs +import settings_object +import upgrade import utility @@ -62,8 +69,16 @@ class MultipleBugMatches(ValueError): self.shortname = shortname self.matches = matches +class NoBugMatches(KeyError): + def __init__(self, shortname): + msg = "No bug matches %s" % shortname + KeyError.__init__(self, msg) + self.shortname = shortname -TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n" +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) class BugDir (list, settings_object.SavedSettingsObject): @@ -99,7 +114,8 @@ class BugDir (list, settings_object.SavedSettingsObject): 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. + changed in memory, a call to save() immediately before the + .set_sync_with_disk(True) call 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. @@ -107,15 +123,14 @@ class BugDir (list, settings_object.SavedSettingsObject): 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)). + loads new settings/bugs/comments that it doesn't already have in + memory and .sync_with_disk == True. - Allow RCS initialization + Allow VCS 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 + BugDir to search for an installed VCS backend and initialize it in the root directory. This is a convenience option for supporting tests of versioning functionality (e.g. .duplicate_bugdir). @@ -172,9 +187,9 @@ class BugDir (list, settings_object.SavedSettingsObject): def encoding(): return {} def _setup_user_id(self, user_id): - self.rcs.user_id = user_id + self.vcs.user_id = user_id def _guess_user_id(self): - return self.rcs.get_user_id() + return self.vcs.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) @@ -182,7 +197,7 @@ class BugDir (list, settings_object.SavedSettingsObject): @_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.""", +that the Arch VCS backend *enforces* ids with this format.""", change_hook=_set_user_id, generator=_guess_user_id) def user_id(): return {} @@ -192,32 +207,32 @@ that the Arch RCS backend *enforces* ids with this format.""", """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.""", + @_versioned_property(name="vcs_name", + doc="""The name of the current VCS. Kept seperate to make saving/loading +settings easy. Don't set this attribute. Set .vcs instead, and +.vcs_name will be automatically adjusted.""", default="None", allowed=["None", "Arch", "bzr", "darcs", "git", "hg"]) - def rcs_name(): return {} + def vcs_name(): return {} - def _get_rcs(self, rcs_name=None): + def _get_vcs(self, vcs_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 + if vcs_name == None: + vcs_name = self.vcs_name + new_vcs = vcs.vcs_by_name(vcs_name) + self._change_vcs(None, new_vcs) + return new_vcs + def _change_vcs(self, old_vcs, new_vcs): + new_vcs.encoding = self.encoding + new_vcs.root(self.root) + self.vcs_name = new_vcs.name @Property - @change_hook_property(hook=_change_rcs) - @cached_property(generator=_get_rcs) - @local_property("rcs") + @change_hook_property(hook=_change_vcs) + @cached_property(generator=_get_vcs) + @local_property("vcs") @doc_property(doc="A revision control system instance.") - def rcs(): return {} + def vcs(): return {} def _bug_map_gen(self): map = {} @@ -279,9 +294,8 @@ settings easy. Don't set this attribute. Set .rcs instead, and 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): + assert_new_BugDir=False, allow_vcs_init=False, + manipulate_encodings=True, from_disk=False, vcs=None): list.__init__(self) settings_object.SavedSettingsObject.__init__(self) self._manipulate_encodings = manipulate_encodings @@ -293,9 +307,9 @@ settings easy. Don't set this attribute. Set .rcs instead, and if not os.path.exists(root): raise NoRootEntry(root) self.root = root - # get a temporary rcs until we've loaded settings + # get a temporary vcs until we've loaded settings self.sync_with_disk = False - self.rcs = self._guess_rcs() + self.vcs = self._guess_vcs() if from_disk == True: self.sync_with_disk = True @@ -305,20 +319,24 @@ settings easy. Don't set this attribute. Set .rcs instead, and 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 + if vcs == None: + vcs = self._guess_vcs(allow_vcs_init) + self.vcs = vcs 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 __del__(self): + self.cleanup() + + def cleanup(self): + self.vcs.cleanup() + + # methods for getting the BugDir situated in the filesystem def _find_root(self, path): """ Search for an existing bug database dir and it's ancestors and - return a BugDir rooted there. + return a BugDir rooted there. Only called by __init__, and + then only if sink_to_existing_root == True. """ if not os.path.exists(path): raise NoRootEntry(path) @@ -334,136 +352,212 @@ settings easy. Don't set this attribute. Set .rcs instead, and 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() + def _guess_vcs(self, allow_vcs_init=False): + """ + Only called by __init__. + """ + deepdir = self.get_path() + if not os.path.exists(deepdir): + deepdir = os.path.dirname(deepdir) + new_vcs = vcs.detect_vcs(deepdir) + install = False + if new_vcs.name == "None": + if allow_vcs_init == True: + new_vcs = vcs.installed_vcs() + new_vcs.init(self.root) + return new_vcs + + # methods for saving/loading/accessing settings and properties. + + def get_path(self, *args): + """ + Return a path relative to .root. + """ + dir = os.path.join(self.root, ".be") + if len(args) == 0: + return dir + assert args[0] in ["version", "settings", "bugs"], str(args) + return os.path.join(dir, *args) + + def _get_settings(self, settings_path, for_duplicate_bugdir=False): + allow_no_vcs = not self.vcs.path_in_root(settings_path) + if allow_no_vcs == True: + assert for_duplicate_bugdir == True + if self.sync_with_disk == False and for_duplicate_bugdir == False: + # duplicates can ignore this bugdir's .sync_with_disk status + raise DiskAccessRequired("_get settings") + try: + settings = mapfile.map_load(self.vcs, settings_path, allow_no_vcs) + except vcs.NoSuchFile: + settings = {"vcs_name": "None"} + return settings + + def _save_settings(self, settings_path, settings, + for_duplicate_bugdir=False): + allow_no_vcs = not self.vcs.path_in_root(settings_path) + if allow_no_vcs == True: + assert for_duplicate_bugdir == True + if self.sync_with_disk == False and for_duplicate_bugdir == False: + # duplicates can ignore this bugdir's .sync_with_disk status + raise DiskAccessRequired("_save settings") + self.vcs.mkdir(self.get_path(), allow_no_vcs) + mapfile.map_save(self.vcs, settings_path, settings, allow_no_vcs) + + 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) + self.vcs = vcs.vcs_by_name(self.vcs_name) + self._setup_user_id(self.user_id) + + def save_settings(self): + settings = self._get_saved_settings() + self._save_settings(self.get_path("settings"), settings) + + def get_version(self, path=None, use_none_vcs=False, + for_duplicate_bugdir=False): + """ + Requires disk access. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("get version") + if use_none_vcs == True: + VCS = vcs.vcs_by_name("None") + VCS.root(self.root) + VCS.encoding = encoding.get_encoding() else: - RCS = self.rcs + VCS = self.vcs if path == None: path = self.get_path("version") - tree_version = RCS.get_file_contents(path) - return tree_version + allow_no_vcs = not VCS.path_in_root(path) + if allow_no_vcs == True: + assert for_duplicate_bugdir == True + version = VCS.get_file_contents( + path, allow_no_vcs=allow_no_vcs).rstrip("\n") + return version def set_version(self): - self.rcs.mkdir(self.get_path()) - self.rcs.set_file_contents(self.get_path("version"), - TREE_VERSION_STRING) + """ + Requires disk access. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("set version") + self.vcs.mkdir(self.get_path()) + self.vcs.set_file_contents(self.get_path("version"), + upgrade.BUGDIR_DISK_VERSION+"\n") - 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) + # methods controlling disk access - 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 set_sync_with_disk(self, value): + """ + Adjust .sync_with_disk for the BugDir and all it's children. + See the BugDir docstring for a description of the role of + .sync_with_disk. + """ + self.sync_with_disk = value + for bug in self: + bug.set_sync_with_disk(value) 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 + """ + Reqires disk access + """ + version = self.get_version(use_none_vcs=True) + if version != upgrade.BUGDIR_DISK_VERSION: + upgrade.upgrade(self.root, 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." + """ + Requires disk access. + Warning: this could take a while. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("load all bugs") self._clear_bugs() for uuid in self.list_uuids(): self._load_bug(uuid) def save(self): """ + Note that this command writes to disk _regardless_ of the + status of .sync_with_disk. + 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 + However, if .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). + + Requires disk access. """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) self.set_version() self.save_settings() for bug in self: bug.save() + if sync_with_disk == False: + self.set_sync_with_disk(sync_with_disk) - 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) + # methods for managing duplicate BugDirs - 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 + def duplicate_bugdir(self, revision): + duplicate_path = self.vcs.duplicate_repo(revision) + duplicate_version_path = os.path.join(duplicate_path, ".be", "version") 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) + version = self.get_version(duplicate_version_path, + for_duplicate_bugdir=True) + except DiskAccessRequired: + self.sync_with_disk = True # temporarily allow access + version = self.get_version(duplicate_version_path, + for_duplicate_bugdir=True) + self.sync_with_disk = False + if version != upgrade.BUGDIR_DISK_VERSION: + upgrade.upgrade(duplicate_path, version) - # setup revision RCS as None, since the duplicate may not be + # setup revision VCS 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 = self._get_settings(duplicate_settings_path, + for_duplicate_bugdir=True) + if "vcs_name" in duplicate_settings: + duplicate_settings["vcs_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) + self._save_settings(duplicate_settings_path, duplicate_settings, + for_duplicate_bugdir=True) return BugDir(duplicate_path, from_disk=True, manipulate_encodings=self._manipulate_encodings) def remove_duplicate_bugdir(self): - self.rcs.remove_duplicate_repo() + self.vcs.remove_duplicate_repo() + + # methods for managing bugs def list_uuids(self): uuids = [] - if os.path.exists(self.get_path()): + if self.sync_with_disk == True and 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 + if os.path.exists(self.get_path("bugs")): + 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: @@ -476,6 +570,8 @@ settings easy. Don't set this attribute. Set .rcs instead, and self._bug_map_gen() def _load_bug(self, uuid): + if self.sync_with_disk == False: + raise DiskAccessRequired("_load bug") bg = bug.Bug(bugdir=self, uuid=uuid, from_disk=True) self.append(bg) self._bug_map_gen() @@ -492,7 +588,8 @@ settings easy. Don't set this attribute. Set .rcs instead, and def remove_bug(self, bug): self.remove(bug) - bug.remove() + if bug.sync_with_disk == True: + bug.remove() def bug_shortname(self, bug): """ @@ -514,12 +611,13 @@ settings easy. Don't set this attribute. Set .rcs instead, and def bug_from_shortname(self, shortname): """ - >>> bd = simple_bug_dir() + >>> bd = SimpleBugDir(sync_with_disk=False) >>> bug_a = bd.bug_from_shortname('a') >>> print type(bug_a) >>> print bug_a a:om: Bug A + >>> bd.cleanup() """ matches = [] self._bug_map_gen() @@ -530,7 +628,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and raise MultipleBugMatches(shortname, matches) if len(matches) == 1: return self.bug_from_uuid(matches[0]) - raise KeyError("No bug matches %s" % shortname) + raise NoBugMatches(shortname) def bug_from_uuid(self, uuid): if not self.has_bug(uuid): @@ -548,41 +646,56 @@ settings easy. Don't set this attribute. Set .rcs instead, and return True -def simple_bug_dir(): +class SimpleBugDir (BugDir): """ - For testing - >>> bugdir = simple_bug_dir() - >>> ls = list(bugdir.list_uuids()) - >>> ls.sort() - >>> print ls + For testing. Set sync_with_disk==False for a memory-only bugdir. + >>> bugdir = SimpleBugDir() + >>> uuids = list(bugdir.list_uuids()) + >>> uuids.sort() + >>> print uuids ['a', 'b'] + >>> bugdir.cleanup() """ - dir = utility.Dir() - assert os.path.exists(dir.path) - bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True, + def __init__(self, sync_with_disk=True): + if sync_with_disk == True: + dir = utility.Dir() + assert os.path.exists(dir.path) + root = dir.path + assert_new_BugDir = True + vcs_init = True + else: + root = "/" + assert_new_BugDir = False + vcs_init = False + BugDir.__init__(self, root, sink_to_existing_root=False, + assert_new_BugDir=assert_new_BugDir, + allow_vcs_init=vcs_init, 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 - + if sync_with_disk == True: # postpone cleanup since dir.__del__() removes dir. + self._dir_ref = dir + bug_a = self.new_bug("a", summary="Bug A") + bug_a.creator = "John Doe " + bug_a.time = 0 + bug_b = self.new_bug("b", summary="Bug B") + bug_b.creator = "Jane Doe " + bug_b.time = 0 + bug_b.status = "closed" + if sync_with_disk == True: + self.save() + self.set_sync_with_disk(True) + def cleanup(self): + if hasattr(self, "_dir_ref"): + self._dir_ref.cleanup() + BugDir.cleanup(self) 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 + allow_vcs_init=True) + self.vcs = self.bugdir.vcs def tearDown(self): - self.rcs.cleanup() + self.bugdir.cleanup() self.dir.cleanup() def fullPath(self, path): return os.path.join(self.dir.path, path) @@ -593,13 +706,13 @@ class BugDirTestCase(unittest.TestCase): self.assertRaises(AlreadyInitialized, BugDir, self.dir.path, assertNewBugDir=True) def versionTest(self): - if self.rcs.versioned == False: + if self.vcs.versioned == False: return - original = self.bugdir.rcs.commit("Began versioning") + original = self.bugdir.vcs.commit("Began versioning") bugA = self.bugdir.bug_from_uuid("a") bugA.status = "fixed" self.bugdir.save() - new = self.rcs.commit("Fixed bug a") + new = self.vcs.commit("Fixed bug a") dupdir = self.bugdir.duplicate_bugdir(original) self.failUnless(dupdir.root != self.bugdir.root, "%s, %s" % (dupdir.root, self.bugdir.root)) @@ -645,17 +758,19 @@ class BugDirTestCase(unittest.TestCase): rep.new_reply("And they have six legs.") if sync_with_disk == False: self.bugdir.save() + self.bugdir.set_sync_with_disk(True) self.bugdir._clear_bugs() bug = self.bugdir.bug_from_uuid("a") bug.load_comments() + if sync_with_disk == False: + self.bugdir.set_sync_with_disk(False) 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, + self.failUnless(comment.sync_with_disk == sync_with_disk, comment.sync_with_disk) - #load_settings() self.failUnless(comment.content_type == "text/plain", comment.content_type) self.failUnless(repLoaded.settings["Content-type"]=="text/plain", @@ -672,5 +787,46 @@ class BugDirTestCase(unittest.TestCase): def testSyncedComments(self): self.testComments(sync_with_disk=True) -unitsuite = unittest.TestLoader().loadTestsFromTestCase(BugDirTestCase) -suite = unittest.TestSuite([unitsuite])#, doctest.DocTestSuite()]) +class SimpleBugDirTestCase (unittest.TestCase): + def setUp(self): + # create a pre-existing bugdir in a temporary directory + self.dir = utility.Dir() + self.original_working_dir = os.getcwd() + os.chdir(self.dir.path) + self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, + allow_vcs_init=True) + self.bugdir.new_bug("preexisting", summary="Hopefully not imported") + self.bugdir.save() + def tearDown(self): + os.chdir(self.original_working_dir) + self.bugdir.cleanup() + self.dir.cleanup() + def testOnDiskCleanLoad(self): + """SimpleBugDir(sync_with_disk==True) should not import preexisting bugs.""" + bugdir = SimpleBugDir(sync_with_disk=True) + self.failUnless(bugdir.sync_with_disk==True, bugdir.sync_with_disk) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir._clear_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == [], uuids) + bugdir.load_all_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir.cleanup() + def testInMemoryCleanLoad(self): + """SimpleBugDir(sync_with_disk==False) should not import preexisting bugs.""" + bugdir = SimpleBugDir(sync_with_disk=False) + self.failUnless(bugdir.sync_with_disk==False, bugdir.sync_with_disk) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + self.failUnlessRaises(DiskAccessRequired, bugdir.load_all_bugs) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir._clear_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == [], uuids) + bugdir.cleanup() + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +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 index d7cd1e5..e9e0649 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/bzr.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/bzr.py @@ -17,61 +17,65 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Bazaar (bzr) backend. +""" + import os import re import sys import unittest import doctest -import rcs -from rcs import RCS +import vcs + def new(): return Bzr() -class Bzr(RCS): +class Bzr(vcs.VCS): name = "bzr" client = "bzr" versioned = True - def _rcs_help(self): + def _vcs_help(self): status,output,error = self._u_invoke_client("--help") return output - def _rcs_detect(self, path): + def _vcs_detect(self, path): if self._u_search_parent_directories(path, ".bzr") != None : return True return False - def _rcs_root(self, path): + def _vcs_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): + def _vcs_init(self, path): self._u_invoke_client("init", directory=path) - def _rcs_get_user_id(self): + def _vcs_get_user_id(self): status,output,error = self._u_invoke_client("whoami") return output.rstrip('\n') - def _rcs_set_user_id(self, value): + def _vcs_set_user_id(self, value): self._u_invoke_client("whoami", value) - def _rcs_add(self, path): + def _vcs_add(self, path): self._u_invoke_client("add", path) - def _rcs_remove(self, path): + def _vcs_remove(self, path): # --force to also remove unversioned files. self._u_invoke_client("remove", "--force", path) - def _rcs_update(self, path): + def _vcs_update(self, path): pass - def _rcs_get_file_contents(self, path, revision=None, binary=False): + def _vcs_get_file_contents(self, path, revision=None, binary=False): if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + return vcs.VCS._vcs_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): + def _vcs_duplicate_repo(self, directory, revision=None): if revision == None: - RCS._rcs_duplicate_repo(self, directory, revision) + vcs.VCS._vcs_duplicate_repo(self, directory, revision) else: self._u_invoke_client("branch", "--revision", revision, ".", directory) - def _rcs_commit(self, commitfile, allow_empty=False): + def _vcs_commit(self, commitfile, allow_empty=False): args = ["commit", "--file", commitfile] if allow_empty == True: args.append("--unchanged") @@ -83,9 +87,9 @@ class Bzr(RCS): 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() + raise vcs.EmptyCommit() else: - raise rcs.CommandError(args, status, error) + raise vcs.CommandError(args, status, stdout="", stderr=error) revision = None revline = re.compile("Committed revision (.*)[.]") match = revline.search(error) @@ -93,9 +97,17 @@ class Bzr(RCS): assert len(match.groups()) == 1 revision = match.groups()[0] return revision + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("revno") + current_revision = int(output) + if index >= current_revision or index < -current_revision: + return None + if index >= 0: + return str(index+1) # bzr commit 0 is the empty tree. + return str(current_revision+index+1) -rcs.make_rcs_testcase_subclasses(Bzr, sys.modules[__name__]) +vcs.make_vcs_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 index 853a75a..9b64142 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/cmdutil.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/cmdutil.py @@ -15,6 +15,11 @@ # 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. + +""" +Define assorted utilities to make command-line handling easier. +""" + import glob import optparse import os @@ -70,10 +75,11 @@ def get_command(command_name): return cmd -def execute(cmd, args): +def execute(cmd, args, manipulate_encodings=True): enc = encoding.get_encoding() cmd = get_command(cmd) - ret = cmd.execute([a.decode(enc) for a in args]) + ret = cmd.execute([a.decode(enc) for a in args], + manipulate_encodings=manipulate_encodings) if ret == None: ret = 0 return ret @@ -206,6 +212,15 @@ def underlined(instring): return "%s\n%s" % (instring, "="*len(instring)) +def bug_from_shortname(bdir, shortname): + """ + Exception translation for the command-line interface. + """ + try: + bug = bdir.bug_from_shortname(shortname) + except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: + raise UserError(e.message) + return bug def _test(): import doctest diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/comment.py b/interfaces/web/Bugs-Everywhere-Web/libbe/comment.py index 3249e8b..41bc7e6 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/comment.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/comment.py @@ -16,6 +16,11 @@ # 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. + +""" +Define the Comment class for representing bug comments. +""" + import base64 import os import os.path @@ -61,6 +66,11 @@ class MissingReference(ValueError): self.reference = comment.in_reply_to self.comment = comment +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) + INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!" def list_to_root(comments, bug, root=None, @@ -115,8 +125,10 @@ 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. """ + if bug.sync_with_disk == False: + raise DiskAccessRequired("load comments") path = bug.get_path("comments") - if not os.path.isdir(path): + if not os.path.exists(path): return Comment(bug, uuid=INVALID_UUID) comments = [] for uuid in os.listdir(path): @@ -131,6 +143,8 @@ def loadComments(bug, load_full=False): return list_to_root(comments, bug) def saveComments(bug): + if bug.sync_with_disk == False: + raise DiskAccessRequired("save comments") for comment in bug.comment_root.traverse(): comment.save() @@ -162,9 +176,9 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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", + @_versioned_property(name="Author", doc="The author of the comment") - def From(): return {} + def author(): return {} @_versioned_property(name="In-reply-to", doc="UUID for parent comment or bug") @@ -178,28 +192,28 @@ class Comment(Tree, settings_object.SavedSettingsObject): @_versioned_property(name="Date", doc="An RFC 2822 timestamp for comment creation") - def time_string(): return {} + def date(): return {} def _get_time(self): - if self.time_string == None: + if self.date == None: return None - return utility.str_to_time(self.time_string) + return utility.str_to_time(self.date) def _set_time(self, value): - self.time_string = utility.time_to_str(value) + self.date = utility.time_to_str(value) time = property(fget=_get_time, fset=_set_time, - doc="An integer version of .time_string") + doc="An integer version of .date") def _get_comment_body(self): - if self.rcs != None and self.sync_with_disk == True: - import rcs + if self.vcs != None and self.sync_with_disk == True: + import vcs binary = not self.content_type.startswith("text/") - return self.rcs.get_file_contents(self.get_path("body"), binary=binary) + return self.vcs.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: + if (self.vcs != 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) + self.vcs.set_file_contents(self.get_path("body"), new, binary=binary) @Property @change_hook_property(hook=_set_comment_body) @@ -208,15 +222,15 @@ class Comment(Tree, settings_object.SavedSettingsObject): @doc_property(doc="The meat of the comment") def body(): return {} - def _get_rcs(self): - if hasattr(self.bug, "rcs"): - return self.bug.rcs + def _get_vcs(self): + if hasattr(self.bug, "vcs"): + return self.bug.vcs @Property - @cached_property(generator=_get_rcs) - @local_property("rcs") + @cached_property(generator=_get_vcs) + @local_property("vcs") @doc_property(doc="A revision control system instance.") - def rcs(): return {} + def vcs(): return {} def _extra_strings_check_fn(value): return utility.iterable_full_of_strings(value, \ @@ -257,13 +271,29 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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() + if self.vcs != None: + self.author = self.vcs.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 __cmp__(self, other): + return cmp_full(self, other) + + def __str__(self): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> comm.uuid = "com-1" + >>> comm.date = "Thu, 20 Nov 2008 15:55:11 +0000" + >>> comm.author = "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 traverse(self, *args, **kwargs): """Avoid working with the possible dummy root comment""" @@ -272,6 +302,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): continue yield comment + # serializing methods + def _setting_attr_string(self, setting): value = getattr(self, setting) if value == None: @@ -282,12 +314,12 @@ class Comment(Tree, settings_object.SavedSettingsObject): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> comm.uuid = "0123" - >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> comm.date = "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 @@ -309,8 +341,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): ("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), + ("author", self._setting_attr_string("author")), + ("date", self.date), ("content-type", self.content_type), ("body", body)] lines = [""] @@ -328,11 +360,11 @@ class Comment(Tree, settings_object.SavedSettingsObject): fields. >>> commA = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> commA.uuid = "0123" - >>> commA.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> commA.date = "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'] + >>> attrs=['uuid','alt_id','in_reply_to','author','date','content_type','body'] >>> for attr in attrs: # doctest: +ELLIPSIS ... if getattr(commB, attr) != getattr(commA, attr): ... estr = "Mismatch on %s: '%s' should be '%s'" @@ -342,15 +374,15 @@ class Comment(Tree, settings_object.SavedSettingsObject): Mismatch on alt_id: '0123' should be 'None' >>> print commB.alt_id 0123 - >>> commA.From - >>> commB.From + >>> commA.author + >>> commB.author """ 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'] + tags=['uuid','alt-id','in-reply-to','author','date','content-type','body'] uuid = None body = None for child in comment.getchildren(): @@ -368,10 +400,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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) @@ -389,7 +417,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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" + >>> comm.date = "Thu, 01 Jan 1970 00:00:00 +0000" >>> print comm.string(indent=2, shortname="com-1") --------- Comment --------- Name: com-1 @@ -405,8 +433,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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("From: %s" % (self._setting_attr_string("author"))) + lines.append("Date: %s" % self.date) lines.append("") if self.content_type.startswith("text/"): lines.extend((self.body or "").splitlines()) @@ -417,78 +445,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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): @@ -506,7 +462,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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.sort(key=lambda c : c.author) # your sort comm.string_thread(name_map=name_map) >>> a = Comment(bug=None, uuid="a", body="Insightful remarks") @@ -593,6 +549,77 @@ class Comment(Tree, settings_object.SavedSettingsObject): indent=indent, auto_name_map=auto_name_map, bug_shortname=bug_shortname) + # methods for saving/loading/acessing settings and properties. + + def get_path(self, *args): + dir = os.path.join(self.bug.get_path("comments"), self.uuid) + if len(args) == 0: + return dir + assert args[0] in ["values", "body"], str(args) + return os.path.join(dir, *args) + + def set_sync_with_disk(self, value): + self.sync_with_disk = value + + def load_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("load settings") + self.settings = mapfile.map_load(self.vcs, self.get_path("values")) + self._setup_saved_settings() + + def save_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("save settings") + self.vcs.mkdir(self.get_path()) + path = self.get_path("values") + mapfile.map_save(self.vcs, 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). + """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) + assert self.body != None, "Can't save blank comment" + self.save_settings() + self._set_comment_body(new=self.body, force=True) + if sync_with_disk == False: + self.set_sync_with_disk(False) + + def remove(self): + if self.sync_with_disk == False and self.uuid != INVALID_UUID: + raise DiskAccessRequired("remove") + for comment in self.traverse(): + path = comment.get_path() + self.vcs.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) + + 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 comment_shortnames(self, bug_shortname=None): """ Iterate through (id, comment) pairs, in time order. @@ -659,4 +686,59 @@ class Comment(Tree, settings_object.SavedSettingsObject): return comment raise KeyError(uuid) +def cmp_attr(comment_1, comment_2, attr, invert=False): + """ + Compare a general attribute between two comments using the conventional + comparison rule for that attribute type. If invert == True, sort + *against* that convention. + >>> attr="author" + >>> commentA = Comment() + >>> commentB = Comment() + >>> commentA.author = "John Doe" + >>> commentB.author = "Jane Doe" + >>> cmp_attr(commentA, commentB, attr) > 0 + True + >>> cmp_attr(commentA, commentB, attr, invert=True) < 0 + True + >>> commentB.author = "John Doe" + >>> cmp_attr(commentA, commentB, attr) == 0 + True + """ + if not hasattr(comment_2, attr) : + return 1 + val_1 = getattr(comment_1, attr) + val_2 = getattr(comment_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_uuid = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "uuid") +cmp_author = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "author") +cmp_in_reply_to = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "in_reply_to") +cmp_content_type = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "content_type") +cmp_body = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "body") +# chronological rankings (newer < older) +cmp_time = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "time", invert=True) + +DEFAULT_CMP_FULL_CMP_LIST = \ + (cmp_time, cmp_author, cmp_content_type, cmp_body, cmp_in_reply_to, + cmp_uuid) + +class CommentCompoundComparator (object): + def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): + self.cmp_list = cmp_list + def __call__(self, comment_1, comment_2): + for comparison in self.cmp_list : + val = comparison(comment_1, comment_2) + if val != 0 : + return val + return 0 + +cmp_full = CommentCompoundComparator() + suite = doctest.DocTestSuite() diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/config.py b/interfaces/web/Bugs-Everywhere-Web/libbe/config.py index 5e343b9..fb5a028 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/config.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/config.py @@ -14,6 +14,11 @@ # 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, save, and load the per-user config file at path(). +""" + import ConfigParser import codecs import locale @@ -21,6 +26,7 @@ import os.path import sys import doctest + default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() def path(): diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/darcs.py b/interfaces/web/Bugs-Everywhere-Web/libbe/darcs.py index e7132c0..16005f2 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/darcs.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/darcs.py @@ -14,31 +14,40 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Darcs backend. +""" + import codecs import os import re import sys -import unittest +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 import doctest +import unittest + +import vcs -import rcs -from rcs import RCS def new(): return Darcs() -class Darcs(RCS): +class Darcs(vcs.VCS): name="darcs" client="darcs" versioned=True - def _rcs_help(self): + def _vcs_help(self): status,output,error = self._u_invoke_client("--help") return output - def _rcs_detect(self, path): + def _vcs_detect(self, path): if self._u_search_parent_directories(path, "_darcs") != None : return True return False - def _rcs_root(self, path): + def _vcs_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. @@ -48,9 +57,9 @@ class Darcs(RCS): if darcs_dir == None: return None return os.path.dirname(darcs_dir) - def _rcs_init(self, path): + def _vcs_init(self, path): self._u_invoke_client("init", directory=path) - def _rcs_get_user_id(self): + def _vcs_get_user_id(self): # following http://darcs.net/manual/node4.html#SECTION00410030000000000000 # as of June 29th, 2009 if self.rootdir == None: @@ -65,32 +74,32 @@ class Darcs(RCS): if env_variable in os.environ: return os.environ[env_variable] return None - def _rcs_set_user_id(self, value): + def _vcs_set_user_id(self, value): if self.rootdir == None: self.root(".") if self.rootdir == None: - raise rcs.SettingIDnotSupported + raise vcs.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): + def _vcs_add(self, path): if os.path.isdir(path): return self._u_invoke_client("add", path) - def _rcs_remove(self, path): + def _vcs_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): + def _vcs_update(self, path): pass # darcs notices changes - def _rcs_get_file_contents(self, path, revision=None, binary=False): + def _vcs_get_file_contents(self, path, revision=None, binary=False): if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, + return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) else: try: return self._u_invoke_client("show", "contents", "--patch", revision, path) - except rcs.CommandError: + except vcs.CommandError: # Darcs versions < 2.0.0pre2 lack the "show contents" command status,output,error = self._u_invoke_client("diff", "--unified", @@ -113,7 +122,7 @@ class Darcs(RCS): 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, + contents = vcs.VCS._vcs_get_file_contents(self, path, binary=binary) else: contents = "" @@ -123,41 +132,53 @@ class Darcs(RCS): 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, + current_contents = vcs.VCS._vcs_get_file_contents(self, path, binary=binary) return contents - def _rcs_duplicate_repo(self, directory, revision=None): + def _vcs_duplicate_repo(self, directory, revision=None): if revision==None: - RCS._rcs_duplicate_repo(self, directory, revision) + vcs.VCS._vcs_duplicate_repo(self, directory, revision) else: self._u_invoke_client("put", "--to-patch", revision, directory) - def _rcs_commit(self, commitfile, allow_empty=False): + def _vcs_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... + raise vcs.EmptyCommit() + # note that darcs does _not_ make an empty revision. + # this returns the last non-empty revision id... + revision = self._vcs_revision_id(-1) 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] + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 1 + revision = match.groups()[0] return revision - + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("changes", "--xml") + revisions = [] + xml_str = output.encode("unicode_escape").replace(r"\n", "\n") + element = ElementTree.XML(xml_str) + assert element.tag == "changelog", element.tag + for patch in element.getchildren(): + assert patch.tag == "patch", patch.tag + for child in patch.getchildren(): + if child.tag == "name": + text = unescape(unicode(child.text).decode("unicode_escape").strip()) + revisions.append(text) + revisions.reverse() + try: + return revisions[index] + except IndexError: + return None -rcs.make_rcs_testcase_subclasses(Darcs, sys.modules[__name__]) +vcs.make_vcs_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 index ba48efc..9253a23 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/diff.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/diff.py @@ -14,112 +14,406 @@ # 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 + +"""Compare two bug trees.""" + +import difflib 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) +from libbe import bugdir, bug, settings_object, tree +from libbe.utility import time_to_str -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") +class DiffTree (tree.Tree): + """ + A tree holding difference data for easy report generation. + >>> bugdir = DiffTree("bugdir") + >>> bdsettings = DiffTree("settings", data="target: None -> 1.0") + >>> bugdir.append(bdsettings) + >>> bugs = DiffTree("bugs", "bug-count: 5 -> 6") + >>> bugdir.append(bugs) + >>> new = DiffTree("new", "new bugs: ABC, DEF") + >>> bugs.append(new) + >>> rem = DiffTree("rem", "removed bugs: RST, UVW") + >>> bugs.append(rem) + >>> print bugdir.report_string() + target: None -> 1.0 + bug-count: 5 -> 6 + new bugs: ABC, DEF + removed bugs: RST, UVW + >>> print "\\n".join(bugdir.paths()) + bugdir + bugdir/settings + bugdir/bugs + bugdir/bugs/new + bugdir/bugs/rem + >>> bugdir.child_by_path("/") == bugdir + True + >>> bugdir.child_by_path("/bugs") == bugs + True + >>> bugdir.child_by_path("/bugs/rem") == rem + True + >>> bugdir.child_by_path("bugdir") == bugdir + True + >>> bugdir.child_by_path("bugdir/") == bugdir + True + >>> bugdir.child_by_path("bugdir/bugs") == bugs + True + >>> bugdir.child_by_path("/bugs").masked = True + >>> print bugdir.report_string() + target: None -> 1.0 + """ + def __init__(self, name, data=None, data_part_fn=str, + requires_children=False, masked=False): + tree.Tree.__init__(self) + self.name = name + self.data = data + self.data_part_fn = data_part_fn + self.requires_children = requires_children + self.masked = masked + def paths(self, parent_path=None): + paths = [] + if parent_path == None: + path = self.name + else: + path = "%s/%s" % (parent_path, self.name) + paths.append(path) + for child in self: + paths.extend(child.paths(path)) + return paths + def child_by_path(self, path): + if hasattr(path, "split"): # convert string path to a list of names + names = path.split("/") + if names[0] == "": + names[0] = self.name # replace root with self + if len(names) > 1 and names[-1] == "": + names = names[:-1] # strip empty tail + else: # it was already an array + names = path + assert len(names) > 0, path + if names[0] == self.name: + if len(names) == 1: + return self + for child in self: + if names[1] == child.name: + return child.child_by_path(names[1:]) + if len(names) == 1: + raise KeyError, "%s doesn't match '%s'" % (names, self.name) + raise KeyError, "%s points to child not in %s" % (names, [c.name for c in self]) + def report_string(self): + return "\n".join(self.report()) + def report(self, root=None, parent=None, depth=0): + if root == None: + root = self.make_root() + if self.masked == True: + return None + data_part = self.data_part(depth) + if self.requires_children == True and len(self) == 0: + pass + else: + self.join(root, parent, data_part) + if data_part != None: + depth += 1 + for child in self: + child.report(root, self, depth) + return root + def make_root(self): + return [] + def join(self, root, parent, data_part): + if data_part != None: + root.append(data_part) + def data_part(self, depth, indent=True): + if self.data == None: + return None + if hasattr(self, "_cached_data_part"): + return self._cached_data_part + data_part = self.data_part_fn(self.data) + if indent == True: + data_part_lines = data_part.splitlines() + indent = " "*(depth) + line_sep = "\n"+indent + data_part = indent+line_sep.join(data_part_lines) + self._cached_data_part = data_part + return data_part -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: +class Diff (object): + """ + Difference tree generator for BugDirs. + >>> import copy + >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) + >>> bd.user_id = "John Doe " + >>> bd_new = copy.deepcopy(bd) + >>> bd_new.target = "1.0" + >>> a = bd_new.bug_from_uuid("a") + >>> rep = a.comment_root.new_reply("I'm closing this bug") + >>> rep.uuid = "acom" + >>> rep.date = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> a.status = "closed" + >>> b = bd_new.bug_from_uuid("b") + >>> bd_new.remove_bug(b) + >>> c = bd_new.new_bug("c", "Bug C") + >>> d = Diff(bd, bd_new) + >>> r = d.report_tree() + >>> print "\\n".join(r.paths()) + bugdir + bugdir/settings + bugdir/bugs + bugdir/bugs/new + bugdir/bugs/new/c + bugdir/bugs/rem + bugdir/bugs/rem/b + bugdir/bugs/mod + bugdir/bugs/mod/a + bugdir/bugs/mod/a/settings + bugdir/bugs/mod/a/comments + bugdir/bugs/mod/a/comments/new + bugdir/bugs/mod/a/comments/new/acom + bugdir/bugs/mod/a/comments/rem + bugdir/bugs/mod/a/comments/mod + >>> print r.report_string() + Changed bug directory settings: + target: None -> 1.0 + New bugs: + c:om: Bug C + Removed bugs: + b:cm: Bug B + Modified bugs: + a:cm: Bug A + Changed bug settings: + status: open -> closed + New comments: + from John Doe on Thu, 01 Jan 1970 00:00:00 +0000 + I'm closing this bug... + >>> bd.cleanup() + """ + def __init__(self, old_bugdir, new_bugdir): + self.old_bugdir = old_bugdir + self.new_bugdir = new_bugdir + + # data assembly methods + + def _changed_bugs(self): + """ + Search for differences in all bugs between .old_bugdir and + .new_bugdir. Returns + (added_bugs, modified_bugs, removed_bugs) + where added_bugs and removed_bugs are lists of added and + removed bugs respectively. modified_bugs is a list of + (old_bug,new_bug) pairs. + """ + if hasattr(self, "__changed_bugs"): + return self.__changed_bugs + added = [] + removed = [] + modified = [] + for uuid in self.new_bugdir.list_uuids(): + new_bug = self.new_bugdir.bug_from_uuid(uuid) + try: + old_bug = self.old_bugdir.bug_from_uuid(uuid) + except KeyError: + added.append(new_bug) + else: + if old_bug.sync_with_disk == True: + old_bug.load_comments() + if new_bug.sync_with_disk == True: + new_bug.load_comments() + if old_bug != new_bug: + modified.append((old_bug, new_bug)) + for uuid in self.old_bugdir.list_uuids(): + if not self.new_bugdir.has_bug(uuid): + old_bug = self.old_bugdir.bug_from_uuid(uuid) + removed.append(old_bug) + added.sort() + removed.sort() + modified.sort(self._bug_modified_cmp) + self.__changed_bugs = (added, modified, removed) + return self.__changed_bugs + def _bug_modified_cmp(self, left, right): + return cmp(left[1], right[1]) + def _changed_comments(self, old, new): + """ + Search for differences in all loaded comments between the bugs + old and new. Returns + (added_comments, modified_comments, removed_comments) + analogous to ._changed_bugs. + """ + if hasattr(self, "__changed_comments"): + if new.uuid in self.__changed_comments: + return self.__changed_comments[new.uuid] + else: + self.__changed_comments = {} + added = [] + removed = [] + modified = [] + old.comment_root.sort(key=lambda comm : comm.time) + new.comment_root.sort(key=lambda comm : comm.time) + old_comment_ids = [c.uuid for c in old.comments()] + new_comment_ids = [c.uuid for c in new.comments()] + for uuid in new_comment_ids: + new_comment = new.comment_from_uuid(uuid) + try: + old_comment = old.comment_from_uuid(uuid) + except KeyError: + added.append(new_comment) + else: + if old_comment != new_comment: + modified.append((old_comment, new_comment)) + for uuid in old_comment_ids: + if uuid not in new_comment_ids: + new_comment = new.comment_from_uuid(uuid) + removed.append(new_comment) + self.__changed_comments[new.uuid] = (added, modified, removed) + return self.__changed_comments[new.uuid] + def _attribute_changes(self, old, new, attributes): + """ + Take two objects old and new, and compare the value of *.attr + for attr in the list attribute names. Returns a list of + (attr_name, old_value, new_value) + tuples. + """ + change_list = [] + for attr in attributes: + old_value = getattr(old, attr) + new_value = getattr(new, attr) + if old_value != new_value: + change_list.append((attr, old_value, new_value)) + if len(change_list) >= 0: + return change_list return None + def _settings_properties_attribute_changes(self, old, new, + hidden_properties=[]): + properties = sorted(new.settings_properties) + for p in hidden_properties: + properties.remove(p) + attributes = [settings_object.setting_name_to_attr_name(None, p) + for p in properties] + return self._attribute_changes(old, new, attributes) + def _bugdir_attribute_changes(self): + return self._settings_properties_attribute_changes( \ + self.old_bugdir, self.new_bugdir, + ["vcs_name"]) # tweaked by bugdir.duplicate_bugdir + def _bug_attribute_changes(self, old, new): + return self._settings_properties_attribute_changes(old, new) + def _comment_attribute_changes(self, old, new): + return self._settings_properties_attribute_changes(old, new) -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] + # report generation methods - 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) + def report_tree(self, diff_tree=DiffTree): + """ + Pretty bare to make it easy to adjust to specific cases. You + can pass in a DiffTree subclass via diff_tree to override the + default report assembly process. + """ + if hasattr(self, "__report_tree"): + return self.__report_tree + bugdir_settings = sorted(self.new_bugdir.settings_properties) + bugdir_settings.remove("vcs_name") # tweaked by bugdir.duplicate_bugdir + root = diff_tree("bugdir") + bugdir_attribute_changes = self._bugdir_attribute_changes() + if len(bugdir_attribute_changes) > 0: + bugdir = diff_tree("settings", bugdir_attribute_changes, + self.bugdir_attribute_change_string) + root.append(bugdir) + bug_root = diff_tree("bugs") + root.append(bug_root) + add,mod,rem = self._changed_bugs() + bnew = diff_tree("new", "New bugs:", requires_children=True) + bug_root.append(bnew) + for bug in add: + b = diff_tree(bug.uuid, bug, self.bug_add_string) + bnew.append(b) + brem = diff_tree("rem", "Removed bugs:", requires_children=True) + bug_root.append(brem) + for bug in rem: + b = diff_tree(bug.uuid, bug, self.bug_rem_string) + brem.append(b) + bmod = diff_tree("mod", "Modified bugs:", requires_children=True) + bug_root.append(bmod) + for old,new in mod: + b = diff_tree(new.uuid, (old,new), self.bug_mod_string) + bmod.append(b) + bug_attribute_changes = self._bug_attribute_changes(old, new) + if len(bug_attribute_changes) > 0: + bset = diff_tree("settings", bug_attribute_changes, + self.bug_attribute_change_string) + b.append(bset) + if old.summary != new.summary: + data = (old.summary, new.summary) + bsum = diff_tree("summary", data, self.bug_summary_change_string) + b.append(bsum) + cr = diff_tree("comments") + b.append(cr) + a,m,d = self._changed_comments(old, new) + cnew = diff_tree("new", "New comments:", requires_children=True) + for comment in a: + c = diff_tree(comment.uuid, comment, self.comment_add_string) + cnew.append(c) + crem = diff_tree("rem", "Removed comments:",requires_children=True) + for comment in d: + c = diff_tree(comment.uuid, comment, self.comment_rem_string) + crem.append(c) + cmod = diff_tree("mod","Modified comments:",requires_children=True) + for o,n in m: + c = diff_tree(n.uuid, (o,n), self.comment_mod_string) + cmod.append(c) + comm_attribute_changes = self._comment_attribute_changes(o, n) + if len(comm_attribute_changes) > 0: + cset = diff_tree("settings", comm_attribute_changes, + self.comment_attribute_change_string) + if o.body != n.body: + data = (o.body, n.body) + cbody = diff_tree("cbody", data, + self.comment_body_change_string) + c.append(cbody) + cr.extend([cnew, crem, cmod]) + self.__report_tree = root + return self.__report_tree - if len(change_strings) == 0: - return None - return "%s\n %s" % (new.string(shortlist=True), - " \n".join(change_strings)) + # change data -> string methods. + # Feel free to play with these in subclasses. + def attribute_change_string(self, attribute_changes, indent=0): + indent_string = " "*indent + change_strings = [u"%s: %s -> %s" % f for f in attribute_changes] + for i,change_string in enumerate(change_strings): + change_strings[i] = indent_string+change_string + return u"\n".join(change_strings) + def bugdir_attribute_change_string(self, attribute_changes): + return "Changed bug directory settings:\n%s" % \ + self.attribute_change_string(attribute_changes, indent=1) + def bug_attribute_change_string(self, attribute_changes): + return "Changed bug settings:\n%s" % \ + self.attribute_change_string(attribute_changes, indent=1) + def comment_attribute_change_string(self, attribute_changes): + return "Changed comment settings:\n%s" % \ + self.attribute_change_string(attribute_changes, indent=1) + def bug_add_string(self, bug): + return bug.string(shortlist=True) + def bug_rem_string(self, bug): + return bug.string(shortlist=True) + def bug_mod_string(self, bugs): + old_bug,new_bug = bugs + return new_bug.string(shortlist=True) + def bug_summary_change_string(self, summaries): + old_summary,new_summary = summaries + return "summary changed:\n %s\n %s" % (old_summary, new_summary) + def _comment_summary_string(self, comment): + return "from %s on %s" % (comment.author, time_to_str(comment.time)) + def comment_add_string(self, comment): + summary = self._comment_summary_string(comment) + first_line = comment.body.splitlines()[0] + return "%s\n %s..." % (summary, first_line) + def comment_rem_string(self, comment): + summary = self._comment_summary_string(comment) + first_line = comment.body.splitlines()[0] + return "%s\n %s..." % (summary, first_line) + def comment_mod_string(self, comments): + old_comment,new_comment = comments + return self._comment_summary_string(new_comment) + def comment_body_change_string(self, bodies): + old_body,new_body = bodies + return difflib.unified_diff(old_body, new_body) -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 index 93144b8..ec41006 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/editor.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/editor.py @@ -15,6 +15,11 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Define editor_string(), a function that invokes an editor to accept +user-produced text as a string. +""" + import codecs import locale import os @@ -22,6 +27,7 @@ import sys import tempfile import doctest + default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() comment_marker = u"== Anything below this line will be ignored\n" @@ -62,7 +68,8 @@ def editor_string(comment=None, encoding=None): fhandle, fname = tempfile.mkstemp() try: if comment is not None: - os.write(fhandle, '\n'+comment_string(comment)) + cstring = u'\n'+comment_string(comment) + os.write(fhandle, cstring.encode(encoding)) os.close(fhandle) oldmtime = os.path.getmtime(fname) os.system("%s %s" % (editor, fname)) diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/encoding.py b/interfaces/web/Bugs-Everywhere-Web/libbe/encoding.py index d603602..fd513b5 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/encoding.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/encoding.py @@ -14,16 +14,26 @@ # 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. + +""" +Support input/output/filesystem encodings (e.g. UTF-8). +""" + import codecs import locale import sys import doctest + +ENCODING = None # override get_encoding() output by setting this + def get_encoding(): """ Guess a useful input/output/filesystem encoding... Maybe we need seperate encodings for input/output and filesystem? Hmm... """ + if ENCODING != None: + return ENCODING 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 diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/git.py b/interfaces/web/Bugs-Everywhere-Web/libbe/git.py index 2f9ffa9..3abe3b8 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/git.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/git.py @@ -16,30 +16,34 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Git backend. +""" + import os import re import sys import unittest import doctest -import rcs -from rcs import RCS +import vcs + def new(): return Git() -class Git(RCS): +class Git(vcs.VCS): name="git" client="git" versioned=True - def _rcs_help(self): + def _vcs_help(self): status,output,error = self._u_invoke_client("--help") return output - def _rcs_detect(self, path): + def _vcs_detect(self, path): if self._u_search_parent_directories(path, ".git") != None : return True return False - def _rcs_root(self, path): + def _vcs_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. @@ -50,13 +54,21 @@ class Git(RCS): gitdir = os.path.join(path, output.rstrip('\n')) dirname = os.path.abspath(os.path.dirname(gitdir)) return dirname - def _rcs_init(self, path): + def _vcs_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') + def _vcs_get_user_id(self): + status,output,error = \ + self._u_invoke_client("config", "user.name", expect=(0,1)) + if status == 0: + name = output.rstrip('\n') + else: + name = "" + status,output,error = \ + self._u_invoke_client("config", "user.email", expect=(0,1)) + if status == 0: + email = output.rstrip('\n') + else: + email = "" if name != "" or email != "": # got something! # guess missing info, if necessary if name == "": @@ -65,35 +77,35 @@ class Git(RCS): 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): + def _vcs_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): + def _vcs_add(self, path): if os.path.isdir(path): return self._u_invoke_client("add", path) - def _rcs_remove(self, path): + def _vcs_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): + def _vcs_update(self, path): + self._vcs_add(path) + def _vcs_get_file_contents(self, path, revision=None, binary=False): if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + return vcs.VCS._vcs_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): + def _vcs_duplicate_repo(self, directory, revision=None): if revision==None: - RCS._rcs_duplicate_repo(self, directory, revision) + vcs.VCS._vcs_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): + def _vcs_commit(self, commitfile, allow_empty=False): args = ['commit', '--all', '--file', commitfile] if allow_empty == True: args.append("--allow-empty") @@ -104,17 +116,33 @@ class Git(RCS): strings = ["nothing to commit", "nothing added to commit"] if self._u_any_in_string(strings, output) == True: - raise rcs.EmptyCommit() + raise vcs.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 + full_revision = self._vcs_revision_id(-1) + assert full_revision.startswith(revision), \ + "Mismatched revisions:\n%s\n%s" % (revision, full_revision) + return full_revision + def _vcs_revision_id(self, index): + args = ["rev-list", "--first-parent", "--reverse", "HEAD"] + kwargs = {"expect":(0,128)} + status,output,error = self._u_invoke_client(*args, **kwargs) + if status == 128: + if error.startswith("fatal: ambiguous argument 'HEAD': unknown "): + return None + raise vcs.CommandError(args, status, stdout="", stderr=error) + commits = output.splitlines() + try: + return commits[index] + except IndexError: + return None -rcs.make_rcs_testcase_subclasses(Git, sys.modules[__name__]) +vcs.make_vcs_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 index a20eeb5..f8f8121 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/hg.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/hg.py @@ -16,81 +16,88 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Mercurial (hg) backend. +""" + import os import re import sys import unittest import doctest -import rcs -from rcs import RCS +import vcs + def new(): return Hg() -class Hg(RCS): +class Hg(vcs.VCS): name="hg" client="hg" versioned=True - def _rcs_help(self): + def _vcs_help(self): status,output,error = self._u_invoke_client("--help") return output - def _rcs_detect(self, path): + def _vcs_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): + def _vcs_root(self, path): status,output,error = self._u_invoke_client("root", directory=path) return output.rstrip('\n') - def _rcs_init(self, path): + def _vcs_init(self, path): self._u_invoke_client("init", directory=path) - def _rcs_get_user_id(self): + def _vcs_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): + def _vcs_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): + raise vcs.SettingIDnotSupported + def _vcs_add(self, path): self._u_invoke_client("add", path) - def _rcs_remove(self, path): + def _vcs_remove(self, path): self._u_invoke_client("rm", "--force", path) - def _rcs_update(self, path): + def _vcs_update(self, path): pass - def _rcs_get_file_contents(self, path, revision=None, binary=False): + def _vcs_get_file_contents(self, path, revision=None, binary=False): if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + return vcs.VCS._vcs_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): + def _vcs_duplicate_repo(self, directory, revision=None): if revision == None: - return RCS._rcs_duplicate_repo(self, directory, revision) + return vcs.VCS._vcs_duplicate_repo(self, directory, revision) else: self._u_invoke_client("archive", "--rev", revision, directory) - def _rcs_commit(self, commitfile, allow_empty=False): + def _vcs_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 + raise vcs.EmptyCommit() + return self._vcs_revision_id(-1) + def _vcs_revision_id(self, index, style="id"): + args = ["identify", "--rev", str(int(index)), "--%s" % style] + kwargs = {"expect": (0,255)} + status,output,error = self._u_invoke_client(*args, **kwargs) + if status == 0: + id = output.strip() + if id == '000000000000': + return None # before initial commit. + return id + return None -rcs.make_rcs_testcase_subclasses(Hg, sys.modules[__name__]) +vcs.make_vcs_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 index b959d76..4d69601 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/mapfile.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/mapfile.py @@ -14,12 +14,19 @@ # 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 + +""" +Provide a means of saving and loading dictionaries of parameters. The +saved "mapfiles" should be clear, flat-text files, and allow easy merging of +independent/conflicting changes. +""" + import errno -import utility +import os.path +import yaml import doctest + class IllegalKey(Exception): def __init__(self, key): Exception.__init__(self, 'Illegal key "%s"' % key) @@ -95,33 +102,15 @@ def parse(contents): >>> 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): +def map_save(vcs, path, map, allow_no_vcs=False): """Save the map as a mapfile to the specified path""" contents = generate(map) - rcs.set_file_contents(path, contents, allow_no_rcs) + vcs.set_file_contents(path, contents, allow_no_vcs) -def map_load(rcs, path, allow_no_rcs=False): - contents = rcs.get_file_contents(path, allow_no_rcs=allow_no_rcs) +def map_load(vcs, path, allow_no_vcs=False): + contents = vcs.get_file_contents(path, allow_no_vcs=allow_no_vcs) 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 index 0545fd7..d593d69 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/plugin.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/plugin.py @@ -15,6 +15,12 @@ # 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. + +""" +Allow simple listing and loading of the various becommands and libbe +submodules (i.e. "plugins"). +""" + import os import os.path import sys diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/properties.py b/interfaces/web/Bugs-Everywhere-Web/libbe/properties.py index 144220b..09dd20e 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/properties.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/properties.py @@ -160,10 +160,10 @@ def _get_cached_mutable_property(self, cacher_name, property_name, default=None) 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): +def _cmp_cached_mutable_property(self, cacher_name, property_name, value, default=None): _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 + _set_cached_mutable_property(self, cacher_name, property_name, default) old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)] return cmp(_hash_mutable_value(value), old_hash) @@ -327,7 +327,7 @@ def primed_property(primer, initVal=None): return funcs return decorator -def change_hook_property(hook, mutable=False): +def change_hook_property(hook, mutable=False, default=None): """ Call the function hook(instance, old_value, new_value) whenever a value different from the current value is set (instance is a a @@ -359,9 +359,9 @@ def change_hook_property(hook, mutable=False): 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: + if _cmp_cached_mutable_property(self, "change hook property", name, value, default) != 0: # there has been a change, cache new value - old_value = _get_cached_mutable_property(self, "change hook property", name) + old_value = _get_cached_mutable_property(self, "change hook property", name, default) _set_cached_mutable_property(self, "change hook property", name, value) if from_fset == True: # return previously cached value value = old_value diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/rcs.py b/interfaces/web/Bugs-Everywhere-Web/libbe/rcs.py index 294b8e0..e69de29 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/rcs.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/rcs.py @@ -1,876 +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., -# 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 index dde247f..ceea9d5 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/settings_object.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/settings_object.py @@ -148,7 +148,8 @@ def versioned_property(name, doc, 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) + hooked = change_hook_property(hook=change_hook, mutable=mutable, + default=EMPTY) primed = primed_property(primer=primer, initVal=UNPRIMED) settings = settings_property(name=name, null=UNPRIMED) docp = doc_property(doc=fulldoc) @@ -385,30 +386,24 @@ class SavedSettingsObjectTests(unittest.TestCase): 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(SAVES == [], 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) diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/tree.py b/interfaces/web/Bugs-Everywhere-Web/libbe/tree.py index 45ae085..06d09e5 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/tree.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/tree.py @@ -15,6 +15,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Define a traversable tree structure. +""" + import doctest class Tree(list): diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/upgrade.py b/interfaces/web/Bugs-Everywhere-Web/libbe/upgrade.py new file mode 100644 index 0000000..4123c72 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/upgrade.py @@ -0,0 +1,187 @@ +# 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. + +""" +Handle conversion between the various on-disk images. +""" + +import os, os.path +import sys +import doctest + +import encoding +import mapfile +import vcs + +# a list of all past versions +BUGDIR_DISK_VERSIONS = ["Bugs Everywhere Tree 1 0", + "Bugs Everywhere Directory v1.1", + "Bugs Everywhere Directory v1.2"] + +# the current version +BUGDIR_DISK_VERSION = BUGDIR_DISK_VERSIONS[-1] + +class Upgrader (object): + "Class for converting " + initial_version = None + final_version = None + def __init__(self, root): + self.root = root + # use the "None" VCS to ensure proper encoding/decoding and + # simplify path construction. + self.vcs = vcs.vcs_by_name("None") + self.vcs.root(self.root) + self.vcs.encoding = encoding.get_encoding() + + def get_path(self, *args): + """ + Return a path relative to .root. + """ + dir = os.path.join(self.root, ".be") + if len(args) == 0: + return dir + assert args[0] in ["version", "settings", "bugs"], str(args) + return os.path.join(dir, *args) + + def check_initial_version(self): + path = self.get_path("version") + version = self.vcs.get_file_contents(path).rstrip("\n") + assert version == self.initial_version, version + + def set_version(self): + path = self.get_path("version") + self.vcs.set_file_contents(path, self.final_version+"\n") + + def upgrade(self): + print >> sys.stderr, "upgrading bugdir from '%s' to '%s'" \ + % (self.initial_version, self.final_version) + self.check_initial_version() + self.set_version() + self._upgrade() + + def _upgrade(self): + raise NotImplementedError + + +class Upgrade_1_0_to_1_1 (Upgrader): + initial_version = "Bugs Everywhere Tree 1 0" + final_version = "Bugs Everywhere Directory v1.1" + def _upgrade_mapfile(self, path): + contents = self.vcs.get_file_contents(path) + old_format = False + for line in contents.splitlines(): + if len(line.split("=")) == 2: + old_format = True + break + if old_format == True: + # translate to YAML. + 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) + # load the YAML and save + map = mapfile.parse(contents) + mapfile.map_save(self.vcs, path, map) + + def _upgrade(self): + """ + Comment value field "From" -> "Author". + Homegrown mapfile -> YAML. + """ + path = self.get_path("settings") + self._upgrade_mapfile(path) + for bug_uuid in os.listdir(self.get_path("bugs")): + path = self.get_path("bugs", bug_uuid, "values") + self._upgrade_mapfile(path) + c_path = ["bugs", bug_uuid, "comments"] + if not os.path.exists(self.get_path(*c_path)): + continue # no comments for this bug + for comment_uuid in os.listdir(self.get_path(*c_path)): + path_list = c_path + [comment_uuid, "values"] + path = self.get_path(*path_list) + self._upgrade_mapfile(path) + settings = mapfile.map_load(self.vcs, path) + if "From" in settings: + settings["Author"] = settings.pop("From") + mapfile.map_save(self.vcs, path, settings) + + +class Upgrade_1_1_to_1_2 (Upgrader): + initial_version = "Bugs Everywhere Directory v1.1" + final_version = "Bugs Everywhere Directory v1.2" + def _upgrade(self): + """ + BugDir settings field "rcs_name" -> "vcs_name". + """ + path = self.get_path("settings") + settings = mapfile.map_load(self.vcs, path) + if "rcs_name" in settings: + settings["vcs_name"] = settings.pop("rcs_name") + mapfile.map_save(self.vcs, path, settings) + + +upgraders = [Upgrade_1_0_to_1_1, + Upgrade_1_1_to_1_2] +upgrade_classes = {} +for upgrader in upgraders: + upgrade_classes[(upgrader.initial_version,upgrader.final_version)]=upgrader + +def upgrade(path, current_version, + target_version=BUGDIR_DISK_VERSION): + """ + Call the appropriate upgrade function to convert current_version + to target_version. If a direct conversion function does not exist, + use consecutive conversion functions. + """ + if current_version not in BUGDIR_DISK_VERSIONS: + raise NotImplementedError, \ + "Cannot handle version '%s' yet." % version + if target_version not in BUGDIR_DISK_VERSIONS: + raise NotImplementedError, \ + "Cannot handle version '%s' yet." % version + + if (current_version, target_version) in upgrade_classes: + # direct conversion + upgrade_class = upgrade_classes[(current_version, target_version)] + u = upgrade_class(path) + u.upgrade() + else: + # consecutive single-step conversion + i = BUGDIR_DISK_VERSIONS.index(current_version) + while True: + version_a = BUGDIR_DISK_VERSIONS[i] + version_b = BUGDIR_DISK_VERSIONS[i+1] + try: + upgrade_class = upgrade_classes[(version_a, version_b)] + except KeyError: + raise NotImplementedError, \ + "Cannot convert version '%s' to '%s' yet." \ + % (version_a, version_b) + u = upgrade_class(path) + u.upgrade() + if version_b == target_version: + break + i += 1 + +suite = doctest.DocTestSuite() diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/utility.py b/interfaces/web/Bugs-Everywhere-Web/libbe/utility.py index 3df06b4..aafbf8d 100644 --- a/interfaces/web/Bugs-Everywhere-Web/libbe/utility.py +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/utility.py @@ -14,6 +14,11 @@ # 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. + +""" +Assorted utility functions that don't fit in anywhere else. +""" + import calendar import codecs import os diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe/vcs.py b/interfaces/web/Bugs-Everywhere-Web/libbe/vcs.py new file mode 100644 index 0000000..a1d3022 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/vcs.py @@ -0,0 +1,938 @@ +# 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. + +""" +Define the base VCS (Version Control System) class, which should be +subclassed by other Version Control System backends. The base class +implements a "do not version" VCS. +""" + +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_vcs(matchfn): + """Return the first module for which matchfn(VCS_instance) is true""" + import arch + import bzr + import darcs + import git + import hg + for module in [arch, bzr, darcs, git, hg]: + vcs = module.new() + if matchfn(vcs) == True: + return vcs + del(vcs) + return VCS() + +def vcs_by_name(vcs_name): + """Return the module for the VCS with the given name""" + return _get_matching_vcs(lambda vcs: vcs.name == vcs_name) + +def detect_vcs(dir): + """Return an VCS instance for the vcs being used in this directory""" + return _get_matching_vcs(lambda vcs: vcs.detect(dir)) + +def installed_vcs(): + """Return an instance of an installed VCS""" + return _get_matching_vcs(lambda vcs: vcs.installed()) + + +class CommandError(Exception): + def __init__(self, command, status, stdout, stderr): + strerror = ["Command failed (%d):\n %s\n" % (status, stderr), + "while executing\n %s" % command] + Exception.__init__(self, "\n".join(strerror)) + self.command = command + self.status = status + self.stdout = stdout + self.stderr = stderr + +class SettingIDnotSupported(NotImplementedError): + pass + +class VCSnotRooted(Exception): + def __init__(self): + msg = "VCS 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 VCS() + +class VCS(object): + """ + This class implements a 'no-vcs' interface. + + Support for other VCSs can be added by subclassing this class, and + overriding methods _vcs_*() with code appropriate for your VCS. + + The methods _u_*() are utility methods available to the _vcs_*() + 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 _vcs_help(self): + """ + Return the command help string. + (Allows a simple test to see if the client is installed.) + """ + pass + def _vcs_detect(self, path=None): + """ + Detect whether a directory is revision controlled with this VCS. + """ + return True + def _vcs_root(self, path): + """ + Get the VCS root. This is the default working directory for + future invocations. You would normally set this to the root + directory for your VCS. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + if path == "": + path = os.path.abspath(".") + return path + def _vcs_init(self, path): + """ + Begin versioning the tree based at path. + """ + pass + def _vcs_cleanup(self): + """ + Remove any cruft that _vcs_init() created outside of the + versioned tree. + """ + pass + def _vcs_get_user_id(self): + """ + Get the VCS's suggested user id (e.g. "John Doe "). + If the VCS has not been configured with a username, return None. + """ + return None + def _vcs_set_user_id(self, value): + """ + Set the VCS's suggested user id (e.g "John Doe "). + This is run if the VCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + raise SettingIDnotSupported + def _vcs_add(self, path): + """ + Add the already created file at path to version control. + """ + pass + def _vcs_remove(self, path): + """ + Remove the file at path from version control. Optionally + remove the file from the filesystem as well. + """ + pass + def _vcs_update(self, path): + """ + Notify the versioning system of changes to the versioned file + at path. + """ + pass + def _vcs_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 VCS 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 _vcs_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 _vcs_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 _vcs_revision_id(self, index): + """ + Return the name of the th revision. Index will be an + integer (possibly <= 0). The choice of which branch to follow + when crossing branches/merges is not defined. + + Return None if revision IDs are not supported, or if the + specified revision does not exist. + """ + return None + def installed(self): + try: + self._vcs_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 VCS. + """ + return self._vcs_detect(path) + def root(self, path): + """ + Set the root directory to the path's VCS root. This is the + default working directory for future invocations. + """ + self.rootdir = self._vcs_root(path) + def init(self, path): + """ + Begin versioning the tree based at path. + Also roots the vcs at path. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + self._vcs_init(path) + self.root(path) + def cleanup(self): + self._vcs_cleanup() + def get_user_id(self): + """ + Get the VCS's suggested user id (e.g. "John Doe "). + If the VCS has not been configured with a username, return the user's + id. You can override the automatic lookup procedure by setting the + VCS.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._vcs_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 VCS's suggested user id (e.g "John Doe "). + This is run if the VCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + self._vcs_set_user_id(value) + def add(self, path): + """ + Add the already created file at path to version control. + """ + self._vcs_add(self._u_rel_path(path)) + def remove(self, path): + """ + Remove a file from both version control and the filesystem. + """ + self._vcs_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._vcs_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._vcs_update(self._u_rel_path(path)) + def get_file_contents(self, path, revision=None, allow_no_vcs=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_vcs(path, allow_no_vcs): + relpath = self._u_rel_path(path) + contents = self._vcs_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_vcs=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_vcs(path, allow_no_vcs): + if add: + self.add(path) + else: + self.update(path) + def mkdir(self, path, allow_no_vcs=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_vcs, check_parents) + if not os.path.exists(path): + os.mkdir(path) + if self._use_vcs(path, allow_no_vcs): + self.add(path) + else: + assert os.path.isdir(path) + if self._use_vcs(path, allow_no_vcs): + #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='BEvcs') + self._duplicateDirname = \ + os.path.join(self._duplicateBasedir, "duplicate") + self._vcs_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._vcs_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 revision_id(self, index=None): + """ + Return the name of the th revision. The choice of + which branch to follow when crossing branches/merges is not + defined. + + Return None if index==None, revision IDs are not supported, or + if the specified revision does not exist. + """ + if index == None: + return None + return self._vcs_revision_id(index) + 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, status=e.args[0], stdout="", stderr=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, output, 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_vcs(self, path, allow_no_vcs): + """ + Try and decide if _vcs_add/update/mkdir/etc calls will + succeed. Returns True is we think the vcs_call would + succeeed, and False otherwise. + """ + use_vcs = True + exception = None + if self.rootdir != None: + if self.path_in_root(path) == False: + use_vcs = False + exception = PathNotInRoot(path, self.rootdir) + else: + use_vcs = False + exception = VCSnotRooted + if use_vcs == False and allow_no_vcs==False: + raise exception + return use_vcs + def path_in_root(self, path, root=None): + """ + Return the relative path to path from root. + >>> vcs = new() + >>> vcs.path_in_root("/a.b/c/.be", "/a.b/c") + True + >>> vcs.path_in_root("/a.b/.be", "/a.b/c") + False + """ + if root == None: + if self.rootdir == None: + raise VCSnotRooted + 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. + >>> vcs = new() + >>> vcs._u_rel_path("/a.b/c/.be", "/a.b/c") + '.be' + """ + if root == None: + if self.rootdir == None: + raise VCSnotRooted + 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. + >>> vcs = new() + >>> vcs._u_abspath(".be", "/a.b/c") + '/a.b/c/.be' + """ + if root == None: + assert self.rootdir != None, "VCS not rooted" + root = self.rootdir + return os.path.abspath(os.path.join(root, path)) + def _u_create_id(self, name, email=None): + """ + >>> vcs = new() + >>> vcs._u_create_id("John Doe", "jdoe@example.com") + 'John Doe ' + >>> vcs._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): + """ + >>> vcs = new() + >>> vcs._u_parse_id("John Doe ") + ('John Doe', 'jdoe@example.com') + >>> vcs._u_parse_id("John Doe") + ('John Doe', None) + >>> try: + ... vcs._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_vcs_test_fixtures(testcase): + """Set up test fixtures for VCS test case.""" + testcase.vcs = testcase.Class() + testcase.dir = Dir() + testcase.dirname = testcase.dir.path + + vcs_not_supporting_uninitialized_user_id = [] + vcs_not_supporting_set_user_id = ["None", "hg"] + testcase.vcs_supports_uninitialized_user_id = ( + testcase.vcs.name not in vcs_not_supporting_uninitialized_user_id) + testcase.vcs_supports_set_user_id = ( + testcase.vcs.name not in vcs_not_supporting_set_user_id) + + if not testcase.vcs.installed(): + testcase.fail( + "%(name)s VCS not found" % vars(testcase.Class)) + + if testcase.Class.name != "None": + testcase.failIf( + testcase.vcs.detect(testcase.dirname), + "Detected %(name)s VCS before initialising" + % vars(testcase.Class)) + + testcase.vcs.init(testcase.dirname) + + +class VCSTestCase(unittest.TestCase): + """Test cases for base VCS class.""" + + Class = VCS + + def __init__(self, *args, **kwargs): + super(VCSTestCase, self).__init__(*args, **kwargs) + self.dirname = None + + def setUp(self): + super(VCSTestCase, self).setUp() + setup_vcs_test_fixtures(self) + + def tearDown(self): + del(self.vcs) + super(VCSTestCase, self).tearDown() + + def full_path(self, rel_path): + return os.path.join(self.dirname, rel_path) + + +class VCS_init_TestCase(VCSTestCase): + """Test cases for VCS.init method.""" + + def test_detect_should_succeed_after_init(self): + """Should detect VCS in directory after initialization.""" + self.failUnless( + self.vcs.detect(self.dirname), + "Did not detect %(name)s VCS after initialising" + % vars(self.Class)) + + def test_vcs_rootdir_in_specified_root_path(self): + """VCS root directory should be in specified root path.""" + rp = os.path.realpath(self.vcs.rootdir) + dp = os.path.realpath(self.dirname) + vcs_name = self.Class.name + self.failUnless( + dp == rp or rp == None, + "%(vcs_name)s VCS root in wrong dir (%(dp)s %(rp)s)" % vars()) + + +class VCS_get_user_id_TestCase(VCSTestCase): + """Test cases for VCS.get_user_id method.""" + + def test_gets_existing_user_id(self): + """Should get the existing user ID.""" + if not self.vcs_supports_uninitialized_user_id: + return + + user_id = self.vcs.get_user_id() + self.failUnless( + user_id is not None, + "unable to get a user id") + + +class VCS_set_user_id_TestCase(VCSTestCase): + """Test cases for VCS.set_user_id method.""" + + def setUp(self): + super(VCS_set_user_id_TestCase, self).setUp() + + if self.vcs_supports_uninitialized_user_id: + self.prev_user_id = self.vcs.get_user_id() + else: + self.prev_user_id = "Uninitialized identity " + + if self.vcs_supports_set_user_id: + self.test_new_user_id = "John Doe " + self.vcs.set_user_id(self.test_new_user_id) + + def tearDown(self): + if self.vcs_supports_set_user_id: + self.vcs.set_user_id(self.prev_user_id) + super(VCS_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.vcs_supports_set_user_id: + return + self.assertRaises( + SettingIDnotSupported, + self.vcs.set_user_id, "foo") + + def test_updates_user_id_in_supporting_vcs(self): + """Should update the user ID in an VCS that supports it.""" + if not self.vcs_supports_set_user_id: + return + user_id = self.vcs.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_vcs_revision_test_fixtures(testcase): + """Set up revision test fixtures for VCS test case.""" + testcase.test_dirs = ['a', 'a/b', 'c'] + for path in testcase.test_dirs: + testcase.vcs.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 VCS_mkdir_TestCase(VCSTestCase): + """Test cases for VCS.mkdir method.""" + + def setUp(self): + super(VCS_mkdir_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_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 VCS_commit_TestCase(VCSTestCase): + """Test cases for VCS.commit method.""" + + def setUp(self): + super(VCS_commit_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_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.vcs.set_file_contents(full_path, test_contents) + current_contents = self.vcs.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.vcs.set_file_contents(full_path, test_contents) + revision = self.vcs.commit("Initial file contents.") + current_contents = self.vcs.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.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial file contents.") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + current_contents = self.vcs.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.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial file contents.") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + committed_contents = self.vcs.get_file_contents( + full_path, revision) + self.failUnlessEqual( + self.test_contents['rev_1'], committed_contents) + + def test_revision_id_as_committed(self): + """Check for compatibility between .commit() and .revision_id()""" + if not self.vcs.versioned: + self.failUnlessEqual(self.vcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial %s contents." % path) + committed_revisions.append(revision) + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + revision = self.vcs.commit("Altered %s contents." % path) + committed_revisions.append(revision) + for i,revision in enumerate(committed_revisions): + self.failUnlessEqual(self.vcs.revision_id(i), revision) + i += -len(committed_revisions) # check negative indices + self.failUnlessEqual(self.vcs.revision_id(i), revision) + i = len(committed_revisions) + self.failUnlessEqual(self.vcs.revision_id(i), None) + self.failUnlessEqual(self.vcs.revision_id(-i-1), None) + + def test_revision_id_as_committed(self): + """Check revision id before first commit""" + if not self.vcs.versioned: + self.failUnlessEqual(self.vcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + self.failUnlessEqual(self.vcs.revision_id(0), None) + + +class VCS_duplicate_repo_TestCase(VCSTestCase): + """Test cases for VCS.duplicate_repo method.""" + + def setUp(self): + super(VCS_duplicate_repo_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + self.vcs.remove_duplicate_repo() + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_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.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Commit current status") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + dup_repo_path = self.vcs.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.vcs.remove_duplicate_repo() + + +def make_vcs_testcase_subclasses(vcs_class, namespace): + """Make VCSTestCase subclasses for vcs_class in the namespace.""" + vcs_testcase_classes = [ + c for c in ( + ob for ob in globals().values() if isinstance(ob, type)) + if issubclass(c, VCSTestCase)] + + for base_class in vcs_testcase_classes: + testcase_class_name = vcs_class.__name__ + base_class.__name__ + testcase_class_bases = (base_class,) + testcase_class_dict = dict(base_class.__dict__) + testcase_class_dict['Class'] = vcs_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/version.py b/interfaces/web/Bugs-Everywhere-Web/libbe/version.py new file mode 100644 index 0000000..f8eebbd --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/libbe/version.py @@ -0,0 +1,50 @@ +#!/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. + +""" +Store version info for this BE installation. By default, use the +bzr-generated information in _version.py, but allow manual overriding +by setting _VERSION. This allows support of both the "I don't want to +be bothered setting version strings" and the "I want complete control +over the version strings" workflows. +""" + +import libbe._version as _version + +# Manually set a version string (optional, defaults to bzr revision id) +#_VERSION = "1.2.3" + +def version(verbose=False): + """ + Returns the version string for this BE installation. If + verbose==True, the string will include extra lines with more + detail (e.g. bzr branch nickname, etc.). + """ + if "_VERSION" in globals(): + string = _VERSION + else: + string = _version.version_info["revision_id"] + if verbose == True: + string += ("\n" + "revision: %(revno)d\n" + "nick: %(branch_nick)s\n" + "revision id: %(revision_id)s" + % _version.version_info) + return string + +if __name__ == "__main__": + print version(verbose=True) diff --git a/interfaces/xml/be-mbox-to-xml b/interfaces/xml/be-mbox-to-xml index 335f92f..a740117 100644 --- a/interfaces/xml/be-mbox-to-xml +++ b/interfaces/xml/be-mbox-to-xml @@ -25,11 +25,10 @@ followed by a blank line. import base64 import email.utils from libbe.encoding import get_encoding, set_IO_stream_encodings +from libbe.utility import time_to_str from mailbox import mbox, Message # the mailbox people really want an on-disk copy -from time import asctime, gmtime +from time import asctime, gmtime, mktime import types -from xml.sax import make_parser -from xml.sax.handler import ContentHandler from xml.sax.saxutils import escape DEFAULT_ENCODING = get_encoding() @@ -37,14 +36,34 @@ set_IO_stream_encodings(DEFAULT_ENCODING) KNOWN_IDS = [] +def normalize_email_address(address): + """ + Standardize whitespace, etc. + """ + return email.utils.formataddr(email.utils.parseaddr(address)) + +def normalize_RFC_2822_date(date): + """ + Some email clients write non-RFC 2822-compliant date tags like: + Fri, 18 Sep 2009 08:49:02 -0400 (EDT) + with the non-standard (EDT) timezone name. This funtion attempts + to deal with such inconsistencies. + """ + time_tuple = email.utils.parsedate(date) + assert time_tuple != None, \ + 'unparsable date: "%s"' % date + return time_to_str(mktime(time_tuple)) + 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'author'] = normalize_email_address(message[u'from']) new_fields[u'date'] = message[u'date'] + if new_fields[u'date'] != None: + new_fields[u'date'] = normalize_RFC_2822_date(new_fields[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: @@ -67,25 +86,27 @@ def comment_message_to_xml(message, fields=None): 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() + found_ref = False for ref in refs: # search for a known reference id. if ref in KNOWN_IDS: fields[u'in-reply-to'] = ref + found_ref = True break - if fields[u'in-reply-to'] == None and len(refs) > 0: + if found_ref == False 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 fields[u'alt-id'] != None: + KNOWN_IDS.append(fields[u'alt-id']) if message.is_multipart(): ret = [] alt_id = fields[u'alt-id'] - from_str = fields[u'from'] + from_str = fields[u'author'] date = fields[u'date'] for m in message.walk(): if m == message: continue - fields[u'from'] = from_str + fields[u'author'] = 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 diff --git a/interfaces/xml/be-xml-to-mbox b/interfaces/xml/be-xml-to-mbox index ea77c34..c630447 100644 --- a/interfaces/xml/be-xml-to-mbox +++ b/interfaces/xml/be-xml-to-mbox @@ -129,7 +129,7 @@ class Comment (LimitedAttrDict): u"alt-id", u"short-name", u"in-reply-to", - u"from", + u"author", u"date", u"content-type", u"body"] @@ -137,7 +137,7 @@ class Comment (LimitedAttrDict): if bug == None: bug = Bug() bug[u"uuid"] = u"no-uuid" - name,addr = email.utils.parseaddr(self["from"]) + name,addr = email.utils.parseaddr(self["author"]) print "From %s %s" % (addr, rfc2822_to_asctime(self["date"])) if "uuid" in self: id = self["uuid"] elif "alt-id" in self: id = self["alt-id"] @@ -145,7 +145,7 @@ class Comment (LimitedAttrDict): if id != None: print "Message-ID: <%s@%s>" % (id, DEFAULT_DOMAIN) print "Date: %s" % self["date"] - print "From: %s" % self["from"] + print "From: %s" % self["author"] subject = "" if "short-name" in self: subject += self["short-name"]+u": " diff --git a/libbe/arch.py b/libbe/arch.py index 2f45aa9..ab55172 100644 --- a/libbe/arch.py +++ b/libbe/arch.py @@ -17,6 +17,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +GNU Arch (tla) backend. +""" + import codecs import os import re @@ -26,10 +30,11 @@ import time import unittest import doctest -import config from beuuid import uuid_gen -import rcs -from rcs import RCS +import config +import vcs + + DEFAULT_CLIENT = "tla" @@ -38,7 +43,7 @@ client = config.get_val("arch_client", default=DEFAULT_CLIENT) def new(): return Arch() -class Arch(RCS): +class Arch(vcs.VCS): name = "Arch" client = client versioned = True @@ -48,21 +53,25 @@ class Arch(RCS): _project_name = None _tmp_project = False _arch_paramdir = os.path.expanduser("~/.arch-params") - def _rcs_help(self): + def _vcs_help(self): status,output,error = self._u_invoke_client("--help") return output - def _rcs_detect(self, path): + def _vcs_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): + def _vcs_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 + """ + Create a temporary Arch archive in the directory PATH. This + archive will be removed by + __del__->cleanup->_vcs_cleanup->_remove_archive + """ # http://regexps.srparish.net/tutorial-tla/new-archive.html#Creating_a_New_Archive assert self._archive_name == None id = self.get_user_id() @@ -103,7 +112,7 @@ class Arch(RCS): """ Create a temporary Arch project in the directory PATH. This project will be removed by - __del__->cleanup->_rcs_cleanup->_remove_project + __del__->cleanup->_vcs_cleanup->_remove_project """ # http://mwolson.org/projects/GettingStartedWithArch.html # http://regexps.srparish.net/tutorial-tla/new-project.html#Starting_a_New_Project @@ -159,13 +168,13 @@ class Arch(RCS): self._adjust_naming_conventions(path) self._invoke_client("import", "--summary", "Began versioning", directory=path) - def _rcs_cleanup(self): + def _vcs_cleanup(self): if self._tmp_project == True: self._remove_project() if self._tmp_archive == True: self._remove_archive() - def _rcs_root(self, path): + def _vcs_root(self, path): if not os.path.isdir(path): dirname = os.path.dirname(path) else: @@ -176,7 +185,6 @@ class Arch(RCS): 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') @@ -188,7 +196,6 @@ class Arch(RCS): 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) @@ -197,7 +204,7 @@ class Arch(RCS): archive_name,project_name = output.rstrip('\n').split('/') self._archive_name = archive_name self._project_name = project_name - def _rcs_get_user_id(self): + def _vcs_get_user_id(self): try: status,output,error = self._u_invoke_client('my-id') return output.rstrip('\n') @@ -206,9 +213,9 @@ class Arch(RCS): return None else: raise - def _rcs_set_user_id(self, value): + def _vcs_set_user_id(self, value): self._u_invoke_client('my-id', value) - def _rcs_add(self, path): + def _vcs_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) @@ -237,14 +244,14 @@ class Arch(RCS): 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): + def _vcs_remove(self, path): if not '.arch-ids' in path: self._u_invoke_client("delete-id", path) - def _rcs_update(self, path): + def _vcs_update(self, path): pass - def _rcs_get_file_contents(self, path, revision=None, binary=False): + def _vcs_get_file_contents(self, path, revision=None, binary=False): if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) else: status,output,error = \ self._invoke_client("file-find", path, revision) @@ -254,18 +261,18 @@ class Arch(RCS): contents = f.read() f.close() return contents - def _rcs_duplicate_repo(self, directory, revision=None): + def _vcs_duplicate_repo(self, directory, revision=None): if revision == None: - RCS._rcs_duplicate_repo(self, directory, revision) + vcs.VCS._vcs_duplicate_repo(self, directory, revision) else: status,output,error = \ self._u_invoke_client("get", revision,directory) - def _rcs_commit(self, commitfile, allow_empty=False): + def _vcs_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() + raise vcs.EmptyCommit() summary,body = self._u_parse_commitfile(commitfile) args = ["commit", "--summary", summary] if body != None: @@ -281,6 +288,16 @@ class Arch(RCS): assert revpath.startswith(self._archive_project_name()+'--') revision = revpath[len(self._archive_project_name()+'--'):] return revpath + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("logs") + logs = output.splitlines() + first_log = logs.pop(0) + assert first_log == "base-0", first_log + try: + log = logs[index] + except IndexError: + return None + return "%s--%s" % (self._archive_project_name(), log) class CantAddFile(Exception): def __init__(self, file): @@ -289,7 +306,7 @@ class CantAddFile(Exception): -rcs.make_rcs_testcase_subclasses(Arch, sys.modules[__name__]) +vcs.make_vcs_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 bc47208..490ed62 100644 --- a/libbe/beuuid.py +++ b/libbe/beuuid.py @@ -13,6 +13,7 @@ # 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 @@ -20,6 +21,7 @@ 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(): diff --git a/libbe/bug.py b/libbe/bug.py index c1e5481..fd30ff7 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -15,6 +15,11 @@ # 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. + +""" +Define the Bug class for representing bugs. +""" + import os import os.path import errno @@ -33,6 +38,11 @@ import comment import utility +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) + ### 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/ @@ -216,15 +226,15 @@ class Bug(settings_object.SavedSettingsObject): @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 + def _get_vcs(self): + if hasattr(self.bugdir, "vcs"): + return self.bugdir.vcs @Property - @cached_property(generator=_get_rcs) - @local_property("rcs") + @cached_property(generator=_get_vcs) + @local_property("vcs") @doc_property(doc="A revision control system instance.") - def rcs(): return {} + def vcs(): return {} def __init__(self, bugdir=None, uuid=None, from_disk=False, load_comments=False, summary=None): @@ -238,17 +248,20 @@ class Bug(settings_object.SavedSettingsObject): 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() + if self.vcs != None: + self.creator = self.vcs.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 __str__(self): + return self.string(shortlist=True) + + def __cmp__(self, other): + return cmp_full(self, other) + + # serializing methods def _setting_attr_string(self, setting): value = getattr(self, setting) @@ -331,43 +344,34 @@ class Bug(settings_object.SavedSettingsObject): output = bugout return output - def __str__(self): - return self.string(shortlist=True) + # methods for saving/loading/acessing settings and properties. - def __cmp__(self, other): - return cmp_full(self, other) + def get_path(self, *args): + dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid) + if len(args) == 0: + return dir + assert args[0] in ["values", "comments"], str(args) + return os.path.join(dir, *args) - 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 set_sync_with_disk(self, value): + self.sync_with_disk = value + for comment in self.comments(): + comment.set_sync_with_disk(value) def load_settings(self): - self.settings = mapfile.map_load(self.rcs, self.get_path("values")) + if self.sync_with_disk == False: + raise DiskAccessRequired("load settings") + self.settings = mapfile.map_load(self.vcs, 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): + if self.sync_with_disk == False: + raise DiskAccessRequired("save settings") assert self.summary != None, "Can't save blank bug" - - self.rcs.mkdir(self.get_path()) + self.vcs.mkdir(self.get_path()) path = self.get_path("values") - mapfile.map_save(self.rcs, path, self._get_saved_settings()) - + mapfile.map_save(self.vcs, path, self._get_saved_settings()) + def save(self): """ Save any loaded contents to disk. Because of lazy loading of @@ -378,15 +382,39 @@ class Bug(settings_object.SavedSettingsObject): calling this method will just waste time (unless something else has been messing with your on-disk files). """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) self.save_settings() if len(self.comment_root) > 0: comment.saveComments(self) + if sync_with_disk == False: + self.set_sync_with_disk(False) + + def load_comments(self, load_full=True): + if self.sync_with_disk == False: + raise DiskAccessRequired("load comments") + 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 remove(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("remove") self.comment_root.remove() path = self.get_path() - self.rcs.recursive_remove(path) + self.vcs.recursive_remove(path) + # methods for managing comments + def comments(self): for comment in self.comment_root.traverse(): yield comment @@ -489,8 +517,12 @@ def cmp_attr(bug_1, bug_2, attr, invert=False): return cmp(val_1, val_2) # alphabetical rankings (a < z) +cmp_uuid = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "uuid") 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") +cmp_target = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "target") +cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter") +cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary") # chronological rankings (newer < older) cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True) @@ -512,7 +544,8 @@ def cmp_comments(bug_1, bug_2): return 0 DEFAULT_CMP_FULL_CMP_LIST = \ - (cmp_status,cmp_severity,cmp_assigned,cmp_time,cmp_creator,cmp_comments) + (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator, + cmp_reporter, cmp_target, cmp_comments, cmp_summary, cmp_uuid) class BugCompoundComparator (object): def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 6e020ee..c4f0f91 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -17,23 +17,30 @@ # 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. + +""" +Define the BugDir class for representing bug comments. +""" + +import copy +import errno import os import os.path -import errno +import sys import time -import copy import unittest import doctest +import bug +import encoding 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 vcs +import settings_object +import upgrade import utility @@ -62,8 +69,16 @@ class MultipleBugMatches(ValueError): self.shortname = shortname self.matches = matches +class NoBugMatches(KeyError): + def __init__(self, shortname): + msg = "No bug matches %s" % shortname + KeyError.__init__(self, msg) + self.shortname = shortname -TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n" +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) class BugDir (list, settings_object.SavedSettingsObject): @@ -99,7 +114,8 @@ class BugDir (list, settings_object.SavedSettingsObject): 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. + changed in memory, a call to save() immediately before the + .set_sync_with_disk(True) call 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. @@ -107,15 +123,14 @@ class BugDir (list, settings_object.SavedSettingsObject): 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)). + loads new settings/bugs/comments that it doesn't already have in + memory and .sync_with_disk == True. - Allow RCS initialization + Allow VCS 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 + BugDir to search for an installed VCS backend and initialize it in the root directory. This is a convenience option for supporting tests of versioning functionality (e.g. .duplicate_bugdir). @@ -172,9 +187,9 @@ class BugDir (list, settings_object.SavedSettingsObject): def encoding(): return {} def _setup_user_id(self, user_id): - self.rcs.user_id = user_id + self.vcs.user_id = user_id def _guess_user_id(self): - return self.rcs.get_user_id() + return self.vcs.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) @@ -182,7 +197,7 @@ class BugDir (list, settings_object.SavedSettingsObject): @_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.""", +that the Arch VCS backend *enforces* ids with this format.""", change_hook=_set_user_id, generator=_guess_user_id) def user_id(): return {} @@ -192,32 +207,32 @@ that the Arch RCS backend *enforces* ids with this format.""", """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.""", + @_versioned_property(name="vcs_name", + doc="""The name of the current VCS. Kept seperate to make saving/loading +settings easy. Don't set this attribute. Set .vcs instead, and +.vcs_name will be automatically adjusted.""", default="None", allowed=["None", "Arch", "bzr", "darcs", "git", "hg"]) - def rcs_name(): return {} + def vcs_name(): return {} - def _get_rcs(self, rcs_name=None): + def _get_vcs(self, vcs_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 + if vcs_name == None: + vcs_name = self.vcs_name + new_vcs = vcs.vcs_by_name(vcs_name) + self._change_vcs(None, new_vcs) + return new_vcs + def _change_vcs(self, old_vcs, new_vcs): + new_vcs.encoding = self.encoding + new_vcs.root(self.root) + self.vcs_name = new_vcs.name @Property - @change_hook_property(hook=_change_rcs) - @cached_property(generator=_get_rcs) - @local_property("rcs") + @change_hook_property(hook=_change_vcs) + @cached_property(generator=_get_vcs) + @local_property("vcs") @doc_property(doc="A revision control system instance.") - def rcs(): return {} + def vcs(): return {} def _bug_map_gen(self): map = {} @@ -279,9 +294,8 @@ settings easy. Don't set this attribute. Set .rcs instead, and 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): + assert_new_BugDir=False, allow_vcs_init=False, + manipulate_encodings=True, from_disk=False, vcs=None): list.__init__(self) settings_object.SavedSettingsObject.__init__(self) self._manipulate_encodings = manipulate_encodings @@ -293,9 +307,9 @@ settings easy. Don't set this attribute. Set .rcs instead, and if not os.path.exists(root): raise NoRootEntry(root) self.root = root - # get a temporary rcs until we've loaded settings + # get a temporary vcs until we've loaded settings self.sync_with_disk = False - self.rcs = self._guess_rcs() + self.vcs = self._guess_vcs() if from_disk == True: self.sync_with_disk = True @@ -305,20 +319,24 @@ settings easy. Don't set this attribute. Set .rcs instead, and 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 + if vcs == None: + vcs = self._guess_vcs(allow_vcs_init) + self.vcs = vcs 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 __del__(self): + self.cleanup() + + def cleanup(self): + self.vcs.cleanup() + + # methods for getting the BugDir situated in the filesystem def _find_root(self, path): """ Search for an existing bug database dir and it's ancestors and - return a BugDir rooted there. + return a BugDir rooted there. Only called by __init__, and + then only if sink_to_existing_root == True. """ if not os.path.exists(path): raise NoRootEntry(path) @@ -334,136 +352,212 @@ settings easy. Don't set this attribute. Set .rcs instead, and 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() + def _guess_vcs(self, allow_vcs_init=False): + """ + Only called by __init__. + """ + deepdir = self.get_path() + if not os.path.exists(deepdir): + deepdir = os.path.dirname(deepdir) + new_vcs = vcs.detect_vcs(deepdir) + install = False + if new_vcs.name == "None": + if allow_vcs_init == True: + new_vcs = vcs.installed_vcs() + new_vcs.init(self.root) + return new_vcs + + # methods for saving/loading/accessing settings and properties. + + def get_path(self, *args): + """ + Return a path relative to .root. + """ + dir = os.path.join(self.root, ".be") + if len(args) == 0: + return dir + assert args[0] in ["version", "settings", "bugs"], str(args) + return os.path.join(dir, *args) + + def _get_settings(self, settings_path, for_duplicate_bugdir=False): + allow_no_vcs = not self.vcs.path_in_root(settings_path) + if allow_no_vcs == True: + assert for_duplicate_bugdir == True + if self.sync_with_disk == False and for_duplicate_bugdir == False: + # duplicates can ignore this bugdir's .sync_with_disk status + raise DiskAccessRequired("_get settings") + try: + settings = mapfile.map_load(self.vcs, settings_path, allow_no_vcs) + except vcs.NoSuchFile: + settings = {"vcs_name": "None"} + return settings + + def _save_settings(self, settings_path, settings, + for_duplicate_bugdir=False): + allow_no_vcs = not self.vcs.path_in_root(settings_path) + if allow_no_vcs == True: + assert for_duplicate_bugdir == True + if self.sync_with_disk == False and for_duplicate_bugdir == False: + # duplicates can ignore this bugdir's .sync_with_disk status + raise DiskAccessRequired("_save settings") + self.vcs.mkdir(self.get_path(), allow_no_vcs) + mapfile.map_save(self.vcs, settings_path, settings, allow_no_vcs) + + 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) + self.vcs = vcs.vcs_by_name(self.vcs_name) + self._setup_user_id(self.user_id) + + def save_settings(self): + settings = self._get_saved_settings() + self._save_settings(self.get_path("settings"), settings) + + def get_version(self, path=None, use_none_vcs=False, + for_duplicate_bugdir=False): + """ + Requires disk access. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("get version") + if use_none_vcs == True: + VCS = vcs.vcs_by_name("None") + VCS.root(self.root) + VCS.encoding = encoding.get_encoding() else: - RCS = self.rcs + VCS = self.vcs if path == None: path = self.get_path("version") - tree_version = RCS.get_file_contents(path) - return tree_version + allow_no_vcs = not VCS.path_in_root(path) + if allow_no_vcs == True: + assert for_duplicate_bugdir == True + version = VCS.get_file_contents( + path, allow_no_vcs=allow_no_vcs).rstrip("\n") + return version def set_version(self): - self.rcs.mkdir(self.get_path()) - self.rcs.set_file_contents(self.get_path("version"), - TREE_VERSION_STRING) + """ + Requires disk access. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("set version") + self.vcs.mkdir(self.get_path()) + self.vcs.set_file_contents(self.get_path("version"), + upgrade.BUGDIR_DISK_VERSION+"\n") - 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) + # methods controlling disk access - 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 set_sync_with_disk(self, value): + """ + Adjust .sync_with_disk for the BugDir and all it's children. + See the BugDir docstring for a description of the role of + .sync_with_disk. + """ + self.sync_with_disk = value + for bug in self: + bug.set_sync_with_disk(value) 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 + """ + Reqires disk access + """ + version = self.get_version(use_none_vcs=True) + if version != upgrade.BUGDIR_DISK_VERSION: + upgrade.upgrade(self.root, 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." + """ + Requires disk access. + Warning: this could take a while. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("load all bugs") self._clear_bugs() for uuid in self.list_uuids(): self._load_bug(uuid) def save(self): """ + Note that this command writes to disk _regardless_ of the + status of .sync_with_disk. + 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 + However, if .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). + + Requires disk access. """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) self.set_version() self.save_settings() for bug in self: bug.save() + if sync_with_disk == False: + self.set_sync_with_disk(sync_with_disk) - 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) + # methods for managing duplicate BugDirs - 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 + def duplicate_bugdir(self, revision): + duplicate_path = self.vcs.duplicate_repo(revision) + duplicate_version_path = os.path.join(duplicate_path, ".be", "version") 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) + version = self.get_version(duplicate_version_path, + for_duplicate_bugdir=True) + except DiskAccessRequired: + self.sync_with_disk = True # temporarily allow access + version = self.get_version(duplicate_version_path, + for_duplicate_bugdir=True) + self.sync_with_disk = False + if version != upgrade.BUGDIR_DISK_VERSION: + upgrade.upgrade(duplicate_path, version) - # setup revision RCS as None, since the duplicate may not be + # setup revision VCS 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 = self._get_settings(duplicate_settings_path, + for_duplicate_bugdir=True) + if "vcs_name" in duplicate_settings: + duplicate_settings["vcs_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) + self._save_settings(duplicate_settings_path, duplicate_settings, + for_duplicate_bugdir=True) return BugDir(duplicate_path, from_disk=True, manipulate_encodings=self._manipulate_encodings) def remove_duplicate_bugdir(self): - self.rcs.remove_duplicate_repo() + self.vcs.remove_duplicate_repo() + + # methods for managing bugs def list_uuids(self): uuids = [] - if os.path.exists(self.get_path()): + if self.sync_with_disk == True and 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 + if os.path.exists(self.get_path("bugs")): + 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: @@ -476,6 +570,8 @@ settings easy. Don't set this attribute. Set .rcs instead, and self._bug_map_gen() def _load_bug(self, uuid): + if self.sync_with_disk == False: + raise DiskAccessRequired("_load bug") bg = bug.Bug(bugdir=self, uuid=uuid, from_disk=True) self.append(bg) self._bug_map_gen() @@ -492,7 +588,8 @@ settings easy. Don't set this attribute. Set .rcs instead, and def remove_bug(self, bug): self.remove(bug) - bug.remove() + if bug.sync_with_disk == True: + bug.remove() def bug_shortname(self, bug): """ @@ -514,12 +611,13 @@ settings easy. Don't set this attribute. Set .rcs instead, and def bug_from_shortname(self, shortname): """ - >>> bd = simple_bug_dir() + >>> bd = SimpleBugDir(sync_with_disk=False) >>> bug_a = bd.bug_from_shortname('a') >>> print type(bug_a) >>> print bug_a a:om: Bug A + >>> bd.cleanup() """ matches = [] self._bug_map_gen() @@ -530,7 +628,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and raise MultipleBugMatches(shortname, matches) if len(matches) == 1: return self.bug_from_uuid(matches[0]) - raise KeyError("No bug matches %s" % shortname) + raise NoBugMatches(shortname) def bug_from_uuid(self, uuid): if not self.has_bug(uuid): @@ -548,41 +646,56 @@ settings easy. Don't set this attribute. Set .rcs instead, and return True -def simple_bug_dir(): +class SimpleBugDir (BugDir): """ - For testing - >>> bugdir = simple_bug_dir() - >>> ls = list(bugdir.list_uuids()) - >>> ls.sort() - >>> print ls + For testing. Set sync_with_disk==False for a memory-only bugdir. + >>> bugdir = SimpleBugDir() + >>> uuids = list(bugdir.list_uuids()) + >>> uuids.sort() + >>> print uuids ['a', 'b'] + >>> bugdir.cleanup() """ - dir = utility.Dir() - assert os.path.exists(dir.path) - bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True, + def __init__(self, sync_with_disk=True): + if sync_with_disk == True: + dir = utility.Dir() + assert os.path.exists(dir.path) + root = dir.path + assert_new_BugDir = True + vcs_init = True + else: + root = "/" + assert_new_BugDir = False + vcs_init = False + BugDir.__init__(self, root, sink_to_existing_root=False, + assert_new_BugDir=assert_new_BugDir, + allow_vcs_init=vcs_init, 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 - + if sync_with_disk == True: # postpone cleanup since dir.__del__() removes dir. + self._dir_ref = dir + bug_a = self.new_bug("a", summary="Bug A") + bug_a.creator = "John Doe " + bug_a.time = 0 + bug_b = self.new_bug("b", summary="Bug B") + bug_b.creator = "Jane Doe " + bug_b.time = 0 + bug_b.status = "closed" + if sync_with_disk == True: + self.save() + self.set_sync_with_disk(True) + def cleanup(self): + if hasattr(self, "_dir_ref"): + self._dir_ref.cleanup() + BugDir.cleanup(self) 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 + allow_vcs_init=True) + self.vcs = self.bugdir.vcs def tearDown(self): - self.rcs.cleanup() + self.bugdir.cleanup() self.dir.cleanup() def fullPath(self, path): return os.path.join(self.dir.path, path) @@ -593,13 +706,13 @@ class BugDirTestCase(unittest.TestCase): self.assertRaises(AlreadyInitialized, BugDir, self.dir.path, assertNewBugDir=True) def versionTest(self): - if self.rcs.versioned == False: + if self.vcs.versioned == False: return - original = self.bugdir.rcs.commit("Began versioning") + original = self.bugdir.vcs.commit("Began versioning") bugA = self.bugdir.bug_from_uuid("a") bugA.status = "fixed" self.bugdir.save() - new = self.rcs.commit("Fixed bug a") + new = self.vcs.commit("Fixed bug a") dupdir = self.bugdir.duplicate_bugdir(original) self.failUnless(dupdir.root != self.bugdir.root, "%s, %s" % (dupdir.root, self.bugdir.root)) @@ -645,17 +758,19 @@ class BugDirTestCase(unittest.TestCase): rep.new_reply("And they have six legs.") if sync_with_disk == False: self.bugdir.save() + self.bugdir.set_sync_with_disk(True) self.bugdir._clear_bugs() bug = self.bugdir.bug_from_uuid("a") bug.load_comments() + if sync_with_disk == False: + self.bugdir.set_sync_with_disk(False) 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, + self.failUnless(comment.sync_with_disk == sync_with_disk, comment.sync_with_disk) - #load_settings() self.failUnless(comment.content_type == "text/plain", comment.content_type) self.failUnless(repLoaded.settings["Content-type"]=="text/plain", @@ -672,5 +787,46 @@ class BugDirTestCase(unittest.TestCase): def testSyncedComments(self): self.testComments(sync_with_disk=True) -unitsuite = unittest.TestLoader().loadTestsFromTestCase(BugDirTestCase) -suite = unittest.TestSuite([unitsuite])#, doctest.DocTestSuite()]) +class SimpleBugDirTestCase (unittest.TestCase): + def setUp(self): + # create a pre-existing bugdir in a temporary directory + self.dir = utility.Dir() + self.original_working_dir = os.getcwd() + os.chdir(self.dir.path) + self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, + allow_vcs_init=True) + self.bugdir.new_bug("preexisting", summary="Hopefully not imported") + self.bugdir.save() + def tearDown(self): + os.chdir(self.original_working_dir) + self.bugdir.cleanup() + self.dir.cleanup() + def testOnDiskCleanLoad(self): + """SimpleBugDir(sync_with_disk==True) should not import preexisting bugs.""" + bugdir = SimpleBugDir(sync_with_disk=True) + self.failUnless(bugdir.sync_with_disk==True, bugdir.sync_with_disk) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir._clear_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == [], uuids) + bugdir.load_all_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir.cleanup() + def testInMemoryCleanLoad(self): + """SimpleBugDir(sync_with_disk==False) should not import preexisting bugs.""" + bugdir = SimpleBugDir(sync_with_disk=False) + self.failUnless(bugdir.sync_with_disk==False, bugdir.sync_with_disk) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + self.failUnlessRaises(DiskAccessRequired, bugdir.load_all_bugs) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir._clear_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == [], uuids) + bugdir.cleanup() + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/bzr.py b/libbe/bzr.py index d7cd1e5..e9e0649 100644 --- a/libbe/bzr.py +++ b/libbe/bzr.py @@ -17,61 +17,65 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Bazaar (bzr) backend. +""" + import os import re import sys import unittest import doctest -import rcs -from rcs import RCS +import vcs + def new(): return Bzr() -class Bzr(RCS): +class Bzr(vcs.VCS): name = "bzr" client = "bzr" versioned = True - def _rcs_help(self): + def _vcs_help(self): status,output,error = self._u_invoke_client("--help") return output - def _rcs_detect(self, path): + def _vcs_detect(self, path): if self._u_search_parent_directories(path, ".bzr") != None : return True return False - def _rcs_root(self, path): + def _vcs_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): + def _vcs_init(self, path): self._u_invoke_client("init", directory=path) - def _rcs_get_user_id(self): + def _vcs_get_user_id(self): status,output,error = self._u_invoke_client("whoami") return output.rstrip('\n') - def _rcs_set_user_id(self, value): + def _vcs_set_user_id(self, value): self._u_invoke_client("whoami", value) - def _rcs_add(self, path): + def _vcs_add(self, path): self._u_invoke_client("add", path) - def _rcs_remove(self, path): + def _vcs_remove(self, path): # --force to also remove unversioned files. self._u_invoke_client("remove", "--force", path) - def _rcs_update(self, path): + def _vcs_update(self, path): pass - def _rcs_get_file_contents(self, path, revision=None, binary=False): + def _vcs_get_file_contents(self, path, revision=None, binary=False): if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + return vcs.VCS._vcs_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): + def _vcs_duplicate_repo(self, directory, revision=None): if revision == None: - RCS._rcs_duplicate_repo(self, directory, revision) + vcs.VCS._vcs_duplicate_repo(self, directory, revision) else: self._u_invoke_client("branch", "--revision", revision, ".", directory) - def _rcs_commit(self, commitfile, allow_empty=False): + def _vcs_commit(self, commitfile, allow_empty=False): args = ["commit", "--file", commitfile] if allow_empty == True: args.append("--unchanged") @@ -83,9 +87,9 @@ class Bzr(RCS): 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() + raise vcs.EmptyCommit() else: - raise rcs.CommandError(args, status, error) + raise vcs.CommandError(args, status, stdout="", stderr=error) revision = None revline = re.compile("Committed revision (.*)[.]") match = revline.search(error) @@ -93,9 +97,17 @@ class Bzr(RCS): assert len(match.groups()) == 1 revision = match.groups()[0] return revision + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("revno") + current_revision = int(output) + if index >= current_revision or index < -current_revision: + return None + if index >= 0: + return str(index+1) # bzr commit 0 is the empty tree. + return str(current_revision+index+1) -rcs.make_rcs_testcase_subclasses(Bzr, sys.modules[__name__]) +vcs.make_vcs_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 853a75a..9b64142 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -15,6 +15,11 @@ # 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. + +""" +Define assorted utilities to make command-line handling easier. +""" + import glob import optparse import os @@ -70,10 +75,11 @@ def get_command(command_name): return cmd -def execute(cmd, args): +def execute(cmd, args, manipulate_encodings=True): enc = encoding.get_encoding() cmd = get_command(cmd) - ret = cmd.execute([a.decode(enc) for a in args]) + ret = cmd.execute([a.decode(enc) for a in args], + manipulate_encodings=manipulate_encodings) if ret == None: ret = 0 return ret @@ -206,6 +212,15 @@ def underlined(instring): return "%s\n%s" % (instring, "="*len(instring)) +def bug_from_shortname(bdir, shortname): + """ + Exception translation for the command-line interface. + """ + try: + bug = bdir.bug_from_shortname(shortname) + except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: + raise UserError(e.message) + return bug def _test(): import doctest diff --git a/libbe/comment.py b/libbe/comment.py index 3249e8b..41bc7e6 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -16,6 +16,11 @@ # 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. + +""" +Define the Comment class for representing bug comments. +""" + import base64 import os import os.path @@ -61,6 +66,11 @@ class MissingReference(ValueError): self.reference = comment.in_reply_to self.comment = comment +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) + INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!" def list_to_root(comments, bug, root=None, @@ -115,8 +125,10 @@ 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. """ + if bug.sync_with_disk == False: + raise DiskAccessRequired("load comments") path = bug.get_path("comments") - if not os.path.isdir(path): + if not os.path.exists(path): return Comment(bug, uuid=INVALID_UUID) comments = [] for uuid in os.listdir(path): @@ -131,6 +143,8 @@ def loadComments(bug, load_full=False): return list_to_root(comments, bug) def saveComments(bug): + if bug.sync_with_disk == False: + raise DiskAccessRequired("save comments") for comment in bug.comment_root.traverse(): comment.save() @@ -162,9 +176,9 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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", + @_versioned_property(name="Author", doc="The author of the comment") - def From(): return {} + def author(): return {} @_versioned_property(name="In-reply-to", doc="UUID for parent comment or bug") @@ -178,28 +192,28 @@ class Comment(Tree, settings_object.SavedSettingsObject): @_versioned_property(name="Date", doc="An RFC 2822 timestamp for comment creation") - def time_string(): return {} + def date(): return {} def _get_time(self): - if self.time_string == None: + if self.date == None: return None - return utility.str_to_time(self.time_string) + return utility.str_to_time(self.date) def _set_time(self, value): - self.time_string = utility.time_to_str(value) + self.date = utility.time_to_str(value) time = property(fget=_get_time, fset=_set_time, - doc="An integer version of .time_string") + doc="An integer version of .date") def _get_comment_body(self): - if self.rcs != None and self.sync_with_disk == True: - import rcs + if self.vcs != None and self.sync_with_disk == True: + import vcs binary = not self.content_type.startswith("text/") - return self.rcs.get_file_contents(self.get_path("body"), binary=binary) + return self.vcs.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: + if (self.vcs != 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) + self.vcs.set_file_contents(self.get_path("body"), new, binary=binary) @Property @change_hook_property(hook=_set_comment_body) @@ -208,15 +222,15 @@ class Comment(Tree, settings_object.SavedSettingsObject): @doc_property(doc="The meat of the comment") def body(): return {} - def _get_rcs(self): - if hasattr(self.bug, "rcs"): - return self.bug.rcs + def _get_vcs(self): + if hasattr(self.bug, "vcs"): + return self.bug.vcs @Property - @cached_property(generator=_get_rcs) - @local_property("rcs") + @cached_property(generator=_get_vcs) + @local_property("vcs") @doc_property(doc="A revision control system instance.") - def rcs(): return {} + def vcs(): return {} def _extra_strings_check_fn(value): return utility.iterable_full_of_strings(value, \ @@ -257,13 +271,29 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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() + if self.vcs != None: + self.author = self.vcs.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 __cmp__(self, other): + return cmp_full(self, other) + + def __str__(self): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> comm.uuid = "com-1" + >>> comm.date = "Thu, 20 Nov 2008 15:55:11 +0000" + >>> comm.author = "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 traverse(self, *args, **kwargs): """Avoid working with the possible dummy root comment""" @@ -272,6 +302,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): continue yield comment + # serializing methods + def _setting_attr_string(self, setting): value = getattr(self, setting) if value == None: @@ -282,12 +314,12 @@ class Comment(Tree, settings_object.SavedSettingsObject): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> comm.uuid = "0123" - >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> comm.date = "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 @@ -309,8 +341,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): ("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), + ("author", self._setting_attr_string("author")), + ("date", self.date), ("content-type", self.content_type), ("body", body)] lines = [""] @@ -328,11 +360,11 @@ class Comment(Tree, settings_object.SavedSettingsObject): fields. >>> commA = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> commA.uuid = "0123" - >>> commA.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> commA.date = "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'] + >>> attrs=['uuid','alt_id','in_reply_to','author','date','content_type','body'] >>> for attr in attrs: # doctest: +ELLIPSIS ... if getattr(commB, attr) != getattr(commA, attr): ... estr = "Mismatch on %s: '%s' should be '%s'" @@ -342,15 +374,15 @@ class Comment(Tree, settings_object.SavedSettingsObject): Mismatch on alt_id: '0123' should be 'None' >>> print commB.alt_id 0123 - >>> commA.From - >>> commB.From + >>> commA.author + >>> commB.author """ 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'] + tags=['uuid','alt-id','in-reply-to','author','date','content-type','body'] uuid = None body = None for child in comment.getchildren(): @@ -368,10 +400,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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) @@ -389,7 +417,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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" + >>> comm.date = "Thu, 01 Jan 1970 00:00:00 +0000" >>> print comm.string(indent=2, shortname="com-1") --------- Comment --------- Name: com-1 @@ -405,8 +433,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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("From: %s" % (self._setting_attr_string("author"))) + lines.append("Date: %s" % self.date) lines.append("") if self.content_type.startswith("text/"): lines.extend((self.body or "").splitlines()) @@ -417,78 +445,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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): @@ -506,7 +462,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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.sort(key=lambda c : c.author) # your sort comm.string_thread(name_map=name_map) >>> a = Comment(bug=None, uuid="a", body="Insightful remarks") @@ -593,6 +549,77 @@ class Comment(Tree, settings_object.SavedSettingsObject): indent=indent, auto_name_map=auto_name_map, bug_shortname=bug_shortname) + # methods for saving/loading/acessing settings and properties. + + def get_path(self, *args): + dir = os.path.join(self.bug.get_path("comments"), self.uuid) + if len(args) == 0: + return dir + assert args[0] in ["values", "body"], str(args) + return os.path.join(dir, *args) + + def set_sync_with_disk(self, value): + self.sync_with_disk = value + + def load_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("load settings") + self.settings = mapfile.map_load(self.vcs, self.get_path("values")) + self._setup_saved_settings() + + def save_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("save settings") + self.vcs.mkdir(self.get_path()) + path = self.get_path("values") + mapfile.map_save(self.vcs, 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). + """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) + assert self.body != None, "Can't save blank comment" + self.save_settings() + self._set_comment_body(new=self.body, force=True) + if sync_with_disk == False: + self.set_sync_with_disk(False) + + def remove(self): + if self.sync_with_disk == False and self.uuid != INVALID_UUID: + raise DiskAccessRequired("remove") + for comment in self.traverse(): + path = comment.get_path() + self.vcs.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) + + 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 comment_shortnames(self, bug_shortname=None): """ Iterate through (id, comment) pairs, in time order. @@ -659,4 +686,59 @@ class Comment(Tree, settings_object.SavedSettingsObject): return comment raise KeyError(uuid) +def cmp_attr(comment_1, comment_2, attr, invert=False): + """ + Compare a general attribute between two comments using the conventional + comparison rule for that attribute type. If invert == True, sort + *against* that convention. + >>> attr="author" + >>> commentA = Comment() + >>> commentB = Comment() + >>> commentA.author = "John Doe" + >>> commentB.author = "Jane Doe" + >>> cmp_attr(commentA, commentB, attr) > 0 + True + >>> cmp_attr(commentA, commentB, attr, invert=True) < 0 + True + >>> commentB.author = "John Doe" + >>> cmp_attr(commentA, commentB, attr) == 0 + True + """ + if not hasattr(comment_2, attr) : + return 1 + val_1 = getattr(comment_1, attr) + val_2 = getattr(comment_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_uuid = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "uuid") +cmp_author = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "author") +cmp_in_reply_to = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "in_reply_to") +cmp_content_type = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "content_type") +cmp_body = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "body") +# chronological rankings (newer < older) +cmp_time = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "time", invert=True) + +DEFAULT_CMP_FULL_CMP_LIST = \ + (cmp_time, cmp_author, cmp_content_type, cmp_body, cmp_in_reply_to, + cmp_uuid) + +class CommentCompoundComparator (object): + def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): + self.cmp_list = cmp_list + def __call__(self, comment_1, comment_2): + for comparison in self.cmp_list : + val = comparison(comment_1, comment_2) + if val != 0 : + return val + return 0 + +cmp_full = CommentCompoundComparator() + suite = doctest.DocTestSuite() diff --git a/libbe/config.py b/libbe/config.py index 5e343b9..fb5a028 100644 --- a/libbe/config.py +++ b/libbe/config.py @@ -14,6 +14,11 @@ # 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, save, and load the per-user config file at path(). +""" + import ConfigParser import codecs import locale @@ -21,6 +26,7 @@ import os.path import sys import doctest + default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() def path(): diff --git a/libbe/darcs.py b/libbe/darcs.py index e7132c0..16005f2 100644 --- a/libbe/darcs.py +++ b/libbe/darcs.py @@ -14,31 +14,40 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Darcs backend. +""" + import codecs import os import re import sys -import unittest +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 import doctest +import unittest + +import vcs -import rcs -from rcs import RCS def new(): return Darcs() -class Darcs(RCS): +class Darcs(vcs.VCS): name="darcs" client="darcs" versioned=True - def _rcs_help(self): + def _vcs_help(self): status,output,error = self._u_invoke_client("--help") return output - def _rcs_detect(self, path): + def _vcs_detect(self, path): if self._u_search_parent_directories(path, "_darcs") != None : return True return False - def _rcs_root(self, path): + def _vcs_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. @@ -48,9 +57,9 @@ class Darcs(RCS): if darcs_dir == None: return None return os.path.dirname(darcs_dir) - def _rcs_init(self, path): + def _vcs_init(self, path): self._u_invoke_client("init", directory=path) - def _rcs_get_user_id(self): + def _vcs_get_user_id(self): # following http://darcs.net/manual/node4.html#SECTION00410030000000000000 # as of June 29th, 2009 if self.rootdir == None: @@ -65,32 +74,32 @@ class Darcs(RCS): if env_variable in os.environ: return os.environ[env_variable] return None - def _rcs_set_user_id(self, value): + def _vcs_set_user_id(self, value): if self.rootdir == None: self.root(".") if self.rootdir == None: - raise rcs.SettingIDnotSupported + raise vcs.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): + def _vcs_add(self, path): if os.path.isdir(path): return self._u_invoke_client("add", path) - def _rcs_remove(self, path): + def _vcs_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): + def _vcs_update(self, path): pass # darcs notices changes - def _rcs_get_file_contents(self, path, revision=None, binary=False): + def _vcs_get_file_contents(self, path, revision=None, binary=False): if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, + return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) else: try: return self._u_invoke_client("show", "contents", "--patch", revision, path) - except rcs.CommandError: + except vcs.CommandError: # Darcs versions < 2.0.0pre2 lack the "show contents" command status,output,error = self._u_invoke_client("diff", "--unified", @@ -113,7 +122,7 @@ class Darcs(RCS): 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, + contents = vcs.VCS._vcs_get_file_contents(self, path, binary=binary) else: contents = "" @@ -123,41 +132,53 @@ class Darcs(RCS): 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, + current_contents = vcs.VCS._vcs_get_file_contents(self, path, binary=binary) return contents - def _rcs_duplicate_repo(self, directory, revision=None): + def _vcs_duplicate_repo(self, directory, revision=None): if revision==None: - RCS._rcs_duplicate_repo(self, directory, revision) + vcs.VCS._vcs_duplicate_repo(self, directory, revision) else: self._u_invoke_client("put", "--to-patch", revision, directory) - def _rcs_commit(self, commitfile, allow_empty=False): + def _vcs_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... + raise vcs.EmptyCommit() + # note that darcs does _not_ make an empty revision. + # this returns the last non-empty revision id... + revision = self._vcs_revision_id(-1) 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] + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 1 + revision = match.groups()[0] return revision - + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("changes", "--xml") + revisions = [] + xml_str = output.encode("unicode_escape").replace(r"\n", "\n") + element = ElementTree.XML(xml_str) + assert element.tag == "changelog", element.tag + for patch in element.getchildren(): + assert patch.tag == "patch", patch.tag + for child in patch.getchildren(): + if child.tag == "name": + text = unescape(unicode(child.text).decode("unicode_escape").strip()) + revisions.append(text) + revisions.reverse() + try: + return revisions[index] + except IndexError: + return None -rcs.make_rcs_testcase_subclasses(Darcs, sys.modules[__name__]) +vcs.make_vcs_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 ba48efc..9253a23 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -14,112 +14,406 @@ # 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 + +"""Compare two bug trees.""" + +import difflib 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) +from libbe import bugdir, bug, settings_object, tree +from libbe.utility import time_to_str -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") +class DiffTree (tree.Tree): + """ + A tree holding difference data for easy report generation. + >>> bugdir = DiffTree("bugdir") + >>> bdsettings = DiffTree("settings", data="target: None -> 1.0") + >>> bugdir.append(bdsettings) + >>> bugs = DiffTree("bugs", "bug-count: 5 -> 6") + >>> bugdir.append(bugs) + >>> new = DiffTree("new", "new bugs: ABC, DEF") + >>> bugs.append(new) + >>> rem = DiffTree("rem", "removed bugs: RST, UVW") + >>> bugs.append(rem) + >>> print bugdir.report_string() + target: None -> 1.0 + bug-count: 5 -> 6 + new bugs: ABC, DEF + removed bugs: RST, UVW + >>> print "\\n".join(bugdir.paths()) + bugdir + bugdir/settings + bugdir/bugs + bugdir/bugs/new + bugdir/bugs/rem + >>> bugdir.child_by_path("/") == bugdir + True + >>> bugdir.child_by_path("/bugs") == bugs + True + >>> bugdir.child_by_path("/bugs/rem") == rem + True + >>> bugdir.child_by_path("bugdir") == bugdir + True + >>> bugdir.child_by_path("bugdir/") == bugdir + True + >>> bugdir.child_by_path("bugdir/bugs") == bugs + True + >>> bugdir.child_by_path("/bugs").masked = True + >>> print bugdir.report_string() + target: None -> 1.0 + """ + def __init__(self, name, data=None, data_part_fn=str, + requires_children=False, masked=False): + tree.Tree.__init__(self) + self.name = name + self.data = data + self.data_part_fn = data_part_fn + self.requires_children = requires_children + self.masked = masked + def paths(self, parent_path=None): + paths = [] + if parent_path == None: + path = self.name + else: + path = "%s/%s" % (parent_path, self.name) + paths.append(path) + for child in self: + paths.extend(child.paths(path)) + return paths + def child_by_path(self, path): + if hasattr(path, "split"): # convert string path to a list of names + names = path.split("/") + if names[0] == "": + names[0] = self.name # replace root with self + if len(names) > 1 and names[-1] == "": + names = names[:-1] # strip empty tail + else: # it was already an array + names = path + assert len(names) > 0, path + if names[0] == self.name: + if len(names) == 1: + return self + for child in self: + if names[1] == child.name: + return child.child_by_path(names[1:]) + if len(names) == 1: + raise KeyError, "%s doesn't match '%s'" % (names, self.name) + raise KeyError, "%s points to child not in %s" % (names, [c.name for c in self]) + def report_string(self): + return "\n".join(self.report()) + def report(self, root=None, parent=None, depth=0): + if root == None: + root = self.make_root() + if self.masked == True: + return None + data_part = self.data_part(depth) + if self.requires_children == True and len(self) == 0: + pass + else: + self.join(root, parent, data_part) + if data_part != None: + depth += 1 + for child in self: + child.report(root, self, depth) + return root + def make_root(self): + return [] + def join(self, root, parent, data_part): + if data_part != None: + root.append(data_part) + def data_part(self, depth, indent=True): + if self.data == None: + return None + if hasattr(self, "_cached_data_part"): + return self._cached_data_part + data_part = self.data_part_fn(self.data) + if indent == True: + data_part_lines = data_part.splitlines() + indent = " "*(depth) + line_sep = "\n"+indent + data_part = indent+line_sep.join(data_part_lines) + self._cached_data_part = data_part + return data_part -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: +class Diff (object): + """ + Difference tree generator for BugDirs. + >>> import copy + >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) + >>> bd.user_id = "John Doe " + >>> bd_new = copy.deepcopy(bd) + >>> bd_new.target = "1.0" + >>> a = bd_new.bug_from_uuid("a") + >>> rep = a.comment_root.new_reply("I'm closing this bug") + >>> rep.uuid = "acom" + >>> rep.date = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> a.status = "closed" + >>> b = bd_new.bug_from_uuid("b") + >>> bd_new.remove_bug(b) + >>> c = bd_new.new_bug("c", "Bug C") + >>> d = Diff(bd, bd_new) + >>> r = d.report_tree() + >>> print "\\n".join(r.paths()) + bugdir + bugdir/settings + bugdir/bugs + bugdir/bugs/new + bugdir/bugs/new/c + bugdir/bugs/rem + bugdir/bugs/rem/b + bugdir/bugs/mod + bugdir/bugs/mod/a + bugdir/bugs/mod/a/settings + bugdir/bugs/mod/a/comments + bugdir/bugs/mod/a/comments/new + bugdir/bugs/mod/a/comments/new/acom + bugdir/bugs/mod/a/comments/rem + bugdir/bugs/mod/a/comments/mod + >>> print r.report_string() + Changed bug directory settings: + target: None -> 1.0 + New bugs: + c:om: Bug C + Removed bugs: + b:cm: Bug B + Modified bugs: + a:cm: Bug A + Changed bug settings: + status: open -> closed + New comments: + from John Doe on Thu, 01 Jan 1970 00:00:00 +0000 + I'm closing this bug... + >>> bd.cleanup() + """ + def __init__(self, old_bugdir, new_bugdir): + self.old_bugdir = old_bugdir + self.new_bugdir = new_bugdir + + # data assembly methods + + def _changed_bugs(self): + """ + Search for differences in all bugs between .old_bugdir and + .new_bugdir. Returns + (added_bugs, modified_bugs, removed_bugs) + where added_bugs and removed_bugs are lists of added and + removed bugs respectively. modified_bugs is a list of + (old_bug,new_bug) pairs. + """ + if hasattr(self, "__changed_bugs"): + return self.__changed_bugs + added = [] + removed = [] + modified = [] + for uuid in self.new_bugdir.list_uuids(): + new_bug = self.new_bugdir.bug_from_uuid(uuid) + try: + old_bug = self.old_bugdir.bug_from_uuid(uuid) + except KeyError: + added.append(new_bug) + else: + if old_bug.sync_with_disk == True: + old_bug.load_comments() + if new_bug.sync_with_disk == True: + new_bug.load_comments() + if old_bug != new_bug: + modified.append((old_bug, new_bug)) + for uuid in self.old_bugdir.list_uuids(): + if not self.new_bugdir.has_bug(uuid): + old_bug = self.old_bugdir.bug_from_uuid(uuid) + removed.append(old_bug) + added.sort() + removed.sort() + modified.sort(self._bug_modified_cmp) + self.__changed_bugs = (added, modified, removed) + return self.__changed_bugs + def _bug_modified_cmp(self, left, right): + return cmp(left[1], right[1]) + def _changed_comments(self, old, new): + """ + Search for differences in all loaded comments between the bugs + old and new. Returns + (added_comments, modified_comments, removed_comments) + analogous to ._changed_bugs. + """ + if hasattr(self, "__changed_comments"): + if new.uuid in self.__changed_comments: + return self.__changed_comments[new.uuid] + else: + self.__changed_comments = {} + added = [] + removed = [] + modified = [] + old.comment_root.sort(key=lambda comm : comm.time) + new.comment_root.sort(key=lambda comm : comm.time) + old_comment_ids = [c.uuid for c in old.comments()] + new_comment_ids = [c.uuid for c in new.comments()] + for uuid in new_comment_ids: + new_comment = new.comment_from_uuid(uuid) + try: + old_comment = old.comment_from_uuid(uuid) + except KeyError: + added.append(new_comment) + else: + if old_comment != new_comment: + modified.append((old_comment, new_comment)) + for uuid in old_comment_ids: + if uuid not in new_comment_ids: + new_comment = new.comment_from_uuid(uuid) + removed.append(new_comment) + self.__changed_comments[new.uuid] = (added, modified, removed) + return self.__changed_comments[new.uuid] + def _attribute_changes(self, old, new, attributes): + """ + Take two objects old and new, and compare the value of *.attr + for attr in the list attribute names. Returns a list of + (attr_name, old_value, new_value) + tuples. + """ + change_list = [] + for attr in attributes: + old_value = getattr(old, attr) + new_value = getattr(new, attr) + if old_value != new_value: + change_list.append((attr, old_value, new_value)) + if len(change_list) >= 0: + return change_list return None + def _settings_properties_attribute_changes(self, old, new, + hidden_properties=[]): + properties = sorted(new.settings_properties) + for p in hidden_properties: + properties.remove(p) + attributes = [settings_object.setting_name_to_attr_name(None, p) + for p in properties] + return self._attribute_changes(old, new, attributes) + def _bugdir_attribute_changes(self): + return self._settings_properties_attribute_changes( \ + self.old_bugdir, self.new_bugdir, + ["vcs_name"]) # tweaked by bugdir.duplicate_bugdir + def _bug_attribute_changes(self, old, new): + return self._settings_properties_attribute_changes(old, new) + def _comment_attribute_changes(self, old, new): + return self._settings_properties_attribute_changes(old, new) -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] + # report generation methods - 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) + def report_tree(self, diff_tree=DiffTree): + """ + Pretty bare to make it easy to adjust to specific cases. You + can pass in a DiffTree subclass via diff_tree to override the + default report assembly process. + """ + if hasattr(self, "__report_tree"): + return self.__report_tree + bugdir_settings = sorted(self.new_bugdir.settings_properties) + bugdir_settings.remove("vcs_name") # tweaked by bugdir.duplicate_bugdir + root = diff_tree("bugdir") + bugdir_attribute_changes = self._bugdir_attribute_changes() + if len(bugdir_attribute_changes) > 0: + bugdir = diff_tree("settings", bugdir_attribute_changes, + self.bugdir_attribute_change_string) + root.append(bugdir) + bug_root = diff_tree("bugs") + root.append(bug_root) + add,mod,rem = self._changed_bugs() + bnew = diff_tree("new", "New bugs:", requires_children=True) + bug_root.append(bnew) + for bug in add: + b = diff_tree(bug.uuid, bug, self.bug_add_string) + bnew.append(b) + brem = diff_tree("rem", "Removed bugs:", requires_children=True) + bug_root.append(brem) + for bug in rem: + b = diff_tree(bug.uuid, bug, self.bug_rem_string) + brem.append(b) + bmod = diff_tree("mod", "Modified bugs:", requires_children=True) + bug_root.append(bmod) + for old,new in mod: + b = diff_tree(new.uuid, (old,new), self.bug_mod_string) + bmod.append(b) + bug_attribute_changes = self._bug_attribute_changes(old, new) + if len(bug_attribute_changes) > 0: + bset = diff_tree("settings", bug_attribute_changes, + self.bug_attribute_change_string) + b.append(bset) + if old.summary != new.summary: + data = (old.summary, new.summary) + bsum = diff_tree("summary", data, self.bug_summary_change_string) + b.append(bsum) + cr = diff_tree("comments") + b.append(cr) + a,m,d = self._changed_comments(old, new) + cnew = diff_tree("new", "New comments:", requires_children=True) + for comment in a: + c = diff_tree(comment.uuid, comment, self.comment_add_string) + cnew.append(c) + crem = diff_tree("rem", "Removed comments:",requires_children=True) + for comment in d: + c = diff_tree(comment.uuid, comment, self.comment_rem_string) + crem.append(c) + cmod = diff_tree("mod","Modified comments:",requires_children=True) + for o,n in m: + c = diff_tree(n.uuid, (o,n), self.comment_mod_string) + cmod.append(c) + comm_attribute_changes = self._comment_attribute_changes(o, n) + if len(comm_attribute_changes) > 0: + cset = diff_tree("settings", comm_attribute_changes, + self.comment_attribute_change_string) + if o.body != n.body: + data = (o.body, n.body) + cbody = diff_tree("cbody", data, + self.comment_body_change_string) + c.append(cbody) + cr.extend([cnew, crem, cmod]) + self.__report_tree = root + return self.__report_tree - if len(change_strings) == 0: - return None - return "%s\n %s" % (new.string(shortlist=True), - " \n".join(change_strings)) + # change data -> string methods. + # Feel free to play with these in subclasses. + def attribute_change_string(self, attribute_changes, indent=0): + indent_string = " "*indent + change_strings = [u"%s: %s -> %s" % f for f in attribute_changes] + for i,change_string in enumerate(change_strings): + change_strings[i] = indent_string+change_string + return u"\n".join(change_strings) + def bugdir_attribute_change_string(self, attribute_changes): + return "Changed bug directory settings:\n%s" % \ + self.attribute_change_string(attribute_changes, indent=1) + def bug_attribute_change_string(self, attribute_changes): + return "Changed bug settings:\n%s" % \ + self.attribute_change_string(attribute_changes, indent=1) + def comment_attribute_change_string(self, attribute_changes): + return "Changed comment settings:\n%s" % \ + self.attribute_change_string(attribute_changes, indent=1) + def bug_add_string(self, bug): + return bug.string(shortlist=True) + def bug_rem_string(self, bug): + return bug.string(shortlist=True) + def bug_mod_string(self, bugs): + old_bug,new_bug = bugs + return new_bug.string(shortlist=True) + def bug_summary_change_string(self, summaries): + old_summary,new_summary = summaries + return "summary changed:\n %s\n %s" % (old_summary, new_summary) + def _comment_summary_string(self, comment): + return "from %s on %s" % (comment.author, time_to_str(comment.time)) + def comment_add_string(self, comment): + summary = self._comment_summary_string(comment) + first_line = comment.body.splitlines()[0] + return "%s\n %s..." % (summary, first_line) + def comment_rem_string(self, comment): + summary = self._comment_summary_string(comment) + first_line = comment.body.splitlines()[0] + return "%s\n %s..." % (summary, first_line) + def comment_mod_string(self, comments): + old_comment,new_comment = comments + return self._comment_summary_string(new_comment) + def comment_body_change_string(self, bodies): + old_body,new_body = bodies + return difflib.unified_diff(old_body, new_body) -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 93144b8..ec41006 100644 --- a/libbe/editor.py +++ b/libbe/editor.py @@ -15,6 +15,11 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Define editor_string(), a function that invokes an editor to accept +user-produced text as a string. +""" + import codecs import locale import os @@ -22,6 +27,7 @@ import sys import tempfile import doctest + default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() comment_marker = u"== Anything below this line will be ignored\n" @@ -62,7 +68,8 @@ def editor_string(comment=None, encoding=None): fhandle, fname = tempfile.mkstemp() try: if comment is not None: - os.write(fhandle, '\n'+comment_string(comment)) + cstring = u'\n'+comment_string(comment) + os.write(fhandle, cstring.encode(encoding)) os.close(fhandle) oldmtime = os.path.getmtime(fname) os.system("%s %s" % (editor, fname)) diff --git a/libbe/encoding.py b/libbe/encoding.py index d603602..fd513b5 100644 --- a/libbe/encoding.py +++ b/libbe/encoding.py @@ -14,16 +14,26 @@ # 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. + +""" +Support input/output/filesystem encodings (e.g. UTF-8). +""" + import codecs import locale import sys import doctest + +ENCODING = None # override get_encoding() output by setting this + def get_encoding(): """ Guess a useful input/output/filesystem encoding... Maybe we need seperate encodings for input/output and filesystem? Hmm... """ + if ENCODING != None: + return ENCODING 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 diff --git a/libbe/git.py b/libbe/git.py index 2f9ffa9..3abe3b8 100644 --- a/libbe/git.py +++ b/libbe/git.py @@ -16,30 +16,34 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Git backend. +""" + import os import re import sys import unittest import doctest -import rcs -from rcs import RCS +import vcs + def new(): return Git() -class Git(RCS): +class Git(vcs.VCS): name="git" client="git" versioned=True - def _rcs_help(self): + def _vcs_help(self): status,output,error = self._u_invoke_client("--help") return output - def _rcs_detect(self, path): + def _vcs_detect(self, path): if self._u_search_parent_directories(path, ".git") != None : return True return False - def _rcs_root(self, path): + def _vcs_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. @@ -50,13 +54,21 @@ class Git(RCS): gitdir = os.path.join(path, output.rstrip('\n')) dirname = os.path.abspath(os.path.dirname(gitdir)) return dirname - def _rcs_init(self, path): + def _vcs_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') + def _vcs_get_user_id(self): + status,output,error = \ + self._u_invoke_client("config", "user.name", expect=(0,1)) + if status == 0: + name = output.rstrip('\n') + else: + name = "" + status,output,error = \ + self._u_invoke_client("config", "user.email", expect=(0,1)) + if status == 0: + email = output.rstrip('\n') + else: + email = "" if name != "" or email != "": # got something! # guess missing info, if necessary if name == "": @@ -65,35 +77,35 @@ class Git(RCS): 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): + def _vcs_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): + def _vcs_add(self, path): if os.path.isdir(path): return self._u_invoke_client("add", path) - def _rcs_remove(self, path): + def _vcs_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): + def _vcs_update(self, path): + self._vcs_add(path) + def _vcs_get_file_contents(self, path, revision=None, binary=False): if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + return vcs.VCS._vcs_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): + def _vcs_duplicate_repo(self, directory, revision=None): if revision==None: - RCS._rcs_duplicate_repo(self, directory, revision) + vcs.VCS._vcs_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): + def _vcs_commit(self, commitfile, allow_empty=False): args = ['commit', '--all', '--file', commitfile] if allow_empty == True: args.append("--allow-empty") @@ -104,17 +116,33 @@ class Git(RCS): strings = ["nothing to commit", "nothing added to commit"] if self._u_any_in_string(strings, output) == True: - raise rcs.EmptyCommit() + raise vcs.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 + full_revision = self._vcs_revision_id(-1) + assert full_revision.startswith(revision), \ + "Mismatched revisions:\n%s\n%s" % (revision, full_revision) + return full_revision + def _vcs_revision_id(self, index): + args = ["rev-list", "--first-parent", "--reverse", "HEAD"] + kwargs = {"expect":(0,128)} + status,output,error = self._u_invoke_client(*args, **kwargs) + if status == 128: + if error.startswith("fatal: ambiguous argument 'HEAD': unknown "): + return None + raise vcs.CommandError(args, status, stdout="", stderr=error) + commits = output.splitlines() + try: + return commits[index] + except IndexError: + return None -rcs.make_rcs_testcase_subclasses(Git, sys.modules[__name__]) +vcs.make_vcs_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 a20eeb5..f8f8121 100644 --- a/libbe/hg.py +++ b/libbe/hg.py @@ -16,81 +16,88 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Mercurial (hg) backend. +""" + import os import re import sys import unittest import doctest -import rcs -from rcs import RCS +import vcs + def new(): return Hg() -class Hg(RCS): +class Hg(vcs.VCS): name="hg" client="hg" versioned=True - def _rcs_help(self): + def _vcs_help(self): status,output,error = self._u_invoke_client("--help") return output - def _rcs_detect(self, path): + def _vcs_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): + def _vcs_root(self, path): status,output,error = self._u_invoke_client("root", directory=path) return output.rstrip('\n') - def _rcs_init(self, path): + def _vcs_init(self, path): self._u_invoke_client("init", directory=path) - def _rcs_get_user_id(self): + def _vcs_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): + def _vcs_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): + raise vcs.SettingIDnotSupported + def _vcs_add(self, path): self._u_invoke_client("add", path) - def _rcs_remove(self, path): + def _vcs_remove(self, path): self._u_invoke_client("rm", "--force", path) - def _rcs_update(self, path): + def _vcs_update(self, path): pass - def _rcs_get_file_contents(self, path, revision=None, binary=False): + def _vcs_get_file_contents(self, path, revision=None, binary=False): if revision == None: - return RCS._rcs_get_file_contents(self, path, revision, binary=binary) + return vcs.VCS._vcs_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): + def _vcs_duplicate_repo(self, directory, revision=None): if revision == None: - return RCS._rcs_duplicate_repo(self, directory, revision) + return vcs.VCS._vcs_duplicate_repo(self, directory, revision) else: self._u_invoke_client("archive", "--rev", revision, directory) - def _rcs_commit(self, commitfile, allow_empty=False): + def _vcs_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 + raise vcs.EmptyCommit() + return self._vcs_revision_id(-1) + def _vcs_revision_id(self, index, style="id"): + args = ["identify", "--rev", str(int(index)), "--%s" % style] + kwargs = {"expect": (0,255)} + status,output,error = self._u_invoke_client(*args, **kwargs) + if status == 0: + id = output.strip() + if id == '000000000000': + return None # before initial commit. + return id + return None -rcs.make_rcs_testcase_subclasses(Hg, sys.modules[__name__]) +vcs.make_vcs_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 b959d76..4d69601 100644 --- a/libbe/mapfile.py +++ b/libbe/mapfile.py @@ -14,12 +14,19 @@ # 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 + +""" +Provide a means of saving and loading dictionaries of parameters. The +saved "mapfiles" should be clear, flat-text files, and allow easy merging of +independent/conflicting changes. +""" + import errno -import utility +import os.path +import yaml import doctest + class IllegalKey(Exception): def __init__(self, key): Exception.__init__(self, 'Illegal key "%s"' % key) @@ -95,33 +102,15 @@ def parse(contents): >>> 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): +def map_save(vcs, path, map, allow_no_vcs=False): """Save the map as a mapfile to the specified path""" contents = generate(map) - rcs.set_file_contents(path, contents, allow_no_rcs) + vcs.set_file_contents(path, contents, allow_no_vcs) -def map_load(rcs, path, allow_no_rcs=False): - contents = rcs.get_file_contents(path, allow_no_rcs=allow_no_rcs) +def map_load(vcs, path, allow_no_vcs=False): + contents = vcs.get_file_contents(path, allow_no_vcs=allow_no_vcs) return parse(contents) suite = doctest.DocTestSuite() diff --git a/libbe/plugin.py b/libbe/plugin.py index 0545fd7..d593d69 100644 --- a/libbe/plugin.py +++ b/libbe/plugin.py @@ -15,6 +15,12 @@ # 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. + +""" +Allow simple listing and loading of the various becommands and libbe +submodules (i.e. "plugins"). +""" + import os import os.path import sys diff --git a/libbe/properties.py b/libbe/properties.py index 144220b..09dd20e 100644 --- a/libbe/properties.py +++ b/libbe/properties.py @@ -160,10 +160,10 @@ def _get_cached_mutable_property(self, cacher_name, property_name, default=None) 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): +def _cmp_cached_mutable_property(self, cacher_name, property_name, value, default=None): _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 + _set_cached_mutable_property(self, cacher_name, property_name, default) old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)] return cmp(_hash_mutable_value(value), old_hash) @@ -327,7 +327,7 @@ def primed_property(primer, initVal=None): return funcs return decorator -def change_hook_property(hook, mutable=False): +def change_hook_property(hook, mutable=False, default=None): """ Call the function hook(instance, old_value, new_value) whenever a value different from the current value is set (instance is a a @@ -359,9 +359,9 @@ def change_hook_property(hook, mutable=False): 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: + if _cmp_cached_mutable_property(self, "change hook property", name, value, default) != 0: # there has been a change, cache new value - old_value = _get_cached_mutable_property(self, "change hook property", name) + old_value = _get_cached_mutable_property(self, "change hook property", name, default) _set_cached_mutable_property(self, "change hook property", name, value) if from_fset == True: # return previously cached value value = old_value diff --git a/libbe/rcs.py b/libbe/rcs.py index 294b8e0..e69de29 100644 --- a/libbe/rcs.py +++ b/libbe/rcs.py @@ -1,876 +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., -# 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/libbe/settings_object.py b/libbe/settings_object.py index dde247f..ceea9d5 100644 --- a/libbe/settings_object.py +++ b/libbe/settings_object.py @@ -148,7 +148,8 @@ def versioned_property(name, doc, 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) + hooked = change_hook_property(hook=change_hook, mutable=mutable, + default=EMPTY) primed = primed_property(primer=primer, initVal=UNPRIMED) settings = settings_property(name=name, null=UNPRIMED) docp = doc_property(doc=fulldoc) @@ -385,30 +386,24 @@ class SavedSettingsObjectTests(unittest.TestCase): 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(SAVES == [], 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) diff --git a/libbe/tree.py b/libbe/tree.py index 45ae085..06d09e5 100644 --- a/libbe/tree.py +++ b/libbe/tree.py @@ -15,6 +15,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Define a traversable tree structure. +""" + import doctest class Tree(list): diff --git a/libbe/upgrade.py b/libbe/upgrade.py new file mode 100644 index 0000000..4123c72 --- /dev/null +++ b/libbe/upgrade.py @@ -0,0 +1,187 @@ +# 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. + +""" +Handle conversion between the various on-disk images. +""" + +import os, os.path +import sys +import doctest + +import encoding +import mapfile +import vcs + +# a list of all past versions +BUGDIR_DISK_VERSIONS = ["Bugs Everywhere Tree 1 0", + "Bugs Everywhere Directory v1.1", + "Bugs Everywhere Directory v1.2"] + +# the current version +BUGDIR_DISK_VERSION = BUGDIR_DISK_VERSIONS[-1] + +class Upgrader (object): + "Class for converting " + initial_version = None + final_version = None + def __init__(self, root): + self.root = root + # use the "None" VCS to ensure proper encoding/decoding and + # simplify path construction. + self.vcs = vcs.vcs_by_name("None") + self.vcs.root(self.root) + self.vcs.encoding = encoding.get_encoding() + + def get_path(self, *args): + """ + Return a path relative to .root. + """ + dir = os.path.join(self.root, ".be") + if len(args) == 0: + return dir + assert args[0] in ["version", "settings", "bugs"], str(args) + return os.path.join(dir, *args) + + def check_initial_version(self): + path = self.get_path("version") + version = self.vcs.get_file_contents(path).rstrip("\n") + assert version == self.initial_version, version + + def set_version(self): + path = self.get_path("version") + self.vcs.set_file_contents(path, self.final_version+"\n") + + def upgrade(self): + print >> sys.stderr, "upgrading bugdir from '%s' to '%s'" \ + % (self.initial_version, self.final_version) + self.check_initial_version() + self.set_version() + self._upgrade() + + def _upgrade(self): + raise NotImplementedError + + +class Upgrade_1_0_to_1_1 (Upgrader): + initial_version = "Bugs Everywhere Tree 1 0" + final_version = "Bugs Everywhere Directory v1.1" + def _upgrade_mapfile(self, path): + contents = self.vcs.get_file_contents(path) + old_format = False + for line in contents.splitlines(): + if len(line.split("=")) == 2: + old_format = True + break + if old_format == True: + # translate to YAML. + 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) + # load the YAML and save + map = mapfile.parse(contents) + mapfile.map_save(self.vcs, path, map) + + def _upgrade(self): + """ + Comment value field "From" -> "Author". + Homegrown mapfile -> YAML. + """ + path = self.get_path("settings") + self._upgrade_mapfile(path) + for bug_uuid in os.listdir(self.get_path("bugs")): + path = self.get_path("bugs", bug_uuid, "values") + self._upgrade_mapfile(path) + c_path = ["bugs", bug_uuid, "comments"] + if not os.path.exists(self.get_path(*c_path)): + continue # no comments for this bug + for comment_uuid in os.listdir(self.get_path(*c_path)): + path_list = c_path + [comment_uuid, "values"] + path = self.get_path(*path_list) + self._upgrade_mapfile(path) + settings = mapfile.map_load(self.vcs, path) + if "From" in settings: + settings["Author"] = settings.pop("From") + mapfile.map_save(self.vcs, path, settings) + + +class Upgrade_1_1_to_1_2 (Upgrader): + initial_version = "Bugs Everywhere Directory v1.1" + final_version = "Bugs Everywhere Directory v1.2" + def _upgrade(self): + """ + BugDir settings field "rcs_name" -> "vcs_name". + """ + path = self.get_path("settings") + settings = mapfile.map_load(self.vcs, path) + if "rcs_name" in settings: + settings["vcs_name"] = settings.pop("rcs_name") + mapfile.map_save(self.vcs, path, settings) + + +upgraders = [Upgrade_1_0_to_1_1, + Upgrade_1_1_to_1_2] +upgrade_classes = {} +for upgrader in upgraders: + upgrade_classes[(upgrader.initial_version,upgrader.final_version)]=upgrader + +def upgrade(path, current_version, + target_version=BUGDIR_DISK_VERSION): + """ + Call the appropriate upgrade function to convert current_version + to target_version. If a direct conversion function does not exist, + use consecutive conversion functions. + """ + if current_version not in BUGDIR_DISK_VERSIONS: + raise NotImplementedError, \ + "Cannot handle version '%s' yet." % version + if target_version not in BUGDIR_DISK_VERSIONS: + raise NotImplementedError, \ + "Cannot handle version '%s' yet." % version + + if (current_version, target_version) in upgrade_classes: + # direct conversion + upgrade_class = upgrade_classes[(current_version, target_version)] + u = upgrade_class(path) + u.upgrade() + else: + # consecutive single-step conversion + i = BUGDIR_DISK_VERSIONS.index(current_version) + while True: + version_a = BUGDIR_DISK_VERSIONS[i] + version_b = BUGDIR_DISK_VERSIONS[i+1] + try: + upgrade_class = upgrade_classes[(version_a, version_b)] + except KeyError: + raise NotImplementedError, \ + "Cannot convert version '%s' to '%s' yet." \ + % (version_a, version_b) + u = upgrade_class(path) + u.upgrade() + if version_b == target_version: + break + i += 1 + +suite = doctest.DocTestSuite() diff --git a/libbe/utility.py b/libbe/utility.py index 3df06b4..aafbf8d 100644 --- a/libbe/utility.py +++ b/libbe/utility.py @@ -14,6 +14,11 @@ # 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. + +""" +Assorted utility functions that don't fit in anywhere else. +""" + import calendar import codecs import os diff --git a/libbe/vcs.py b/libbe/vcs.py new file mode 100644 index 0000000..a1d3022 --- /dev/null +++ b/libbe/vcs.py @@ -0,0 +1,938 @@ +# 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. + +""" +Define the base VCS (Version Control System) class, which should be +subclassed by other Version Control System backends. The base class +implements a "do not version" VCS. +""" + +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_vcs(matchfn): + """Return the first module for which matchfn(VCS_instance) is true""" + import arch + import bzr + import darcs + import git + import hg + for module in [arch, bzr, darcs, git, hg]: + vcs = module.new() + if matchfn(vcs) == True: + return vcs + del(vcs) + return VCS() + +def vcs_by_name(vcs_name): + """Return the module for the VCS with the given name""" + return _get_matching_vcs(lambda vcs: vcs.name == vcs_name) + +def detect_vcs(dir): + """Return an VCS instance for the vcs being used in this directory""" + return _get_matching_vcs(lambda vcs: vcs.detect(dir)) + +def installed_vcs(): + """Return an instance of an installed VCS""" + return _get_matching_vcs(lambda vcs: vcs.installed()) + + +class CommandError(Exception): + def __init__(self, command, status, stdout, stderr): + strerror = ["Command failed (%d):\n %s\n" % (status, stderr), + "while executing\n %s" % command] + Exception.__init__(self, "\n".join(strerror)) + self.command = command + self.status = status + self.stdout = stdout + self.stderr = stderr + +class SettingIDnotSupported(NotImplementedError): + pass + +class VCSnotRooted(Exception): + def __init__(self): + msg = "VCS 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 VCS() + +class VCS(object): + """ + This class implements a 'no-vcs' interface. + + Support for other VCSs can be added by subclassing this class, and + overriding methods _vcs_*() with code appropriate for your VCS. + + The methods _u_*() are utility methods available to the _vcs_*() + 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 _vcs_help(self): + """ + Return the command help string. + (Allows a simple test to see if the client is installed.) + """ + pass + def _vcs_detect(self, path=None): + """ + Detect whether a directory is revision controlled with this VCS. + """ + return True + def _vcs_root(self, path): + """ + Get the VCS root. This is the default working directory for + future invocations. You would normally set this to the root + directory for your VCS. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + if path == "": + path = os.path.abspath(".") + return path + def _vcs_init(self, path): + """ + Begin versioning the tree based at path. + """ + pass + def _vcs_cleanup(self): + """ + Remove any cruft that _vcs_init() created outside of the + versioned tree. + """ + pass + def _vcs_get_user_id(self): + """ + Get the VCS's suggested user id (e.g. "John Doe "). + If the VCS has not been configured with a username, return None. + """ + return None + def _vcs_set_user_id(self, value): + """ + Set the VCS's suggested user id (e.g "John Doe "). + This is run if the VCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + raise SettingIDnotSupported + def _vcs_add(self, path): + """ + Add the already created file at path to version control. + """ + pass + def _vcs_remove(self, path): + """ + Remove the file at path from version control. Optionally + remove the file from the filesystem as well. + """ + pass + def _vcs_update(self, path): + """ + Notify the versioning system of changes to the versioned file + at path. + """ + pass + def _vcs_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 VCS 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 _vcs_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 _vcs_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 _vcs_revision_id(self, index): + """ + Return the name of the th revision. Index will be an + integer (possibly <= 0). The choice of which branch to follow + when crossing branches/merges is not defined. + + Return None if revision IDs are not supported, or if the + specified revision does not exist. + """ + return None + def installed(self): + try: + self._vcs_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 VCS. + """ + return self._vcs_detect(path) + def root(self, path): + """ + Set the root directory to the path's VCS root. This is the + default working directory for future invocations. + """ + self.rootdir = self._vcs_root(path) + def init(self, path): + """ + Begin versioning the tree based at path. + Also roots the vcs at path. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + self._vcs_init(path) + self.root(path) + def cleanup(self): + self._vcs_cleanup() + def get_user_id(self): + """ + Get the VCS's suggested user id (e.g. "John Doe "). + If the VCS has not been configured with a username, return the user's + id. You can override the automatic lookup procedure by setting the + VCS.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._vcs_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 VCS's suggested user id (e.g "John Doe "). + This is run if the VCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + self._vcs_set_user_id(value) + def add(self, path): + """ + Add the already created file at path to version control. + """ + self._vcs_add(self._u_rel_path(path)) + def remove(self, path): + """ + Remove a file from both version control and the filesystem. + """ + self._vcs_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._vcs_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._vcs_update(self._u_rel_path(path)) + def get_file_contents(self, path, revision=None, allow_no_vcs=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_vcs(path, allow_no_vcs): + relpath = self._u_rel_path(path) + contents = self._vcs_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_vcs=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_vcs(path, allow_no_vcs): + if add: + self.add(path) + else: + self.update(path) + def mkdir(self, path, allow_no_vcs=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_vcs, check_parents) + if not os.path.exists(path): + os.mkdir(path) + if self._use_vcs(path, allow_no_vcs): + self.add(path) + else: + assert os.path.isdir(path) + if self._use_vcs(path, allow_no_vcs): + #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='BEvcs') + self._duplicateDirname = \ + os.path.join(self._duplicateBasedir, "duplicate") + self._vcs_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._vcs_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 revision_id(self, index=None): + """ + Return the name of the th revision. The choice of + which branch to follow when crossing branches/merges is not + defined. + + Return None if index==None, revision IDs are not supported, or + if the specified revision does not exist. + """ + if index == None: + return None + return self._vcs_revision_id(index) + 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, status=e.args[0], stdout="", stderr=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, output, 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_vcs(self, path, allow_no_vcs): + """ + Try and decide if _vcs_add/update/mkdir/etc calls will + succeed. Returns True is we think the vcs_call would + succeeed, and False otherwise. + """ + use_vcs = True + exception = None + if self.rootdir != None: + if self.path_in_root(path) == False: + use_vcs = False + exception = PathNotInRoot(path, self.rootdir) + else: + use_vcs = False + exception = VCSnotRooted + if use_vcs == False and allow_no_vcs==False: + raise exception + return use_vcs + def path_in_root(self, path, root=None): + """ + Return the relative path to path from root. + >>> vcs = new() + >>> vcs.path_in_root("/a.b/c/.be", "/a.b/c") + True + >>> vcs.path_in_root("/a.b/.be", "/a.b/c") + False + """ + if root == None: + if self.rootdir == None: + raise VCSnotRooted + 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. + >>> vcs = new() + >>> vcs._u_rel_path("/a.b/c/.be", "/a.b/c") + '.be' + """ + if root == None: + if self.rootdir == None: + raise VCSnotRooted + 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. + >>> vcs = new() + >>> vcs._u_abspath(".be", "/a.b/c") + '/a.b/c/.be' + """ + if root == None: + assert self.rootdir != None, "VCS not rooted" + root = self.rootdir + return os.path.abspath(os.path.join(root, path)) + def _u_create_id(self, name, email=None): + """ + >>> vcs = new() + >>> vcs._u_create_id("John Doe", "jdoe@example.com") + 'John Doe ' + >>> vcs._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): + """ + >>> vcs = new() + >>> vcs._u_parse_id("John Doe ") + ('John Doe', 'jdoe@example.com') + >>> vcs._u_parse_id("John Doe") + ('John Doe', None) + >>> try: + ... vcs._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_vcs_test_fixtures(testcase): + """Set up test fixtures for VCS test case.""" + testcase.vcs = testcase.Class() + testcase.dir = Dir() + testcase.dirname = testcase.dir.path + + vcs_not_supporting_uninitialized_user_id = [] + vcs_not_supporting_set_user_id = ["None", "hg"] + testcase.vcs_supports_uninitialized_user_id = ( + testcase.vcs.name not in vcs_not_supporting_uninitialized_user_id) + testcase.vcs_supports_set_user_id = ( + testcase.vcs.name not in vcs_not_supporting_set_user_id) + + if not testcase.vcs.installed(): + testcase.fail( + "%(name)s VCS not found" % vars(testcase.Class)) + + if testcase.Class.name != "None": + testcase.failIf( + testcase.vcs.detect(testcase.dirname), + "Detected %(name)s VCS before initialising" + % vars(testcase.Class)) + + testcase.vcs.init(testcase.dirname) + + +class VCSTestCase(unittest.TestCase): + """Test cases for base VCS class.""" + + Class = VCS + + def __init__(self, *args, **kwargs): + super(VCSTestCase, self).__init__(*args, **kwargs) + self.dirname = None + + def setUp(self): + super(VCSTestCase, self).setUp() + setup_vcs_test_fixtures(self) + + def tearDown(self): + del(self.vcs) + super(VCSTestCase, self).tearDown() + + def full_path(self, rel_path): + return os.path.join(self.dirname, rel_path) + + +class VCS_init_TestCase(VCSTestCase): + """Test cases for VCS.init method.""" + + def test_detect_should_succeed_after_init(self): + """Should detect VCS in directory after initialization.""" + self.failUnless( + self.vcs.detect(self.dirname), + "Did not detect %(name)s VCS after initialising" + % vars(self.Class)) + + def test_vcs_rootdir_in_specified_root_path(self): + """VCS root directory should be in specified root path.""" + rp = os.path.realpath(self.vcs.rootdir) + dp = os.path.realpath(self.dirname) + vcs_name = self.Class.name + self.failUnless( + dp == rp or rp == None, + "%(vcs_name)s VCS root in wrong dir (%(dp)s %(rp)s)" % vars()) + + +class VCS_get_user_id_TestCase(VCSTestCase): + """Test cases for VCS.get_user_id method.""" + + def test_gets_existing_user_id(self): + """Should get the existing user ID.""" + if not self.vcs_supports_uninitialized_user_id: + return + + user_id = self.vcs.get_user_id() + self.failUnless( + user_id is not None, + "unable to get a user id") + + +class VCS_set_user_id_TestCase(VCSTestCase): + """Test cases for VCS.set_user_id method.""" + + def setUp(self): + super(VCS_set_user_id_TestCase, self).setUp() + + if self.vcs_supports_uninitialized_user_id: + self.prev_user_id = self.vcs.get_user_id() + else: + self.prev_user_id = "Uninitialized identity " + + if self.vcs_supports_set_user_id: + self.test_new_user_id = "John Doe " + self.vcs.set_user_id(self.test_new_user_id) + + def tearDown(self): + if self.vcs_supports_set_user_id: + self.vcs.set_user_id(self.prev_user_id) + super(VCS_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.vcs_supports_set_user_id: + return + self.assertRaises( + SettingIDnotSupported, + self.vcs.set_user_id, "foo") + + def test_updates_user_id_in_supporting_vcs(self): + """Should update the user ID in an VCS that supports it.""" + if not self.vcs_supports_set_user_id: + return + user_id = self.vcs.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_vcs_revision_test_fixtures(testcase): + """Set up revision test fixtures for VCS test case.""" + testcase.test_dirs = ['a', 'a/b', 'c'] + for path in testcase.test_dirs: + testcase.vcs.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 VCS_mkdir_TestCase(VCSTestCase): + """Test cases for VCS.mkdir method.""" + + def setUp(self): + super(VCS_mkdir_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_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 VCS_commit_TestCase(VCSTestCase): + """Test cases for VCS.commit method.""" + + def setUp(self): + super(VCS_commit_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_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.vcs.set_file_contents(full_path, test_contents) + current_contents = self.vcs.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.vcs.set_file_contents(full_path, test_contents) + revision = self.vcs.commit("Initial file contents.") + current_contents = self.vcs.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.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial file contents.") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + current_contents = self.vcs.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.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial file contents.") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + committed_contents = self.vcs.get_file_contents( + full_path, revision) + self.failUnlessEqual( + self.test_contents['rev_1'], committed_contents) + + def test_revision_id_as_committed(self): + """Check for compatibility between .commit() and .revision_id()""" + if not self.vcs.versioned: + self.failUnlessEqual(self.vcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial %s contents." % path) + committed_revisions.append(revision) + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + revision = self.vcs.commit("Altered %s contents." % path) + committed_revisions.append(revision) + for i,revision in enumerate(committed_revisions): + self.failUnlessEqual(self.vcs.revision_id(i), revision) + i += -len(committed_revisions) # check negative indices + self.failUnlessEqual(self.vcs.revision_id(i), revision) + i = len(committed_revisions) + self.failUnlessEqual(self.vcs.revision_id(i), None) + self.failUnlessEqual(self.vcs.revision_id(-i-1), None) + + def test_revision_id_as_committed(self): + """Check revision id before first commit""" + if not self.vcs.versioned: + self.failUnlessEqual(self.vcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + self.failUnlessEqual(self.vcs.revision_id(0), None) + + +class VCS_duplicate_repo_TestCase(VCSTestCase): + """Test cases for VCS.duplicate_repo method.""" + + def setUp(self): + super(VCS_duplicate_repo_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + self.vcs.remove_duplicate_repo() + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_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.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Commit current status") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + dup_repo_path = self.vcs.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.vcs.remove_duplicate_repo() + + +def make_vcs_testcase_subclasses(vcs_class, namespace): + """Make VCSTestCase subclasses for vcs_class in the namespace.""" + vcs_testcase_classes = [ + c for c in ( + ob for ob in globals().values() if isinstance(ob, type)) + if issubclass(c, VCSTestCase)] + + for base_class in vcs_testcase_classes: + testcase_class_name = vcs_class.__name__ + base_class.__name__ + testcase_class_bases = (base_class,) + testcase_class_dict = dict(base_class.__dict__) + testcase_class_dict['Class'] = vcs_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/version.py b/libbe/version.py new file mode 100644 index 0000000..f8eebbd --- /dev/null +++ b/libbe/version.py @@ -0,0 +1,50 @@ +#!/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. + +""" +Store version info for this BE installation. By default, use the +bzr-generated information in _version.py, but allow manual overriding +by setting _VERSION. This allows support of both the "I don't want to +be bothered setting version strings" and the "I want complete control +over the version strings" workflows. +""" + +import libbe._version as _version + +# Manually set a version string (optional, defaults to bzr revision id) +#_VERSION = "1.2.3" + +def version(verbose=False): + """ + Returns the version string for this BE installation. If + verbose==True, the string will include extra lines with more + detail (e.g. bzr branch nickname, etc.). + """ + if "_VERSION" in globals(): + string = _VERSION + else: + string = _version.version_info["revision_id"] + if verbose == True: + string += ("\n" + "revision: %(revno)d\n" + "nick: %(branch_nick)s\n" + "revision id: %(revision_id)s" + % _version.version_info) + return string + +if __name__ == "__main__": + print version(verbose=True) diff --git a/update_copyright.sh b/update_copyright.sh index 28eb0e0..84a5913 100755 --- a/update_copyright.sh +++ b/update_copyright.sh @@ -59,7 +59,7 @@ ESCAPED_TEXT=`echo "$COPYRIGHT_TEXT" | awk '{printf("%s\\\\n", $0)}' | sed "$SED # 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 +AUTHORS=`echo "$AUTHORS" | grep -v 'j\^\|wking\|John Doe\|gianluca'` # remove non-names echo "Bugs Everywhere was written by:" > AUTHORS echo "$AUTHORS" >> AUTHORS @@ -99,6 +99,12 @@ do # Tweak the author list to make up for problems in the bzr # history, change of email address, etc. + # Consolidate Chris Ball + GREP_OUT=`echo "$AUTHORS" | grep 'Chris Ball '` + if [ -n "$GREP_OUT" ]; then + AUTHORS=`echo "$AUTHORS" | grep -v '^Chris Ball $'` + fi + # Consolidate Aaron Bentley AUTHORS=`echo "$AUTHORS" | sed 's//and Panometrics, Inc./'` GREP_OUT=`echo "$AUTHORS" | grep 'Aaron Bentley and Panometrics, Inc.'` @@ -113,15 +119,12 @@ do AUTHORS=`echo "$AUTHORS" | grep -v '^Ben Finney $'` fi - # Consolidate Chris Ball - GREP_OUT=`echo "$AUTHORS" | grep 'Chris Ball '` - if [ -n "$GREP_OUT" ]; then - AUTHORS=`echo "$AUTHORS" | grep -v '^Chris Ball $'` - fi - # Consolidate Trevor King AUTHORS=`echo "$AUTHORS" | grep -v "wking "` + # Consolidate Gianluca Montecchi + AUTHORS=`echo "$AUTHORS" | grep -v "gianluca"` + # Sort again... AUTHORS=`echo "$AUTHORS" | sort | uniq` -- 2.26.2