Merged Anton Batenev's report of Nicolas Alvarez' unicode-in-be-new bug
authorW. Trevor King <wking@drexel.edu>
Fri, 19 Mar 2010 11:18:13 +0000 (07:18 -0400)
committerW. Trevor King <wking@drexel.edu>
Fri, 19 Mar 2010 11:18:13 +0000 (07:18 -0400)
770 files changed:
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/body [moved from .be/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/values [moved from .be/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/body [moved from .be/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/values [moved from .be/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/values [moved from .be/bugs/00f26f04-9202-4288-8744-b29abc2342d6/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/b8e5c376-32a4-42ea-b6b2-adbee069384a/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/b8e5c376-32a4-42ea-b6b2-adbee069384a/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/f5139012-e20b-4d24-90a5-10d969ddd364/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/f5139012-e20b-4d24-90a5-10d969ddd364/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/comments/2f9beed6-4008-442a-8d44-a45cb7ce0a36/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/comments/2f9beed6-4008-442a-8d44-a45cb7ce0a36/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/02223264-e28a-4720-9f20-1e7a27a7041d/values [moved from .be/bugs/02223264-e28a-4720-9f20-1e7a27a7041d/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body [moved from .be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values [moved from .be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/body [moved from .be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values [moved from .be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body [moved from .be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values [moved from .be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/values [moved from .be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0ca2d112-b5bb-4df1-8ac0-e46db6cdd442/values [moved from .be/bugs/0ca2d112-b5bb-4df1-8ac0-e46db6cdd442/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/body [moved from .be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values [moved from .be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/body [moved from .be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values [moved from .be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/body [moved from .be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/body [moved from .be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/body [moved from .be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/values [moved from .be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0e0c806c-5443-4839-aa60-9615c8c10853/values [moved from .be/bugs/0e0c806c-5443-4839-aa60-9615c8c10853/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/body [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/values [moved from .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/comments/85770405-0ead-4044-a3cf-082615ff1b6f/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/comments/85770405-0ead-4044-a3cf-082615ff1b6f/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body [moved from .be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values [moved from .be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values [moved from .be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/body [moved from .be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/values [moved from .be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body [moved from .be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values [moved from .be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/body [moved from .be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/values [moved from .be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/values [moved from .be/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/body [moved from .be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values [moved from .be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body [moved from .be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values [moved from .be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/body [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/values [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/values with 72% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/body [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/values [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/body [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/values [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/body [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/values [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/body [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/values [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/body [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/values [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/body [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/values [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values [moved from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values with 94% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/27bb8bc2-05c2-417a-9d09-928471380d7a/values [moved from .be/bugs/27bb8bc2-05c2-417a-9d09-928471380d7a/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/body [moved from .be/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values [moved from .be/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/values [moved from .be/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body [moved from .be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values [moved from .be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values [moved from .be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2b81b428-fc43-4970-9469-b442385b9c0d/values [moved from .be/bugs/2b81b428-fc43-4970-9469-b442385b9c0d/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/body [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/values [moved from .be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body [moved from .be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values [moved from .be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values [moved from .be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/body [moved from .be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values [moved from .be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/e7d8343a-bd85-4359-bcda-bf0dc1e8177a/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/e7d8343a-bd85-4359-bcda-bf0dc1e8177a/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3613e6e9-db9e-4775-8914-f31f0b4b81ac/values [moved from .be/bugs/3613e6e9-db9e-4775-8914-f31f0b4b81ac/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values [moved from .be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body [moved from .be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values [moved from .be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/body [moved from .be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values [moved from .be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body [moved from .be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values [moved from .be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values [moved from .be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/body [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/values [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/body [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/values [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/body [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/values [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/body [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/values [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/body [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/values [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/body [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/values [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/values [moved from .be/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/body [moved from .be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values [moved from .be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/body [moved from .be/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/values [moved from .be/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/body [moved from .be/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/values [moved from .be/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/values [moved from .be/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/body [moved from .be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values [moved from .be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/values [moved from .be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body [moved from .be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values [moved from .be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body [moved from .be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values [moved from .be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values [moved from .be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body [moved from .be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values [moved from .be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values [moved from .be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/body [moved from .be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/values [moved from .be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/body [moved from .be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/values [moved from .be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/values [moved from .be/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/body [moved from .be/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/values [moved from .be/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/body [moved from .be/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/values [moved from .be/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/values [moved from .be/bugs/51930348-9ccc-4165-af41-6c7450de050e/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/body [moved from .be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/values [moved from .be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/body [moved from .be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/values [moved from .be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/values [moved from .be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/values with 77% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/values with 72% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/body [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values [moved from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values with 93% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/body [moved from .be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/values [moved from .be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/body [moved from .be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/values [moved from .be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/body [moved from .be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/values [moved from .be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/values [moved from .be/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body [moved from .be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values [moved from .be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body [moved from .be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values [moved from .be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/values [moved from .be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/628a050a-f969-4290-8468-f5e991528f40/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/628a050a-f969-4290-8468-f5e991528f40/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/f3e90a7e-b8c4-4a7c-8609-6a783ae59762/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/f3e90a7e-b8c4-4a7c-8609-6a783ae59762/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/62a74b85-0d4b-49f5-8794-74bafd871cd4/values [moved from .be/bugs/62a74b85-0d4b-49f5-8794-74bafd871cd4/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/65776f00-34d8-4b58-874d-333196a5e245/values [moved from .be/bugs/65776f00-34d8-4b58-874d-333196a5e245/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6622c06a-ed84-4d45-8011-a082fca219b6/values [moved from .be/bugs/6622c06a-ed84-4d45-8011-a082fca219b6/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body [moved from .be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values [moved from .be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values [moved from .be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/body [moved from .be/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values [moved from .be/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/values [moved from .be/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values [moved from .be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values [moved from .be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/764b812f-a0bb-4f4d-8e2f-c255c9474a0e/values [moved from .be/bugs/764b812f-a0bb-4f4d-8e2f-c255c9474a0e/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/body [moved from .be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values [moved from .be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/body [moved from .be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values [moved from .be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/values [moved from .be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/body [moved from .be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values [moved from .be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/body [moved from .be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values [moved from .be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/values [moved from .be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/comments/a555d577-7f8c-49f2-96f6-263ce5fdff8e/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/comments/a555d577-7f8c-49f2-96f6-263ce5fdff8e/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7d182ab9-9c0c-4b4f-885e-c5762d7a2437/values [moved from .be/bugs/7d182ab9-9c0c-4b4f-885e-c5762d7a2437/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/body [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/values [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/body [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/values [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/body [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/values [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/body [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/values [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/body [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/values [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/body [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/values [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/values [moved from .be/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/17d045d1-3b21-4d3d-8f81-29a5bbc5e6c1/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/17d045d1-3b21-4d3d-8f81-29a5bbc5e6c1/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/d463e2d9-6dcc-41a4-a6b2-647fb3bddf88/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/d463e2d9-6dcc-41a4-a6b2-647fb3bddf88/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8385a1fb-63df-4ca6-81cd-28ede83bb0c2/values [moved from .be/bugs/8385a1fb-63df-4ca6-81cd-28ede83bb0c2/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body [moved from .be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values [moved from .be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body [moved from .be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values [moved from .be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values [moved from .be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/body [moved from .be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values [moved from .be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/body [moved from .be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values [moved from .be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/body [moved from .be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values [moved from .be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/values [moved from .be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/body [moved from .be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/values [moved from .be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/body [moved from .be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values [moved from .be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/values [moved from .be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/432e994f-3759-42bf-a80d-7cd626c7ce7c/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/432e994f-3759-42bf-a80d-7cd626c7ce7c/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/e3d802cf-1fff-4a48-a61c-a07578969333/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/e3d802cf-1fff-4a48-a61c-a07578969333/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body [moved from .be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values [moved from .be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/body [moved from .be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values [moved from .be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values [moved from .be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9b1a0e71-4f7d-40b1-ab32-18496bf19a3f/values [moved from .be/bugs/9b1a0e71-4f7d-40b1-ab32-18496bf19a3f/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/comments/7cd2d475-676f-4d60-b431-c7635468e9bd/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/comments/7cd2d475-676f-4d60-b431-c7635468e9bd/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/body [moved from .be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/values [moved from .be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/body [moved from .be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/values [moved from .be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/body [moved from .be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/values [moved from .be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/values [moved from .be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9daa72ee-0721-4f68-99ee-f06fec0b340e/values [moved from .be/bugs/9daa72ee-0721-4f68-99ee-f06fec0b340e/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9f910ee0-ff0f-4fa3-b1e3-79a4118e48e9/values [moved from .be/bugs/9f910ee0-ff0f-4fa3-b1e3-79a4118e48e9/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body [moved from .be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values [moved from .be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body [moved from .be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values [moved from .be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body [moved from .be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values [moved from .be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/values [moved from .be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/body [moved from .be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values [moved from .be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/body [moved from .be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/values [moved from .be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/values [moved from .be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a63bd76a-cd43-4f97-88ba-2323546d4572/values [moved from .be/bugs/a63bd76a-cd43-4f97-88ba-2323546d4572/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body [moved from .be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values [moved from .be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body [moved from .be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values [moved from .be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body [moved from .be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values [moved from .be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values [moved from .be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/body [moved from .be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/values [moved from .be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values [moved from .be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b1bc6f39-8166-46c5-a724-4c4a3e1e7d74/values [moved from .be/bugs/b1bc6f39-8166-46c5-a724-4c4a3e1e7d74/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/2a51d90a-d47e-4a67-abe7-cce19c1eafad/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/2a51d90a-d47e-4a67-abe7-cce19c1eafad/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/body [moved from .be/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/values [moved from .be/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/values [moved from .be/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/values with 92% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3c6da51-3a30-42c9-8c75-587c7a1705c5/values [moved from .be/bugs/b3c6da51-3a30-42c9-8c75-587c7a1705c5/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body [moved from .be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values [moved from .be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values [moved from .be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/27a5a4cc-1782-4509-a3d2-db00c190f97d/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/27a5a4cc-1782-4509-a3d2-db00c190f97d/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/76d54016-755b-42ca-ad07-eb9a1c77c33d/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/76d54016-755b-42ca-ad07-eb9a1c77c33d/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/values [moved from .be/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/values with 92% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/body [moved from .be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/values [moved from .be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/body [moved from .be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values [moved from .be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/values [moved from .be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/body [moved from .be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/body [moved from .be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values [moved from .be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/body [moved from .be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/values [moved from .be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values [moved from .be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/body [moved from .be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/values [moved from .be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/body [moved from .be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/values [moved from .be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/body [moved from .be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/values [moved from .be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/body [moved from .be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/values [moved from .be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/values [moved from .be/bugs/c76d7899-d495-4103-9355-012c0a6fece3/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body [moved from .be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values [moved from .be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/body [moved from .be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values [moved from .be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body [moved from .be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values [moved from .be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values [moved from .be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cb56c990-a757-4aef-9888-a30918a7b3d7/values [moved from .be/bugs/cb56c990-a757-4aef-9888-a30918a7b3d7/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/body [moved from .be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/values [moved from .be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body [moved from .be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values [moved from .be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values [moved from .be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/body [moved from .be/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/values [moved from .be/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/values [moved from .be/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/body [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/body with 69% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/values [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/body [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/body with 86% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/values [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/body [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/body with 92% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/values [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/values [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/body [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/body with 82% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/values [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/body [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/body with 95% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/values [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/body [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/body with 98% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/values [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/body [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/body with 79% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/values [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/body [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/body with 91% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/values [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/values [moved from .be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/da2b09ff-af24-40f3-9b8d-6ffaa5f41164/values [moved from .be/bugs/da2b09ff-af24-40f3-9b8d-6ffaa5f41164/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/body [moved from .be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values [moved from .be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/body [moved from .be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/values [moved from .be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c/values [moved from .be/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/body [moved from .be/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/values [moved from .be/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/values [moved from .be/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0155831-499f-421a-ad02-cd15fc3fecf1/values [moved from .be/bugs/e0155831-499f-421a-ad02-cd15fc3fecf1/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/body [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/values [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/body [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/values [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/body [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/values [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/body [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/values [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/values with 78% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/body [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/values [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/body [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/values [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/values with 72% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/body [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/values [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/values [moved from .be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/body [moved from .be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/values [moved from .be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/body [moved from .be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/values [moved from .be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values [moved from .be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/2cd562f5-fcb9-4cc5-bf8c-ad5c9d960761/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/2cd562f5-fcb9-4cc5-bf8c-ad5c9d960761/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/68ec74b9-d2c7-421f-ac70-602b43bbd263/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/68ec74b9-d2c7-421f-ac70-602b43bbd263/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/body [moved from .be/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/values [moved from .be/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/body [moved from .be/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/values [moved from .be/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/body [moved from .be/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/values [moved from .be/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/body [moved from .be/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/values [moved from .be/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/body [moved from .be/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/values [moved from .be/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/values [moved from .be/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ecc91b94-7f3f-44a7-af58-03191d327a7f/values [moved from .be/bugs/ecc91b94-7f3f-44a7-af58-03191d327a7f/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9525e3f3-a044-4fa9-b311-56336267b8b5/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9525e3f3-a044-4fa9-b311-56336267b8b5/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9c4b8921-7b43-4bb6-b650-34144b414dc0/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9c4b8921-7b43-4bb6-b650-34144b414dc0/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/c664b7be-ded5-42dd-a16a-82b2bdb52e36/body [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/c664b7be-ded5-42dd-a16a-82b2bdb52e36/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values [moved from .be/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values with 60% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values [new file with mode: 0644]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values [moved from .be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values with 59% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f65b680b-4309-43a2-ae2d-e65811c9d107/values [moved from .be/bugs/f65b680b-4309-43a2-ae2d-e65811c9d107/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body [moved from .be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values [moved from .be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values [moved from .be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f77fc673-c852-4c81-bfa2-1d59de2661c8/values [moved from .be/bugs/f77fc673-c852-4c81-bfa2-1d59de2661c8/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body [moved from .be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values [moved from .be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body [moved from .be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values [moved from .be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body [moved from .be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values [moved from .be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/body [moved from .be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/body with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/values [moved from .be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values [moved from .be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values with 100% similarity]
.be/bea86499-824e-4e77-b085-2d581fa9ccab/settings [moved from .be/settings with 90% similarity]
.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values [deleted file]
.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values [deleted file]
.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values [deleted file]
.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values [deleted file]
.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values [deleted file]
.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values [deleted file]
.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values [deleted file]
.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values [deleted file]
.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values [deleted file]
.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/body [deleted file]
.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/values [deleted file]
.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/body [deleted file]
.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values [deleted file]
.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values [deleted file]
.be/version
AUTHORS
Makefile
NEWS
README
README.dev [deleted file]
be
becommands/assign.py [deleted file]
becommands/comment.py [deleted file]
becommands/commit.py [deleted file]
becommands/depend.py [deleted file]
becommands/diff.py [deleted file]
becommands/help.py [deleted file]
becommands/html.py [deleted file]
becommands/init.py [deleted file]
becommands/list.py [deleted file]
becommands/merge.py [deleted file]
becommands/new.py [deleted file]
becommands/remove.py [deleted file]
becommands/set.py [deleted file]
becommands/severity.py [deleted file]
becommands/show.py [deleted file]
becommands/status.py [deleted file]
becommands/subscribe.py [deleted file]
becommands/tag.py [deleted file]
becommands/target.py [deleted file]
doc/Makefile [new file with mode: 0644]
doc/conf.py [new file with mode: 0644]
doc/distributed_bugtracking.txt [new file with mode: 0644]
doc/doc.txt [new file with mode: 0644]
doc/email.txt [new symlink]
doc/generate-libbe-txt.py [new file with mode: 0644]
doc/hacking.txt [new file with mode: 0644]
doc/html.txt [new file with mode: 0644]
doc/index.txt [new file with mode: 0644]
doc/install.txt [new file with mode: 0644]
doc/man/be.1.sgml [moved from doc/be.1.sgml with 100% similarity]
doc/man/module.mk [moved from doc/module.mk with 89% similarity]
doc/spam.txt [new file with mode: 0644]
doc/tutorial.txt [new file with mode: 0644]
interfaces/README [deleted file]
interfaces/email/interactive/README
interfaces/email/interactive/be-handle-mail
interfaces/email/interactive/becommands [deleted symlink]
interfaces/email/interactive/examples/email_bugs [new file with mode: 0644]
interfaces/email/interactive/send_pgp_mime.py
interfaces/gui/beg/beg [deleted file]
interfaces/gui/beg/table.py [deleted file]
interfaces/gui/wxbe/wxbe [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/not-zip-safe [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/PKG-INFO [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/SOURCES.txt [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/dependency_links.txt [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/not-zip-safe [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/paster_plugins.txt [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/requires.txt [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/sqlobject.txt [deleted file]
interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/top_level.txt [deleted file]
interfaces/web/Bugs-Everywhere-Web/README.txt [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/__init__.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/app.cfg [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/config.py.example [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/config/app.cfg [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/config/log.cfg [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/formatting.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/json.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/model.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/prest.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/release.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/css/style.css [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-b.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-bl.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-br.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-l.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-r.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-t.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tl.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tr.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-b.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-r.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.ico [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/half-spiral.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/header_inner.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/info.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-b.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-bl.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-br.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-l.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-r.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-t.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tl.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tr.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ok.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/shadows.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/spiral.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/tg_under_the_hood.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/static/images/under_the_hood_blue.png [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/templates/__init__.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/templates/about.kid [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/templates/bugs.kid [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/templates/error.kid [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/templates/login.kid [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/templates/master.kid [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/templates/projects.kid [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/templates/welcome.kid [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/tests/__init__.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_controllers.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_model.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/dev.cfg [deleted file]
interfaces/web/Bugs-Everywhere-Web/libbe [deleted symlink]
interfaces/web/Bugs-Everywhere-Web/prod.cfg [deleted file]
interfaces/web/Bugs-Everywhere-Web/sample-prod.cfg [deleted file]
interfaces/web/Bugs-Everywhere-Web/server.log [deleted file]
interfaces/web/Bugs-Everywhere-Web/setup-tables.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/setup.py [deleted file]
interfaces/web/Bugs-Everywhere-Web/start-beweb.py [deleted file]
libbe/__init__.py
libbe/arch.py [deleted file]
libbe/beuuid.py [deleted file]
libbe/bug.py
libbe/bugdir.py
libbe/bzr.py [deleted file]
libbe/cmdutil.py [deleted file]
libbe/command/__init__.py [new file with mode: 0644]
libbe/command/assign.py [new file with mode: 0644]
libbe/command/base.py [new file with mode: 0644]
libbe/command/close.py [moved from becommands/close.py with 89% similarity]
libbe/command/comment.py [new file with mode: 0644]
libbe/command/commit.py [new file with mode: 0644]
libbe/command/depend.py [new file with mode: 0644]
libbe/command/diff.py [new file with mode: 0644]
libbe/command/due.py [new file with mode: 0644]
libbe/command/email_bugs.py [new file with mode: 0644]
libbe/command/help.py [new file with mode: 0644]
libbe/command/html.py [new file with mode: 0644]
libbe/command/import_xml.py [new file with mode: 0644]
libbe/command/init.py [new file with mode: 0644]
libbe/command/list.py [new file with mode: 0644]
libbe/command/merge.py [new file with mode: 0644]
libbe/command/new.py [new file with mode: 0644]
libbe/command/open.py [moved from becommands/open.py with 89% similarity]
libbe/command/remove.py [new file with mode: 0644]
libbe/command/serve.py [new file with mode: 0644]
libbe/command/set.py [new file with mode: 0644]
libbe/command/severity.py [new file with mode: 0644]
libbe/command/show.py [new file with mode: 0644]
libbe/command/status.py [new file with mode: 0644]
libbe/command/subscribe.py [new file with mode: 0644]
libbe/command/tag.py [new file with mode: 0644]
libbe/command/target.py [new file with mode: 0644]
libbe/command/util.py [new file with mode: 0644]
libbe/comment.py
libbe/darcs.py [deleted file]
libbe/diff.py
libbe/error.py [new file with mode: 0644]
libbe/git.py [deleted file]
libbe/hg.py [deleted file]
libbe/settings_object.py [deleted file]
libbe/storage/__init__.py [new file with mode: 0644]
libbe/storage/base.py [new file with mode: 0644]
libbe/storage/http.py [new file with mode: 0644]
libbe/storage/util/__init__.py [moved from becommands/__init__.py with 100% similarity]
libbe/storage/util/config.py [moved from libbe/config.py with 54% similarity]
libbe/storage/util/mapfile.py [moved from libbe/mapfile.py with 59% similarity]
libbe/storage/util/properties.py [moved from libbe/properties.py with 52% similarity]
libbe/storage/util/settings_object.py [new file with mode: 0644]
libbe/storage/util/upgrade.py [new file with mode: 0644]
libbe/storage/vcs/__init__.py [new file with mode: 0644]
libbe/storage/vcs/arch.py [new file with mode: 0644]
libbe/storage/vcs/base.py [new file with mode: 0644]
libbe/storage/vcs/bzr.py [new file with mode: 0644]
libbe/storage/vcs/darcs.py [new file with mode: 0644]
libbe/storage/vcs/git.py [new file with mode: 0644]
libbe/storage/vcs/hg.py [new file with mode: 0644]
libbe/ui/__init__.py [new file with mode: 0644]
libbe/ui/command_line.py [new file with mode: 0644]
libbe/ui/util/__init__.py [new file with mode: 0644]
libbe/ui/util/editor.py [moved from libbe/editor.py with 79% similarity]
libbe/ui/util/pager.py [new file with mode: 0644]
libbe/ui/util/user.py [new file with mode: 0644]
libbe/upgrade.py [deleted file]
libbe/util/__init__.py [new file with mode: 0644]
libbe/util/encoding.py [moved from libbe/encoding.py with 61% similarity]
libbe/util/id.py [new file with mode: 0644]
libbe/util/plugin.py [moved from libbe/plugin.py with 51% similarity]
libbe/util/subproc.py [new file with mode: 0644]
libbe/util/tree.py [moved from libbe/tree.py with 59% similarity]
libbe/util/utility.py [moved from libbe/utility.py with 51% similarity]
libbe/vcs.py [deleted file]
libbe/version.py
misc/xml/be-mail-to-xml [moved from interfaces/xml/be-mbox-to-xml with 75% similarity]
misc/xml/be-xml-to-mbox [moved from interfaces/xml/be-xml-to-mbox with 70% similarity]
misc/xml/catmutt [moved from interfaces/email/catmutt with 100% similarity]
release.py [new file with mode: 0755]
setup.py
test.py
test_upgrade.py [new file with mode: 0755]
test_usage.sh
update_copyright.py [new file with mode: 0755]
update_copyright.sh [deleted file]

diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/b8e5c376-32a4-42ea-b6b2-adbee069384a/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/b8e5c376-32a4-42ea-b6b2-adbee069384a/body
new file mode 100644 (file)
index 0000000..6af098a
--- /dev/null
@@ -0,0 +1,19 @@
+On Wed, Jan 20, 2010 at 01:24:25PM -0500, W. Trevor King wrote:
+> Of course, incorperating interactive functionality in command output
+> (i.e. changing the bug target from the bug-show page), doesn't fit
+> into this model.  To do that, we'd have to abstract the default
+> command output the way we've already abstracted the commands and their
+> input...
+
+Does anyone know of any output-abstraction implementations to look at
+for inspiration.
+  * How would we handle the options we currently pass through
+    (shortlist, show_comments, etc.)?
+  * Would standard arguments know how to display themselves?
+    class Status (Argument):
+        def str(self, ui, command, *args, **kwargs):
+            ui.display_status(self, command, *args, **kwargs)
+    class Bug (Argument):
+        def str(self, ui, command, *args, **kwargs):
+            ui.display_bug(self, command, *args, **kwargs)
+    ...
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/b8e5c376-32a4-42ea-b6b2-adbee069384a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/b8e5c376-32a4-42ea-b6b2-adbee069384a/values
new file mode 100644 (file)
index 0000000..378cc67
--- /dev/null
@@ -0,0 +1,14 @@
+Alt-id: <20100120183646.GC14791@mjolnir>
+
+
+Author: '"W. Trevor King" <wking@drexel.edu>'
+
+
+Content-type: text/plain
+
+
+Date: Wed, 20 Jan 2010 18:36:46 +0000
+
+
+In-reply-to: <20100120182425.GB14791@mjolnir>
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/f5139012-e20b-4d24-90a5-10d969ddd364/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/f5139012-e20b-4d24-90a5-10d969ddd364/body
new file mode 100644 (file)
index 0000000..636137c
--- /dev/null
@@ -0,0 +1,76 @@
+On Wed, Jan 20, 2010 at 09:34:44AM -0500, W. Trevor King wrote:
+> On Sun, Dec 06, 2009 at 04:47:23AM -0500, W. Trevor King wrote:
+> > Steve, I've caught my CFBE branch up to my current pre-trunk BE and
+> > added dependency links to the bug page, so you should be all set once
+> > you get back to CFBE.
+> 
+> And I haven't pulled it up to date with my recent reorganization.  As
+> far as release tarballs go though, we don't have to port to Bazaar at
+> all, we can stuff a recent CFBE snapshot into the BE tarball.  How
+> do people feel about that?
+
+Ok, I've got CFBE working with my BE head:
+  http://www.physics.drexel.edu/~wking/code/hg/cfbe/
+However, I haven't reworked CFBE to take advantage of the new command
+structure.
+
+We'll need to extend libbe.command.base.Argument a bit as we work this
+out, but I expect we can auto-generate handlers for various commands
+with something along the lines of:
+
+<snip web.py>
+
+class CommandHandler (object):
+    def __init__(self, command):
+        self.command = command
+    def __call__(self, *args, **kwargs):
+        if GET:
+            template = self.env.get_template('command.html')
+            return template.render(command=self.command)
+       else:
+            try:
+                ret = libbe.ui.command_line.dispatch(
+                    self.command.ui, self.command, *args, **kwargs)
+            except libbe.command.UserError, e:
+                HANDLE ERROR
+            stdout = self.command.ui.get_stdout()
+            DISPLAY STDOUT OR REDIRECT...
+
+class WebInterface (libbe.command.UserInterface):
+    ...
+    def add_commands(self):
+        for command_name in libbe.command.commands():
+            Class = libbe.command.get_command_class(
+                command_name=command_name)
+            command = Class(ui=self)
+            self.command_name = cherrypy.expose(
+                CommandHandler(command))
+
+</snip web.py>
+
+<snip command.html>
+
+<form id="command-form" action="/command" method="post">
+    <fieldset>
+        {% for option in command.options %}
+         {{ option_form_html(option) }}
+        {% endfor %}
+        {% for argument in command.args %}
+         {{ argument_form_html(argument) }}
+        {% endfor %}
+    </fieldset>
+</form>
+
+{{ command.help() }}
+
+</snip command.html>
+
+Of course, incorperating interactive functionality in command output
+(i.e. changing the bug target from the bug-show page), doesn't fit
+into this model.  To do that, we'd have to abstract the default
+command output the way we've already abstracted the commands and their
+input...  This sounds like a lot of work, and it is, but the goal is
+that BE adds functionality (new commands, option, etc.), and CFBE,
+be-handle-mail, etc. automatically incorperate the new stuff.
+
+Thoughts?
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/f5139012-e20b-4d24-90a5-10d969ddd364/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/f5139012-e20b-4d24-90a5-10d969ddd364/values
new file mode 100644 (file)
index 0000000..fb6ab4e
--- /dev/null
@@ -0,0 +1,14 @@
+Alt-id: <20100120182425.GB14791@mjolnir>
+
+
+Author: '"W. Trevor King" <wking@drexel.edu>'
+
+
+Content-type: text/plain
+
+
+Date: Wed, 20 Jan 2010 18:24:25 +0000
+
+
+In-reply-to: <20100120143444.GA14451@mjolnir>
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/values
new file mode 100644 (file)
index 0000000..d1b7cbe
--- /dev/null
@@ -0,0 +1,14 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: open
+
+
+summary: Need command output abstraction for flexible UIs
+
+
+time: Wed, 20 Jan 2010 20:35:12 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/comments/2f9beed6-4008-442a-8d44-a45cb7ce0a36/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/comments/2f9beed6-4008-442a-8d44-a45cb7ce0a36/body
new file mode 100644 (file)
index 0000000..d20af30
--- /dev/null
@@ -0,0 +1,9 @@
+I'm not sure that changing the URLs is a good idea.  I'd rather use
+.htaccess and mod_rewrite to redirect short URLs to their permanent
+long equivalents.  Nobody else seems to mind though, so I've merged
+Gianluca's solution with a few changes:
+  * Since we're truncating bug IDs, truncate comment IDs too.
+  * Use libbe.util.id._truncate to generate the short IDs, so that `be
+    html` truncation is consistent with general BE truncation.
+  * Updated cross-linking code to match.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/comments/2f9beed6-4008-442a-8d44-a45cb7ce0a36/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/comments/2f9beed6-4008-442a-8d44-a45cb7ce0a36/values
new file mode 100644 (file)
index 0000000..c7365f5
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 20 Feb 2010 18:10:42 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/values
new file mode 100644 (file)
index 0000000..9efe209
--- /dev/null
@@ -0,0 +1,17 @@
+creator: Gianluca Montecchi <gian@grys.it>
+
+
+reporter: Gianluca Montecchi <gian@grys.it>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Short the files name used by the be html command
+
+
+time: Tue, 09 Feb 2010 23:03:33 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values
new file mode 100644 (file)
index 0000000..9cfd081
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 16 Nov 2008 20:36:20 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values
new file mode 100644 (file)
index 0000000..9e40714
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 13 Nov 2008 19:31:04 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values
new file mode 100644 (file)
index 0000000..ce0ab73
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 13 Nov 2008 20:18:02 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/body
new file mode 100644 (file)
index 0000000..861fb1d
--- /dev/null
@@ -0,0 +1,14 @@
+> We also have the unfortunate situation of duplicate UUIDs from the old
+>   be merge
+> implemtation.  This means that id-to-path is not a well defined
+> mapping with single-uuid ids.  That's ok though, we get a bit uglier
+> and send the long_user() id into the storage backend instead.  While
+> not so elegant, this will avoid the need for the cached id/path table.
+
+The situation is worse than just the old `be merge` effects, because
+the existence, children, and parents of a particular UUID may be
+revision dependent.  A UUID will always refer to the same
+bugdir/bug/comment, but that bugdir/bug/comment may have different
+relatives.  Another point in favor of long_user()-style storage ids,
+but that just pushes relation-tracking up to the command level.  I'm
+still figuring out a good way to deal with this...
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/values
new file mode 100644 (file)
index 0000000..65e4472
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 28 Dec 2009 12:12:45 +0000
+
+
+In-reply-to: bd1207ef-f97e-4078-8c5d-046072012082
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/body
new file mode 100644 (file)
index 0000000..df5b8c5
--- /dev/null
@@ -0,0 +1,14 @@
+> The situation is worse than just the old `be merge` effects, because
+> the existence, children, and parents of a particular UUID may be
+> revision dependent.  A UUID will always refer to the same
+> bugdir/bug/comment, but that bugdir/bug/comment may have different
+> relatives.
+
+I'm not sure how to support .children(revision) in the Arch backend
+or the older versions of Darcs without checking out a pristine tree
+for the revision in question.  That's how we used to support
+  BugDir.duplicate_bugdir()
+but it doesn't fit well with the new Storage system.  Since I don't
+feel strongly about tla or old Darcs support, I'm leaving that
+functionality unimplemented.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/values
new file mode 100644 (file)
index 0000000..d21650d
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 29 Dec 2009 16:20:06 +0000
+
+
+In-reply-to: 3646e056-a2df-46e5-b877-88608c7cc5af
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/body
new file mode 100644 (file)
index 0000000..abb898c
--- /dev/null
@@ -0,0 +1,30 @@
+Rather than all the hackery that goes on with email-bugs, the email
+interface, etc., it would be nice for distribution if be provided a
+uniform issue/bug tracking library and a number of interfaces and
+backends.
+
+Current backends:
+  filesystem (with assorted VCSs)
+Current UIs:
+  command line (be)
+  email (be-handle-mail)
+  web (CFBE)
+
+Future backend architecture:
+  be --repo REPO ...
+where --repo REPO replaces and extends the current --dir DIR.  Example
+REPOs could be
+  path/to/repo                              (the current DIR)
+  http://some-server.com:port/path/to/repo  (http interface)
+  mysql://user@server:port/?db=db-name;pwd=password
+  ...
+Each repo would have to support a few get/set commands at the bugdir,
+bug, and comment level.
+
+The UIs would all load BugDir(REPO), and thus be backend agnostic.
+This way a GUI app that let you work on your own machine could also be
+used to work on a public repository.  Setting up a public repository
+would just consist of exposing one of the wire-capable REPO formats
+(e.g. http via a future `be serve MY-URL`) with public write
+permissions.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/values
new file mode 100644 (file)
index 0000000..d2e65d3
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 08 Dec 2009 01:06:12 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/body
new file mode 100644 (file)
index 0000000..21170a2
--- /dev/null
@@ -0,0 +1,45 @@
+Some additional thoughts, as I've been developing this idea:
+
+Different BE storage versions will be difficult to handle.
+We currently do disk upgrades via
+  libbe.storage.util.upgrade
+which browses through the .be/ directory, making appropriate changes.
+
+The new formats know very little about paths, which brought on the
+whole libbe.storage.vcs.base.CachedPathID bit.  Still, most VCSs
+seem to be able to handle renames, e.g.
+  $ bzr cat -r 200 ./libbe/command/new.py
+works, when as of revision 200, the file was
+  ./becommands/new.py
+In fact, bzr recognizes both names:
+  $ diff <(bzr cat -r 200 ./becommands/new.py) \
+         <(bzr cat -r 200 ./libbe/commands/new.py)
+returns nothing.  Still, I'm not sure this is something we should
+require in a storage backend.  Which means we'd need to have a
+version-dependent id-to-path(version) function.
+
+We also have the unfortunate situation of duplicate UUIDs from the old
+  be merge
+implemtation.  This means that id-to-path is not a well defined
+mapping with single-uuid ids.  That's ok though, we get a bit uglier
+and send the long_user() id into the storage backend instead.  While
+not so elegant, this will avoid the need for the cached id/path table.
+
+Ok, you say, we're fine if we have the compound bugdir/bug/comment ids
+going out to storage, with the upgrader upgrading the file
+appropriately for each file type.  Almost.  You'll still run into
+trouble with upgrades like dir format v1.2 to 1.3 where targets
+moved from a per-bug string to a seperate-bugs-with-dependencies.
+Now you need to create virtual-target-bugs on the fly when you're
+loading the old bugs.  Yuck.
+
+All of this makes me wonder how much we care about being able to
+see bug diffs for any repository format older than the current one.
+I think that we don't really care ;).  After all, the on-disk
+format should settle down as BE matures :p.  When you _do_ want
+to see the long-term history of a particular bug, there's always
+  bzr log .be/123/bugs/456/values
+or the equivalent for your VCS.  If access to the raw log ends
+up being important, it should be very easy to add
+  libbe.storage.base.VersionedStorage.log(id)
+  libbe.command.log
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/values
new file mode 100644 (file)
index 0000000..f0af48d
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 15 Dec 2009 12:21:11 +0000
+
+
+In-reply-to: bb406a33-92b6-46dd-950c-c7cfb5440e7b
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/values
new file mode 100644 (file)
index 0000000..f4b1032
--- /dev/null
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Reoranize BE for more flexible backend / frontend
+
+
+time: Tue, 08 Dec 2009 00:48:27 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/body
new file mode 100644 (file)
index 0000000..e7b48e0
--- /dev/null
@@ -0,0 +1,83 @@
+I read
+  http://weblog.masukomi.org/2008/1/3/distributed-bug-tracking
+yesterday, and the section on bug visibility got me thinking about
+bug 12c (Multi-repo meta-BE?) some more.
+
+We already have interfaces like this email/html mashup:
+
+On Sun, Sep 13, 2009 at 07:04:05AM -0400, W. Trevor King wrote:
+> Since the non-bzr interfaces to BE are coming along nicely, I've put
+> up a non-bzr interface to my be-rr branch.
+>   http://www.physics.drexel.edu/~wking/code/be
+> It uses nightly builds of Gianluca's static html from my devel branch
+> to provide read-only browsing, and accepts changes from the general
+> public through my email interface into a public branch.  I handle the
+> synchronization of these two branches manually.
+
+These interfaces provide a means for remote users to access a BE
+repository without bzr or the command line.  As far as users are
+concerned, this exposed repository looks pretty much like a
+centralized bugtracking system (e.g. bugzilla, ...).
+
+However, with BE we have more bug information living off in other
+branches that haven't yet been merged with the exposed repo.  The
+problem is two-fold:
+  1) how to keep up to date within a distributed community.
+  2) how do users find branches/patches that fix bug XYZ.
+
+For (2), I think the best solution at the moment are along the lines
+of my little scripts (discussed in the bug 12c comments).  With the
+addition of the `be diff --dir DIR` option, it's now even easier to
+find more information on bug 565 (or whatever UUID):
+  be/be.wtk$ for repo in ../*; do \
+              if [ $repo == "be.wtk" ]; then continue; fi; \
+              diff=$(be diff --dir $repo --subscribe 565:all); \
+              if [ -n "$diff" ]; then \
+                echo "Changed from $repo:"; echo "$diff"; \
+              fi; \
+             done
+  Changed from ../be.html:
+  New bugs:
+    565:fm: be email-bugs for bug submission from bzr-less users
+  Changed from ../be.trunk:
+  New bugs:
+    565:fm: be email-bugs for bug submission from bzr-less users
+  Changed from ../cherryflavoredbugseverywhere:
+  New bugs:
+    565:fm: be email-bugs for bug submission from bzr-less users
+where the --dir and --subscribe options to `be diff` are new.  If
+people don't like the command line, this would be easy to bundle into
+a web-frontend (CFBE?) if you wanted, with a cron job pulling updates
+into the tracked branches.
+
+I was starting into a solution for (1) when I did this:
+
+On Mon, Jul 27, 2009 at 08:42:19AM -0400, W. Trevor King wrote:
+> My email interface now supports subscription:
+>   be subscribe DIR       # see any changes to the bug directory.
+>   be subscribe BUG-ID    # see changes to a particular bug.
+> See
+>   be subscribe --help
+> for more details.
+
+The idea was that a dev/user would subscribe to whatever issues they
+wanted to track, and they would get email notifications whenever some
+action affected any of those issues.  These subscriptions would
+percolate through the distributed branches as a result of the usual
+mergers.  For example, my subscription to all changes has made it into
+the trunk branch (see .be/settings).
+
+This subscription mechanism was setup to work through interactive
+public interfaces (my email interface, eventually CFBE, ...), but
+it doesn't work for changes made via the command-line interface,
+so I browsed around a bit and ran across some interesting workflows
+in the bzr documentation
+  doc/developers/HACKING.txt, "Communicating and Coordinating"
+which points out the following plugins
+  * email (http://doc.bazaar-vcs.org/plugins/en/email-plugin.html)
+  * dbus (http://doc.bazaar-vcs.org/plugins/en/dbus-plugin.html)
+which send automatic notification messages after commits, etc.  If
+people want this sort of functionality, it would be easy enough to rig
+a hook for `be commit' that sent a diff email to subscribers, which
+could include be-devel.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/values
new file mode 100644 (file)
index 0000000..adb1ae5
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 05 Dec 2009 22:39:07 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/comments/85770405-0ead-4044-a3cf-082615ff1b6f/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/comments/85770405-0ead-4044-a3cf-082615ff1b6f/body
new file mode 100644 (file)
index 0000000..c49c15c
--- /dev/null
@@ -0,0 +1,22 @@
+This is an outgrowth of #bea86499-824e-4e77-b085-2d581fa9ccab/1100c966-9671-4bc6-8b68-6d408a910da1/bd1207ef-f97e-4078-8c5d-046072012082#:
+> All of this makes me wonder how much we care about being able to
+> see bug diffs for any repository format older than the current one.
+> I think that we don't really care ;).  After all, the on-disk
+> format should settle down as BE matures :p.  When you _do_ want
+> to see the long-term history of a particular bug, there's always
+>   bzr log .be/123/bugs/456/values
+> or the equivalent for your VCS.  If access to the raw log ends
+> up being important, it should be very easy to add
+>   libbe.storage.base.VersionedStorage.log(id)
+>   libbe.command.log
+
+Access to the (parsed) logs will be important for pretty-printing
+bugdir/bug/comment change logs.  Since we do version the bug
+repository, users will expect us to be able to list the history for
+any particular item (e.g. for "last activity" timestamps, automatic
+reminder emails, whatever).  While it does not necessarily need to be
+able to delve into old storage formats, it does need to get
+implemented.  It's probably worth encapsulating changes in something
+like a list of Diff() objects, although it might be worth linking
+along bug lines, etc., like VCS annotation.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/comments/85770405-0ead-4044-a3cf-082615ff1b6f/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/comments/85770405-0ead-4044-a3cf-082615ff1b6f/values
new file mode 100644 (file)
index 0000000..d29604d
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 29 Jan 2010 01:12:54 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/values
new file mode 100644 (file)
index 0000000..6d46f8f
--- /dev/null
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: wishlist
+
+
+status: open
+
+
+summary: Generating per-bugdir/bug/comment change logs
+
+
+time: Thu, 28 Jan 2010 23:10:48 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values
new file mode 100644 (file)
index 0000000..7beb827
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 14 Nov 2008 05:00:43 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/body
new file mode 100644 (file)
index 0000000..08595d1
--- /dev/null
@@ -0,0 +1,8 @@
+Implemented.
+
+You can now list targets by dependency (not by date, but better for
+most cases) with
+  be depend -t-1 --severity target ID
+where ID is the uuid of any target bug, or with
+  be depend -t-1 --severity target $(be target --resolve TARGET)
+where TARGET is the summary of any target bug.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/values
new file mode 100644 (file)
index 0000000..3925aa2
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 06 Dec 2009 05:42:52 +0000
+
+
+In-reply-to: 4012c6cc-1300-4f6b-af0e-9176eedf8de7
+
similarity index 94%
rename from .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values
rename to .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values
index 7440b56c2eab29762b8c2bed75b63ea7ea6e6c22..64928e86729185d52043195bd67054faead8d083 100644 (file)
@@ -14,7 +14,7 @@ reporter: Gianluca Montecchi <gian@grys.it>
 severity: wishlist
 
 
-status: assigned
+status: fixed
 
 
 summary: Sorting targets chronologically
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values
new file mode 100644 (file)
index 0000000..2a52700
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 13 Nov 2008 17:27:17 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46/body
new file mode 100644 (file)
index 0000000..288fc29
--- /dev/null
@@ -0,0 +1,11 @@
+It would be nice if we could store tests.
+  .be/BUGDIR/tests/...
+and link them from bugs.
+
+Then running
+  test.py BUGDIR/BUG
+would run the tests for that particular bug.
+
+This would provide regression testing via
+  test.py $(be list --ids --status fixed)
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46/values
new file mode 100644 (file)
index 0000000..691163d
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 03 Jan 2010 16:32:13 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/e7d8343a-bd85-4359-bcda-bf0dc1e8177a/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/e7d8343a-bd85-4359-bcda-bf0dc1e8177a/body
new file mode 100644 (file)
index 0000000..b6a0435
--- /dev/null
@@ -0,0 +1,42 @@
+> It would be nice if we could store tests.
+>   .be/BUGDIR/tests/...
+> and link them from bugs.
+
+Better: have them be comments with a TEST tag.
+
+The mime type could hint at the execution mechanism:
+  text/x-python
+  application/x-sh
+  ...
+
+> Then running
+>   test.py BUGDIR/BUG
+> would run the tests for that particular bug.
+> 
+> This would provide regression testing via
+>   test.py $(be list --ids --status fixed)
+
+This should be a 'test' command (libbe.command.test.Test), since
+people will want to test bugs for their own projects, and out current
+test.py is for testing BE specifically.  It should be
+  be test BUGDIR/BUG
+  be test $(be list --ids --status fixed)
+
+We _should_ add be
+  test $(be list --ids --status fixed)
+to test.py for regression testing.
+
+This whole thing would make the fixed/closed distinction more clear,
+since fixed bugs would get tests run and expect success, while closed
+bugs' tests would be skipped.
+
+Finally, if users are submitting tests on their own, it would be a
+good idea to sandbox them, but a portable way for sandboxing scripts
+sounds very complicated.  It would probably be easier to sandbox
+python scripts, but I don't know what that would look like...
+
+A work around would be to allow users to post tests, but not allow
+them to set the TEST flag.  Then the bugdir maintainer could set the
+flag themselves once they'd vetted the test.  Much uglier than
+sandboxing, but also much more easily implemented.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/e7d8343a-bd85-4359-bcda-bf0dc1e8177a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/e7d8343a-bd85-4359-bcda-bf0dc1e8177a/values
new file mode 100644 (file)
index 0000000..3ddceba
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 31 Jan 2010 17:36:52 +0000
+
+
+In-reply-to: ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/values
new file mode 100644 (file)
index 0000000..5c72e5f
--- /dev/null
@@ -0,0 +1,14 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: open
+
+
+summary: Attach tests to bugs
+
+
+time: Sun, 03 Jan 2010 16:23:42 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values
new file mode 100644 (file)
index 0000000..5e1f3de
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 13 Nov 2008 15:58:18 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values
new file mode 100644 (file)
index 0000000..f008963
--- /dev/null
@@ -0,0 +1,20 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+extra_strings:
+- BLOCKED-BY:f51dc5a7-37b7-4ce1-859a-b7cb58be6494
+- BLOCKS:4fc71206-4285-417f-8a3c-ed6fb31bbbda
+- BLOCKS:bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c
+
+
+severity: target
+
+
+status: fixed
+
+
+summary: '0.1'
+
+
+time: Sun, 06 Dec 2009 00:37:15 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values
new file mode 100644 (file)
index 0000000..4eebcc4
--- /dev/null
@@ -0,0 +1,20 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+extra_strings:
+- BLOCKED-BY:47c8fd5f-1f5a-4048-bef7-bb4c9a37c411
+- BLOCKED-BY:ee681951-f254-43d3-a53a-1b36ae415d5c
+- BLOCKS:bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c
+
+
+severity: target
+
+
+status: closed
+
+
+summary: patch-52
+
+
+time: Sun, 06 Dec 2009 00:37:16 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body
new file mode 100644 (file)
index 0000000..3ed77e7
--- /dev/null
@@ -0,0 +1,13 @@
+>   * Determining what to commit.
+> 
+>     You'd have to have RCS keep a log of all versioned files it
+>     touched, and extend .commit() to accept the keyword list "files"
+>     and commit only those files.  This is doable, but maybe not worth
+>     the trouble.
+
+On the other hand, just attemting to commit everything after each
+command would make it nice and easy to commit bug fixes:
+  be --auto-commit status XYZ fixed
+which would commit whatever changes you had outstanding with an
+appropriate commit message.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values
new file mode 100644 (file)
index 0000000..b3dba3f
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 06 Dec 2009 21:45:15 +0000
+
+
+In-reply-to: 4c50ca0b-a08f-4723-b00d-4bf342cf86b6
+
similarity index 77%
rename from .be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/values
rename to .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/values
index d060e87569ac97f19098b6db7e9e5ee69f41d498..5b332edbd62d80927a458dd66f5e412b4af0f816 100644 (file)
@@ -1,6 +1,10 @@
 creator: W. Trevor King <wking@drexel.edu>
 
 
+extra_strings:
+- BLOCKED-BY:5fb11e65-68a0-4015-b404-737238299cdc
+
+
 reporter: Martin F Krafft <madduck@debian.org>
 
 
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/body
new file mode 100644 (file)
index 0000000..8c890f3
--- /dev/null
@@ -0,0 +1,27 @@
+"W. Trevor King" <wking@drexel.edu> writes:
+
+> On Tue, Nov 17, 2009 at 01:41:26PM -0300, Nicolas Alvarez wrote:
+> > I'm using the latest version available on Debian
+> > (0.0.193+bzr.r217-2). I should ask for an updated package...
+>
+[…]
+
+> There is also an outstanding Debian bug for updating the Debian package
+>   http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=544515
+> so there may be a more current package on the way, but I don't know
+> about timeframes for that sort of thing.
+
+It would make it much easier on the Debian package maintainer if the
+Bugs Everywhere project would make conventional tarball releases, with
+conventional version numbers, with a changelog describing what has
+changed between versions.
+
+Trying to maintain a package of a project that is only made available by
+undifferentiated VCS revision numbers is a lot more effort, and so
+doesn't happen very often.
+
+-- 
+ \             “Roll dice!” “Why?” “Shut up! I don't need your fucking |
+  `\     *input*, I need you to roll dice!” —Luke Crane, demonstrating |
+_o__)                       his refined approach to play testing, 2009 |
+Ben Finney
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values
new file mode 100644 (file)
index 0000000..3b45fbf
--- /dev/null
@@ -0,0 +1,11 @@
+Alt-id: <87d43gn8ju.fsf_-_@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Wed, 18 Nov 2009 13:30:29 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/body
new file mode 100644 (file)
index 0000000..4ebb4f2
--- /dev/null
@@ -0,0 +1,51 @@
+"W. Trevor King" <wking@drexel.edu> writes:
+
+> ** NEWS file
+
+Speaking as the package maintainer, I would like a ‘ChangeLog’ file
+separate from a ‘NEWS’ file.
+
+The ‘NEWS’ file would continue to be hand-edited, and would be a
+high-level view of user-visible changes in the project each version.
+Users could reasonably expect to be interested in this file when
+installing a new version. It would also make sense to retire old news
+From this file once it becomes sufficiently old, to keep it relevant to
+users to read.
+
+
+The ‘ChangeLog’ would be an automatically-generated changelog of
+low-level changes, not for general human consumption but for letting
+recipients have a fighting chance at knowing the historical context of a
+particular change without access to the VCS. It would probably be best
+done as Trevor says:
+
+> Depending on our level of masochism, either something starting out
+> along the lines of [2]
+>   bzr log --gnu-changelog -n1 -r 200..
+
+That makes it necessary to add the changelog file to the tarball, since
+it won't be a file tracked by VCS and therefore won't be exported. Not a
+problem::
+
+    $ release_version="1.0.0"
+    $ release_name="be-$release_version"
+    $ tarball_file=../$release_name.tar.gz
+    $ work_dir=$(mktemp -t -d)
+    $ export_dir=$work_dir/$release_name
+    $ changelog_file=$export_dir/ChangeLog
+
+    $ bzr export $export_dir
+    $ bzr log --gnu-changelog -n1 -r ..tag:"$release_version" > $changelog_file
+    $ tar -czf $tarball_file $export_dir
+    $ rm -r $work_dir/
+
+    $ ls $tarball_file
+    ../be-1.0.0.tar.gz
+    $ tar -tzf $tarball_file | grep ChangeLog
+    be-1.0.0/ChangeLog
+
+-- 
+ \        “I bought a dog the other day. I named him Stay. It's fun to |
+  `\     call him. ‘Come here, Stay! Come here, Stay!’ He went insane. |
+_o__)         Now he just ignores me and keeps typing.” —Steven Wright |
+Ben Finney
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values
new file mode 100644 (file)
index 0000000..b45a747
--- /dev/null
@@ -0,0 +1,14 @@
+Alt-id: <873a4cmjw5.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Wed, 18 Nov 2009 22:23:06 +0000
+
+
+In-reply-to: a4720227-43cf-49aa-8f9f-f49f46e3e809
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/body
new file mode 100644 (file)
index 0000000..d00eb64
--- /dev/null
@@ -0,0 +1,22 @@
+Hi,
+
+   > It would make it much easier on the Debian package maintainer if
+   > the Bugs Everywhere project would make conventional tarball
+   > releases, with conventional version numbers, with a changelog
+   > describing what has changed between versions.
+
+Fair point.
+
+How do people feel about pushing for a 1.0 release, with Trevor's tree
+plus a finished cfbe merge?  Or would we rather wait until afterwards
+to try for cfbe?
+
+- Chris.
+-- 
+Chris Ball   <cjb@laptop.org>
+One Laptop Per Child
+
+_______________________________________________
+Be-devel mailing list
+Be-devel@bugseverywhere.org
+http://void.printf.net/cgi-bin/mailman/listinfo/be-devel
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/values
new file mode 100644 (file)
index 0000000..4fb068d
--- /dev/null
@@ -0,0 +1,14 @@
+Alt-id: <m3ocn09310.fsf@pullcord.laptop.org>
+
+
+Author: Chris Ball <cjb@laptop.org>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 17 Nov 2009 22:53:31 +0000
+
+
+In-reply-to: 1847f1f8-525a-42c4-ae2b-e9377459d2a6
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/body
new file mode 100644 (file)
index 0000000..a3fc57f
--- /dev/null
@@ -0,0 +1,36 @@
+I've written up a little release script that bundles all the steps
+we've mentioned so far into a single command.  Of course, we'll still
+have to keep NEWS up to date on our own.
+
+The output prints a trace of what's going on:
+
+  $ ./release.py 1.0.0
+  set libbe.version._VERSION = '1.0.0'
+  updating AUTHORS
+  updating ./becommands/assign.py
+  updating ./becommands/html.py
+  ...
+  commit current status: Bumped to version 1.0.0
+  tag current revision 1.0.0
+  export current revision to be-1.0.0
+  generate libbe/_version.py
+  copy libbe/_version.py to be-1.0.0/libbe/_version.py
+  generate ChangeLog file be-1.0.0/ChangeLog up to tag 1.0.0
+  set vcs_name in be-1.0.0/.be/settings to None
+  create tarball be-1.0.0.tar.gz
+  remove be-1.0.0
+
+Since we'll be distributing a non-bzr-repo version, it would be nice
+to adapt our 'submit bug' procedure (outlined on the main page) to one
+that works with this setup.  Without guaranteed versioning, that would
+probably be something along the lines of
+  be email-bugs [--to be-devel@bugseverywhere.org] BUG-ID ...
+With interfaces/email/interactive listening on the recieving end to
+grab new-bug emails and import them into an incoming bug repository.
+
+-- 
+This email may be signed or encrypted with GPG (http://www.gnupg.org).
+The GPG signature (if present) will be attached as 'signature.asc'.
+For more information, see http://en.wikipedia.org/wiki/Pretty_Good_Privacy
+
+My public key is at http://www.physics.drexel.edu/~wking/pubkey.txt
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values
new file mode 100644 (file)
index 0000000..b6d25cb
--- /dev/null
@@ -0,0 +1,14 @@
+Alt-id: <20091120132219.GA17577@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 20 Nov 2009 13:22:19 +0000
+
+
+In-reply-to: 49e0425b-3332-4d0e-b371-300eccd55370
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/body
new file mode 100644 (file)
index 0000000..5d29f85
--- /dev/null
@@ -0,0 +1,102 @@
+On Tue, Nov 17, 2009 at 05:53:31PM -0500, Chris Ball wrote:
+>   > It would make it much easier on the Debian package maintainer if
+>   > the Bugs Everywhere project would make conventional tarball
+>   > releases, with conventional version numbers, with a changelog
+>   > describing what has changed between versions.
+> How do people feel about pushing for a 1.0 release, with Trevor's tree
+> plus a finished cfbe merge?  Or would we rather wait until afterwards
+> to try for cfbe?
+
+Sounds good to me.  Not that my tree is much ahead of the trunk at the
+moment.  We've talked over most of these issues a few times, so I'll
+just summarize where I think we stand on the steps needed to make a
+release.
+
+** cfbe integration
+
+Postpone until we work out bzr/hg versioning [1]?
+
+** Conventional version number
+
+Set to "1.0.0" using libbe.version._VERSION.
+
+** NEWS file
+
+Depending on our level of masochism, either something starting out
+along the lines of [2]
+  bzr log --gnu-changelog -n1 -r 200..
+(commit 200, or
+  aaron.bentley@utoronto.ca-20060411035623-9b8d222282a26ce1
+ was the last time anyone touched the NEWS file),
+or a much abbreviated entry [3,4], along the lines of my current NEWS
+file (changed just a few minutes ago).
+
+** Tag bzr commit
+
+  bzr tag 1.0.0
+
+** Create tarball
+
+From Ben[5]:
+  bzr export /tmp/be-1.0.0.tar.gz
+
+
+References:
+
+[1]
+On Thu, Jul 23, 2009 at 05:38:03PM -0400, Steve Losh wrote:
+> On Jul 21, 2009, at 9:59 AM, W. Trevor King wrote:
+> > Steve's also versioning it with Mercurial.  Will he mind changing to
+> > Bazaar?
+>
+> Yeah, I've tried bazaar but really don't like the interface at all.  If 
+> everyone else really wants me to move it over I guess I can though.
+
+[2]
+On Tue, Jul 14, 2009 at 11:05:38AM -0400, Chris Ball wrote:
+> Actually, there's a `bzr log --gnu-changelog` now, and `bzr help
+> log-formats` offers some more styles.  (None of them seem to match
+> my preferred style for release announcements exactly, which would
+> be `git shortlog`-style.)
+
+[3]
+On Thu, Jul 16, 2009 at 07:21:10PM +1000, Ben Finney wrote:
+> I actually don't think the commit log needs to be part of the release at
+> all. It's of interest only to those who want fine-level detail about
+> changes to every file, and for that purpose I think read access to the
+> VCS is much better. Packaging a static copy of the commit log as plain
+> text seems pointless.
+> 
+> Rather, we should treat a user-changes level “NEWS” file (or whatever
+> name we choose for it) as part of the documentation, and set the
+> expectation among the team that it will be updated for each user-visible
+> change being worked on, like any other documentation.
+
+[4]
+On Tue, Jul 14, 2009 at 11:11:31AM -0400, Chris Ball wrote:
+> Hi,
+> 
+>    > That's not a changelog, that's a commit log of every source-level
+>    > commit made. Far too much detail for a changelog of
+>    > *user-visible* changes associated with a release.
+> 
+> I think I agree with both of you. :) It seems like it's both true that
+> there's no point in keeping a GNU-style ChangeLog these days, and that
+> if we make a release we should write an announce mail that directly
+> mentions new user-visible changes as well as attaching the commit log.
+> That smaller list of highly user-visible changes could live in NEWS,
+> or in the announce mail, or both.
+
+[5]
+On Wed, Jul 15, 2009 at 12:54:05AM +1000, Ben Finney wrote:
+> Even better: ‘bzr export /tmp/foo.tar.gz’ will create a source tarball
+> of all the files in the branch's VCS inventory. All we need to do is
+> start the practice of tagging a release in the VCS, and export the
+> tarball at that time.
+
+-- 
+This email may be signed or encrypted with GPG (http://www.gnupg.org).
+The GPG signature (if present) will be attached as 'signature.asc'.
+For more information, see http://en.wikipedia.org/wiki/Pretty_Good_Privacy
+
+My public key is at http://www.physics.drexel.edu/~wking/pubkey.txt
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values
new file mode 100644 (file)
index 0000000..7f205d6
--- /dev/null
@@ -0,0 +1,14 @@
+Alt-id: <20091118011403.GB9503@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Wed, 18 Nov 2009 01:14:03 +0000
+
+
+In-reply-to: 72a519e3-3d6b-4f0f-b412-1310efd255eb
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/body
new file mode 100644 (file)
index 0000000..dee72c7
--- /dev/null
@@ -0,0 +1,2 @@
+Verdict: run releases.py periodically, and post the tarballs on the
+web.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/values
new file mode 100644 (file)
index 0000000..2e85e56
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 20 Nov 2009 21:45:50 +0000
+
similarity index 93%
rename from .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values
rename to .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values
index b9e8dff6e9978ed600efcf78cc5469464c4869c0..89203d2da067cccea660bc4df4a6044715fd17bf 100644 (file)
@@ -7,7 +7,7 @@ reporter: W. Trevor King <wking@drexel.edu>
 severity: wishlist
 
 
-status: open
+status: fixed
 
 
 summary: How should we version BE?
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body
new file mode 100644 (file)
index 0000000..8596c92
--- /dev/null
@@ -0,0 +1,18 @@
+Since we'll be distributing a non-bzr-repo version, it would be nice
+to adapt our 'submit bug' procedure
+  $ be new "The demuxulizer is broken"
+  Created bug with ID 48f
+  $ be comment 48f
+  <Describe bug>
+  $ bzr commit --message "Reported bug in demuxulizer"
+  $ bzr send --mail-to "be-devel@bugseverywhere.org"
+to one that works with this setup.  Without guaranteed versioning,
+that would probably be something along the lines of
+  $ be new "The demuxulizer is broken"
+  Created bug with ID 48f
+  $ be comment 48f
+  <Describe bug>
+  $ be email-bugs [--to be-devel@bugseverywhere.org] 48f
+With interfaces/email/interactive listening on the recieving end to
+grab new-bug emails and import them into an incoming bug repository.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values
new file mode 100644 (file)
index 0000000..4bd8f81
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 20 Nov 2009 13:31:25 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body
new file mode 100644 (file)
index 0000000..d3d9d0c
--- /dev/null
@@ -0,0 +1,28 @@
+> With interfaces/email/interactive listening on the recieving end to
+> grab new-bug emails and import them into an incoming bug repository.
+
+The email-bugs -> be-handle-mail import is based on `be import-xml`.
+The current import-xml implementation allows good control over what
+gets overwritten during a merge by overriding only those fields
+defined in the incoming XML.
+
+For clients without the versioned bugdir (e.g. they installed via a
+release tarball or their distro's packaging system), `be email-bugs`
+will not know what fields have been changed/added/etc., so it sets
+_all_ the fields in the outgoing XML.  Importing that XML file will
+override any changes that may have been made to the listed
+bugs/comments between the release and your current source version, so
+you may have to do some manual tweaking of the post-merge bugdir.
+
+One possible workaround would be to change the merge algorithm in
+import-xml to take advantage of version information given in the XML
+file.  import-xml could checkout the shared root version of any
+modified bugs, and compute the changes made by the remote user and
+those made in the local tree.  It could then merge these changes more
+intelligently, by prompting the user, keeping the local changes,
+keeping the remote changes, etc.
+
+While the more automated approach might be better, it's also more
+complicated, so for now we'll stick with the simple "override all
+fields defined in the XML" approach.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values
new file mode 100644 (file)
index 0000000..e77ec55
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 29 Nov 2009 01:19:05 +0000
+
+
+In-reply-to: 0a995544-20dc-42a6-8d3f-348ebbc8921e
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/values
new file mode 100644 (file)
index 0000000..2d546cb
--- /dev/null
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: be email-bugs for bug submission from bzr-less users
+
+
+time: Fri, 20 Nov 2009 13:26:59 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/628a050a-f969-4290-8468-f5e991528f40/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/628a050a-f969-4290-8468-f5e991528f40/body
new file mode 100644 (file)
index 0000000..40d9e29
--- /dev/null
@@ -0,0 +1,11 @@
+Either of these could be added at the
+  libbe.command.base.Command.run
+level.
+
+The Git hooks would be 'pre-<command-name>' and 'post-<command-name>'.
+
+Oh, and the hooks are therefore command-level hooks, not storage-level
+hooks.  We still want storage-level hooks for notification emails, etc,
+and they would definately have to follow the Git directory approach.
+Hmm.  Storage level hooks will be awkward...
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/628a050a-f969-4290-8468-f5e991528f40/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/628a050a-f969-4290-8468-f5e991528f40/values
new file mode 100644 (file)
index 0000000..decd72f
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 31 Jan 2010 18:04:49 +0000
+
+
+In-reply-to: f3e90a7e-b8c4-4a7c-8609-6a783ae59762
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/f3e90a7e-b8c4-4a7c-8609-6a783ae59762/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/f3e90a7e-b8c4-4a7c-8609-6a783ae59762/body
new file mode 100644 (file)
index 0000000..80133bf
--- /dev/null
@@ -0,0 +1,18 @@
+Provide hooks so users can easily setup auto-commits, subscriber
+notification, etc.  Probably either Darcs-style options:
+  $ be COMMAND --help
+  ...
+    --posthook=COMMAND   Specify command to run after this command.
+    --no-posthook        Do not run posthook command.
+    --prompt-posthook    Prompt before running posthook. [DEFAULT]
+    --run-posthook       Run posthook command without prompting.
+  ...
+or a Git-style hooks directory:
+  $ tree .be
+  .be/
+  |-- version
+  |-- hooks
+  .   |-- post-commit.sh
+  .   |-- pre-commit.sh
+      `-- update.sh
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/f3e90a7e-b8c4-4a7c-8609-6a783ae59762/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/f3e90a7e-b8c4-4a7c-8609-6a783ae59762/values
new file mode 100644 (file)
index 0000000..4cd8b7a
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 23 Jan 2010 19:17:10 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/values
new file mode 100644 (file)
index 0000000..3e88ad1
--- /dev/null
@@ -0,0 +1,21 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+extra_strings:
+- BLOCKS:52034fd0-ec50-424d-b25d-2beaf2d2c317
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: open
+
+
+summary: Add change hooks to Storage class
+
+
+time: Sat, 23 Jan 2010 19:08:40 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/comments/a555d577-7f8c-49f2-96f6-263ce5fdff8e/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/comments/a555d577-7f8c-49f2-96f6-263ce5fdff8e/body
new file mode 100644 (file)
index 0000000..c45d2c7
--- /dev/null
@@ -0,0 +1,23 @@
+Usage case:
+  * User A installs version 1.0 which contains bug /abc.
+  * Development continues, fixing bug /abc.
+  * User A wants to see which bugs affect their version, and query the
+    main bug repository.
+      $ be --repo http://bugseverywhere.org/bugs list --this-version
+      bea/abc:om: Whatsit not implemented.
+      $ be --repo http://bugseverywhere.org/bugs show bea/abc
+                ID : abc...
+        Short name : bea/abc
+          Severity : minor
+            Status : fixed
+           ...
+      Whatsit not implemented.
+      --------- Comment ---------
+      Name: bea/abc/def
+      From: ...
+      Date: Sat, 23 Jan 2010 14:00 ...
+      
+      Whatsit implemented.
+    "Aha!", says the user, "I need to upgrade to a version of BE
+    that's more recent than 2010/01/23 to get Whatsit functionality."
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/comments/a555d577-7f8c-49f2-96f6-263ce5fdff8e/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/comments/a555d577-7f8c-49f2-96f6-263ce5fdff8e/values
new file mode 100644 (file)
index 0000000..3b8ba33
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 23 Jan 2010 18:59:03 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/values
new file mode 100644 (file)
index 0000000..cb9a372
--- /dev/null
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: open
+
+
+summary: '`be list --this-version` listing bugs affecting your version of BE'
+
+
+time: Sat, 23 Jan 2010 18:49:03 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/17d045d1-3b21-4d3d-8f81-29a5bbc5e6c1/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/17d045d1-3b21-4d3d-8f81-29a5bbc5e6c1/body
new file mode 100644 (file)
index 0000000..6e3f1e7
--- /dev/null
@@ -0,0 +1,14 @@
+> Roundup's great strength is the flexibility of its data model and
+> range of generic support.  It's very easy to extend...
+> ...
+> As far as postponed customization goes, it would be easy enough to
+> duplicate Roundup's schema.py and provide a default schema.py for
+> bugtracking.  This would improve our current system by keeping all the
+> configurable bits under version control from the start (equivalent to
+> setting _versioned_property(require_save=True) for all properties).
+
+How will we handle diffs between with revisions with different
+schema.py?  This re-raises #bea86499-824e-4e77-b085-2d581fa9ccab/ed5eac05-80ed-411d-88a4-d2261b879713/c664b7be-ded5-42dd-a16a-82b2bdb52e36# (#bea86499-824e-4e77-b085-2d581fa9ccab/1100c966-9671-4bc6-8b68-6d408a910da1/bd1207ef-f97e-4078-8c5d-046072012082#), but we
+_expect_ schema.py to evolve, while before we had expected on-disk
+versions to stabilize.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/17d045d1-3b21-4d3d-8f81-29a5bbc5e6c1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/17d045d1-3b21-4d3d-8f81-29a5bbc5e6c1/values
new file mode 100644 (file)
index 0000000..5bc6769
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 03 Jan 2010 16:02:57 +0000
+
+
+In-reply-to: d463e2d9-6dcc-41a4-a6b2-647fb3bddf88
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/d463e2d9-6dcc-41a4-a6b2-647fb3bddf88/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/d463e2d9-6dcc-41a4-a6b2-647fb3bddf88/body
new file mode 100644 (file)
index 0000000..b953536
--- /dev/null
@@ -0,0 +1,67 @@
+The Roundup issue tracker
+  http://roundup.sourceforge.net/
+has been around for a while, and provides a nice, flexible design
+  http://roundup.sourceforge.net/docs/design.html
+What ideas from Roundup are worth incorperating in our setup?
+
+Roundup's great strength is the flexibility of its data model and
+range of generic support.  It's very easy to extend.  However, there
+is only so far you can go with generic support.  Roundup lacks analogs
+to the following Command subclasses (as far as I know):
+  Diff
+    Has per-issue logs, but no repository-wide summary
+  Merge
+  Commit
+    No VCS backends, see http://issues.roundup-tracker.org/issue2550547
+  Import_xml
+  Serve
+    Has HTML server, but no remote command-line access
+Of course, none of these would be particularly hard to add to Roundup,
+with the possible exception of VCS backends, which appears to be
+in-progress anyway.  However, I really like the simplicity of
+  `be init`
+and the ability to postpone repository customization until you need
+it.  So, can we trim down the BE internals to make BE more extensible
+without sacrificing our nice default setup and its tools?  The problem
+is, how to the commands do their thing if they don't know what they're
+working with?
+
+Say, for example, I want to run `be depend bugA bugB`, but my bugs
+don't have blocks or blocked_by link properties.  That could be easily
+handled by having each command would have to keep track of which
+properties it needed and raise appropriate exceptions.
+
+List, Show, Import_xml, etc. would presumably use templates to define
+their output/input formats.
+
+As far as postponed customization goes, it would be easy enough to
+duplicate Roundup's schema.py and provide a default schema.py for
+bugtracking.  This would improve our current system by keeping all the
+configurable bits under version control from the start (equivalent to
+setting _versioned_property(require_save=True) for all properties).
+
+Another part of the difference between BE and Roundup seems to be due
+to the initial backend selection.  Roundup is built on databases,
+which encourages their keyed-Class approach with (property, value)
+pairs of predefined types.  They use Classes for everything, down to
+status values, etc., while we've built those sorts of things into
+_versioned_property()s.
+Benefits of Roundup approach:
+  * easy to configure/alter/retrieve list of allowed values
+  * no need to hard-code properties or resort to extra_strings
+  * assigned values are actually links to centralized definitions
+    - easy updates
+Benefits of BE approach:
+  * single file for all properties
+    - one read and you're done
+    - many file systems don't handle 'lots of tiny files' well
+  * assigned values are actual values, not links to centralized defs.
+    - easy to merge by hand, no need to look up references.
+Since it would be fairly simple to add a merging tool that handled the
+reference lookup transparently, we can move to a Roundup-like Class
+structure by using our current mapfile implementation to store small
+Classes.
+
+Finally, would it be easier to merge these Roundup features into BE,
+or merge the BE features into Roundup...
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/d463e2d9-6dcc-41a4-a6b2-647fb3bddf88/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/d463e2d9-6dcc-41a4-a6b2-647fb3bddf88/values
new file mode 100644 (file)
index 0000000..cb7278c
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 03 Jan 2010 14:16:55 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/values
new file mode 100644 (file)
index 0000000..5feb832
--- /dev/null
@@ -0,0 +1,14 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: open
+
+
+summary: Add Roundup-like flexibility
+
+
+time: Sun, 03 Jan 2010 13:12:38 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/432e994f-3759-42bf-a80d-7cd626c7ce7c/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/432e994f-3759-42bf-a80d-7cd626c7ce7c/body
new file mode 100644 (file)
index 0000000..359c90f
--- /dev/null
@@ -0,0 +1,40 @@
+For example, after merging in a branch with new bugs, the id-cache is
+incomplete.  An example traceback (from `be list`) is
+
+Traceback (most recent call last):
+  File "./be", line 21, in <module>
+    sys.exit(libbe.ui.command_line.main())
+  File ".../be.wtk/libbe/ui/command_line.py", line 327, in main
+    ret = dispatch(ui, command, args)
+  File ".../be.wtk/libbe/ui/command_line.py", line 267, in dispatch
+    ret = ui.run(command, options, args)
+  File ".../be.wtk/libbe/command/base.py", line 504, in run
+    return command.run(options, args)
+  File ".../be.wtk/libbe/command/base.py", line 233, in run
+    self.status = self._run(**params)
+  File ".../be.wtk/libbe/command/list.py", line 168, in _run
+    bugs = self._sort_bugs(bugs, cmp_list)
+  File ".../be.wtk/libbe/command/list.py", line 229, in _sort_bugs
+    bugs.sort(cmp_fn)
+  File ".../be.wtk/libbe/bug.py", line 818, in __call__
+    val = comparison(bug_1, bug_2)
+  File ".../be.wtk/libbe/bug.py", line 798, in cmp_comments
+    comms_1 = sorted(bug_1.comments(), key = lambda comm : comm.uuid)
+  File ".../be.wtk/libbe/bug.py", line 687, in comments
+    for comment in self.comment_root.traverse():
+  File ".../be.wtk/libbe/storage/util/properties.py", line 297, in _fget
+    value = generator(self)
+  File ".../be.wtk/libbe/bug.py", line 225, in _get_comment_root
+    return comment.load_comments(self, load_full=load_full)
+  File ".../be.wtk/libbe/comment.py", line 85, in load_comments
+    bug.id.storage())):
+  File ".../be.wtk/libbe/storage/base.py", line 314, in children
+    return self._children(*args, **kwargs)
+  File ".../be.wtk/libbe/storage/vcs/base.py", line 804, in _children
+    path = self.path(id, revision, relpath=False)
+  File ".../be.wtk/libbe/storage/vcs/base.py", line 705, in path
+    path = self._cached_path_id.path(id)
+  File ".../be.wtk/libbe/storage/vcs/base.py", line 242, in path
+    raise InvalidID(uuid)
+libbe.storage.base.InvalidID: cf56e648-3b09-4131-8847-02dff12b4db2 in revision None
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/432e994f-3759-42bf-a80d-7cd626c7ce7c/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/432e994f-3759-42bf-a80d-7cd626c7ce7c/values
new file mode 100644 (file)
index 0000000..993dce1
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 24 Jan 2010 16:29:46 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/e3d802cf-1fff-4a48-a61c-a07578969333/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/e3d802cf-1fff-4a48-a61c-a07578969333/body
new file mode 100644 (file)
index 0000000..450a208
--- /dev/null
@@ -0,0 +1,5 @@
+Work around by removing id-cache (forcing recreation).
+
+A better solution would be detecting the problem and recreating the
+cache automatically.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/e3d802cf-1fff-4a48-a61c-a07578969333/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/e3d802cf-1fff-4a48-a61c-a07578969333/values
new file mode 100644 (file)
index 0000000..1c44d10
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 25 Jan 2010 00:50:17 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/values
new file mode 100644 (file)
index 0000000..28975af
--- /dev/null
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: be crashes on outdated id-cache
+
+
+time: Sun, 24 Jan 2010 16:28:06 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/comments/7cd2d475-676f-4d60-b431-c7635468e9bd/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/comments/7cd2d475-676f-4d60-b431-c7635468e9bd/body
new file mode 100644 (file)
index 0000000..73ce487
--- /dev/null
@@ -0,0 +1,9 @@
+The comment class could be streamlined and standardized by making it
+subclass (Tree, email.Message).  This should make the per-bug, mini
+mailing list more expressive, and add support for fancy email
+features.  On the other hand, it could make the Comment/xml interface,
+HTML production, etc. more awkward.
+
+Time for another look at Debian's tracker, or do they only allow
+text/plain, single-part messages?
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/comments/7cd2d475-676f-4d60-b431-c7635468e9bd/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/comments/7cd2d475-676f-4d60-b431-c7635468e9bd/values
new file mode 100644 (file)
index 0000000..a774eb2
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 28 Jan 2010 15:41:07 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/values
new file mode 100644 (file)
index 0000000..e6a7689
--- /dev/null
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: wishlist
+
+
+status: open
+
+
+summary: Can comment punt functionality to email.Message?
+
+
+time: Thu, 28 Jan 2010 15:36:16 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/2a51d90a-d47e-4a67-abe7-cce19c1eafad/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/2a51d90a-d47e-4a67-abe7-cce19c1eafad/body
new file mode 100644 (file)
index 0000000..3195bea
--- /dev/null
@@ -0,0 +1,21 @@
+> $ be new 'utf8 string'
+> Traceback (most recent call last):
+>   ...
+> UnicodeDecodeError: 'ascii' codec can't decode byte 0xd0 in position 95: ordinal not in range(128)
+
+(bug reported against cjb@laptop.org-20091006145647-kqkmoh481tl5hvt4)
+
+This was fixed with revision
+  wking@drexel.edu-20091117145118-jltbju9thsn5xvkv
+in my branch on Nov. 17, 2009.
+
+> I think it is more correct to use UTF-8 everywhere or use
+> locale.getdefaultlocale() instead sys.getdefaultencoding().
+
+We try to use unicode strings internally, it's input/output that's
+difficult.  This particular bug turned out to be related to our
+mapfile storage handling.  Take a look at the be.unicode-hg branch
+leading up to revision
+  wking@drexel.edu-20091117145118-jltbju9thsn5xvkv
+for details.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/2a51d90a-d47e-4a67-abe7-cce19c1eafad/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/2a51d90a-d47e-4a67-abe7-cce19c1eafad/values
new file mode 100644 (file)
index 0000000..c79c578
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 19 Mar 2010 11:16:16 +0000
+
+
+In-reply-to: 854eec21-2eeb-4ed4-af35-7a4a2e1f2e98
+
similarity index 92%
rename from .be/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/values
rename to .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/values
index 6e1f957ebd583f5310e37ec71f8121cc969cf51b..5dda50d68684e28f2186d03d7bb19be7f703ae39 100644 (file)
@@ -7,7 +7,7 @@ reporter: Anton Batenev <abbat@abbat>
 severity: minor
 
 
-status: open
+status: fixed
 
 
 summary: UTF-8 problems
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values
new file mode 100644 (file)
index 0000000..e42beab
--- /dev/null
@@ -0,0 +1,20 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+extra_strings:
+- BLOCKED-BY:47c8fd5f-1f5a-4048-bef7-bb4c9a37c411
+- BLOCKED-BY:4fc71206-4285-417f-8a3c-ed6fb31bbbda
+- BLOCKED-BY:f5c06914-dc64-4658-8ec7-32a026a53f55
+
+
+severity: target
+
+
+status: fixed
+
+
+summary: '0.2'
+
+
+time: Sun, 06 Dec 2009 00:37:15 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/27a5a4cc-1782-4509-a3d2-db00c190f97d/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/27a5a4cc-1782-4509-a3d2-db00c190f97d/body
new file mode 100644 (file)
index 0000000..f245ea4
--- /dev/null
@@ -0,0 +1,12 @@
+Added rudimentary authorization with `be serve --auth FILE`.
+
+Special username 'guest' is not allowed to change name,password or
+write to the repository.  All other users in the auth file are allowed
+to do all of that.  A more robust solution would be to have POSIX
+permissions on each storage item, or something.
+
+Note that while the server supports name/password changes for
+non-guest users, there is no command-line interface to this
+functionality.  There is also no automatic way to register
+(i.e. create entries).
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/27a5a4cc-1782-4509-a3d2-db00c190f97d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/27a5a4cc-1782-4509-a3d2-db00c190f97d/values
new file mode 100644 (file)
index 0000000..2169b75
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Wed, 27 Jan 2010 13:05:47 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/76d54016-755b-42ca-ad07-eb9a1c77c33d/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/76d54016-755b-42ca-ad07-eb9a1c77c33d/body
new file mode 100644 (file)
index 0000000..f890566
--- /dev/null
@@ -0,0 +1,2 @@
+Steve's had some related thoughts on authentication for CFBE:
+#bea86499-824e-4e77-b085-2d581fa9ccab/d9959864-ea91-475a-a075-f39aa6760f98/21c90231-d7f2-49bb-97d9-99e16459d799#.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/76d54016-755b-42ca-ad07-eb9a1c77c33d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/76d54016-755b-42ca-ad07-eb9a1c77c33d/values
new file mode 100644 (file)
index 0000000..1566702
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 28 Jan 2010 22:58:08 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/values
new file mode 100644 (file)
index 0000000..364629d
--- /dev/null
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: open
+
+
+summary: '`be serve` authentication / authorization'
+
+
+time: Mon, 25 Jan 2010 21:59:03 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097/body
new file mode 100644 (file)
index 0000000..c5f1b4f
--- /dev/null
@@ -0,0 +1 @@
+Added the option in my be-html branch
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097/values
new file mode 100644 (file)
index 0000000..f7b7498
--- /dev/null
@@ -0,0 +1,8 @@
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 08 Oct 2009 20:16:46 +0000
+
similarity index 92%
rename from .be/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/values
rename to .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/values
index b85936499f666f66ee7081941bbe94a5dfafcb76..27cfc2f756d3a6703e773a9f85fe7065a61636aa 100644 (file)
@@ -7,7 +7,7 @@ reporter: gianluca <gian@galactica>
 severity: wishlist
 
 
-status: open
+status: fixed
 
 
 summary: Add a verbose option to "be html"?
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values
new file mode 100644 (file)
index 0000000..4255708
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 13 Nov 2008 16:35:24 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values
new file mode 100644 (file)
index 0000000..f38cb7f
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 13 Nov 2008 16:38:36 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values
new file mode 100644 (file)
index 0000000..06d6017
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 15 Nov 2008 23:56:51 +0000
+
@@ -24,12 +24,3 @@ for that project.
 I know that's a lot of steps.  I'd like to streamline it quite a bit,  
 but first I wanted to see if you have any feedback on the system  
 itself. Thanks!
-
---
-Steve Losh
-http://stevelosh.com/
-
-_______________________________________________
-Be-devel mailing list
-Be-devel@bugseverywhere.org
-http://void.printf.net/cgi-bin/mailman/listinfo/be-devel
@@ -50,13 +50,3 @@ you switch between branches (effectively switching between revisions)?
 Those are the kind of things that don't really apply when CFBE is just  
 a local interface to a single repository.  If anyone has any advice on  
 how a multi-user interface should work I'd love to hear it!
-
---
-Steve Losh
-http://stevelosh.com/
-
-
-_______________________________________________
-Be-devel mailing list
-Be-devel@bugseverywhere.org
-http://void.printf.net/cgi-bin/mailman/listinfo/be-devel
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/body
new file mode 100644 (file)
index 0000000..ed4f97d
--- /dev/null
@@ -0,0 +1,3 @@
+Speaking of that interface, I changed up the look and feel a bit last  
+weekend.  It's still at http://bitbucket.org/sjl/cherryflavoredbugseverywhere/ 
+  -- if anyone has any feedback (on any aspect of it) I'd appreciate it.
@@ -23,10 +23,3 @@ multiuser?  (Having it handle more than one "user" logged in at once.)
 Great work, thanks!
 
 - Chris.
--- 
-Chris Ball   <cjb@laptop.org>
-
-_______________________________________________
-Be-devel mailing list
-Be-devel@bugseverywhere.org
-http://void.printf.net/cgi-bin/mailman/listinfo/be-devel
@@ -24,10 +24,3 @@ and I'll shut up (or at least move more off-list) ;).
 
 Cheers,
 Trevor
-
--- 
-This email may be signed or encrypted with GPG (http://www.gnupg.org).
-The GPG signature (if present) will be attached as 'signature.asc'.
-For more information, see http://en.wikipedia.org/wiki/Pretty_Good_Privacy
-
-My public key is at http://www.physics.drexel.edu/~wking/pubkey.txt
@@ -60,10 +60,3 @@ to work on with the bug fix, and what branches needed to pull the
 eventual fix.  If the initially reported buggy version wasn't actually
 the root of the bug, oh well :p.  Material for a later related bug
 report or a reopen.
-
--- 
-This email may be signed or encrypted with GPG (http://www.gnupg.org).
-The GPG signature (if present) will be attached as 'signature.asc'.
-For more information, see http://en.wikipedia.org/wiki/Pretty_Good_Privacy
-
-My public key is at http://www.physics.drexel.edu/~wking/pubkey.txt
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values
new file mode 100644 (file)
index 0000000..bb26755
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 17 Nov 2008 15:03:58 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/2cd562f5-fcb9-4cc5-bf8c-ad5c9d960761/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/2cd562f5-fcb9-4cc5-bf8c-ad5c9d960761/body
new file mode 100644 (file)
index 0000000..30286d3
--- /dev/null
@@ -0,0 +1,15 @@
+Before Bugs Everywhere Directory v1.4 we kept
+  "encoding"
+  "vcs_name"
+and other bugdir-wide configuration options in ./be/settings
+
+Now we don't store them anymore, but we should keep some.  For
+example, the encoding setting is useful when running `be html` in a
+cron job.  The settings are repository wide, so they should _still_ go
+in ./be/settings (since there may, eventually, be several bugdirs in a
+repo), but who's job is it to read that file?
+
+The user interface takes care of encoding, but the storage object 
+would be checking for a bug repository and reading the settings file.
+How/when does it notify the UI?
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/2cd562f5-fcb9-4cc5-bf8c-ad5c9d960761/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/2cd562f5-fcb9-4cc5-bf8c-ad5c9d960761/values
new file mode 100644 (file)
index 0000000..a4a84af
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 01 Feb 2010 14:34:10 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/68ec74b9-d2c7-421f-ac70-602b43bbd263/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/68ec74b9-d2c7-421f-ac70-602b43bbd263/body
new file mode 100644 (file)
index 0000000..fafa132
--- /dev/null
@@ -0,0 +1,5 @@
+On the other hand, since encoding decisions seem to be locale-driven,
+so you can just setup the appropriate locale environmental variables
+in your cron job:
+  export LANG=en_US.utf8
+and that should do it...
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/68ec74b9-d2c7-421f-ac70-602b43bbd263/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/68ec74b9-d2c7-421f-ac70-602b43bbd263/values
new file mode 100644 (file)
index 0000000..4bb296a
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 01 Feb 2010 15:35:57 +0000
+
+
+In-reply-to: 2cd562f5-fcb9-4cc5-bf8c-ad5c9d960761
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/values
new file mode 100644 (file)
index 0000000..4d48446
--- /dev/null
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: open
+
+
+summary: Where should the vcs-name and encoding configuration options live?
+
+
+time: Mon, 01 Feb 2010 14:28:13 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9525e3f3-a044-4fa9-b311-56336267b8b5/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9525e3f3-a044-4fa9-b311-56336267b8b5/body
new file mode 100644 (file)
index 0000000..ae7a57f
--- /dev/null
@@ -0,0 +1,21 @@
+> I think a good solution would run along the lines of the currently
+> commented out code in duplicate_bugdir(), where a
+>   VersionedStorage.changed_since(revision)
+> call would give you a list of changed files.  diff could work off of
+> that directly, without the need to generate a whole duplicate bugdir.
+
+This is definately the way to go.  Rough approach for the VCS family:
+
+1) Parse `bzr diff` or such to get a list of new,changed,moved,removed
+   paths.
+2) Convert those paths to ids.
+3) Return a list of ids to duplicate_bugdir().
+4) Provide Storage.parent(id, revision), so duplicate_bugdir() could
+   figure out what type of id we were dealing with (bugdir, bug,
+   comment, other?), and construct the appropriate difference tree.
+
+There could be a DupBugDir class which stored that diff tree and a
+link to the current bugdir, which would make diffs much easier (work
+already done, just copy the diff tree), and provide faster access to
+unchanged files (just use the current version).
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9525e3f3-a044-4fa9-b311-56336267b8b5/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9525e3f3-a044-4fa9-b311-56336267b8b5/values
new file mode 100644 (file)
index 0000000..3dfe992
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 03 Jan 2010 12:25:03 +0000
+
+
+In-reply-to: 9c4b8921-7b43-4bb6-b650-34144b414dc0
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9c4b8921-7b43-4bb6-b650-34144b414dc0/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9c4b8921-7b43-4bb6-b650-34144b414dc0/body
new file mode 100644 (file)
index 0000000..e43a951
--- /dev/null
@@ -0,0 +1,23 @@
+Ok, time to fix the issue I mentioned in this commit message:
+
+revno: 473.1.63
+revision-id: wking@drexel.edu-20091215114420-sbdnvm5jlx0ampbg
+
+...
+duplicate_bugdir() works, but for the vcs backends, it could require
+shelling out for _every_ file read.  This could, and probably will, be
+horribly slow.  Still it works ;).
+    
+I'm not sure what a better implementation would be.  The old
+implementation checked out the entire earlier state into a temporary
+directory
+  pros: single shell out, simple upgrade implementation
+  cons: wouldn't work well for HTTP backens
+    
+I think a good solution would run along the lines of the currently
+commented out code in duplicate_bugdir(), where a
+  VersionedStorage.changed_since(revision)
+call would give you a list of changed files.  diff could work off of
+that directly, without the need to generate a whole duplicate bugdir.
+I'm stuck on how to handle upgrades though...
+...
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9c4b8921-7b43-4bb6-b650-34144b414dc0/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9c4b8921-7b43-4bb6-b650-34144b414dc0/values
new file mode 100644 (file)
index 0000000..a02a32b
--- /dev/null
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 02 Jan 2010 22:58:31 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/c664b7be-ded5-42dd-a16a-82b2bdb52e36/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/c664b7be-ded5-42dd-a16a-82b2bdb52e36/body
new file mode 100644 (file)
index 0000000..b1b9b1a
--- /dev/null
@@ -0,0 +1,7 @@
+> I'm stuck on how to handle upgrades though...
+
+I've satisfied myself with the solution mentioned in #bea86499-824e-4e77-b085-2d581fa9ccab/1100c966-9671-4bc6-8b68-6d408a910da1/bd1207ef-f97e-4078-8c5d-046072012082#,
+namely, upgrading on disk the way we've always done, and not
+supporting on-the-fly upgrading at all.  This isn't important for this
+bug, but I didn't want to just ignore that part of the commit message.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/c664b7be-ded5-42dd-a16a-82b2bdb52e36/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/c664b7be-ded5-42dd-a16a-82b2bdb52e36/values
new file mode 100644 (file)
index 0000000..1d01491
--- /dev/null
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 02 Jan 2010 23:04:01 +0000
+
+
+In-reply-to: 9c4b8921-7b43-4bb6-b650-34144b414dc0
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/values
new file mode 100644 (file)
index 0000000..7b2813d
--- /dev/null
@@ -0,0 +1,14 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Slow and ugly diff implementation
+
+
+time: Sat, 02 Jan 2010 22:56:08 +0000
+
similarity index 60%
rename from .be/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values
rename to .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values
index 01b1768647d00cbeb29a9dfe134642b143dae673..2c8543e41b7fba3d62dda51a24ab5bf26f36eb44 100644 (file)
@@ -1,6 +1,10 @@
 creator: abentley
 
 
+extra_strings:
+- BLOCKS:4fc71206-4285-417f-8a3c-ed6fb31bbbda
+
+
 severity: minor
 
 
@@ -9,6 +13,3 @@ status: closed
 
 summary: Support rcs configuration
 
-
-target: patch-52
-
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values
new file mode 100644 (file)
index 0000000..bb6b222
--- /dev/null
@@ -0,0 +1,15 @@
+creator: Aaron Bentley
+
+
+extra_strings:
+- BLOCKS:47c8fd5f-1f5a-4048-bef7-bb4c9a37c411
+
+
+severity: fatal
+
+
+status: fixed
+
+
+summary: Can't create bugs
+
similarity index 59%
rename from .be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values
rename to .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values
index cf35f07886f71422a55133d12d88b5ad900d698e..3c7b8d15918be086ae7223f4c69c6f175805b15d 100644 (file)
@@ -1,6 +1,10 @@
 creator: abentley
 
 
+extra_strings:
+- BLOCKS:bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c
+
+
 severity: minor
 
 
@@ -9,6 +13,3 @@ status: fixed
 
 summary: Implement bug tree diff
 
-
-target: '0.2'
-
similarity index 90%
rename from .be/settings
rename to .be/bea86499-824e-4e77-b085-2d581fa9ccab/settings
index b3c2b8106bc7ef9467572a178a5f31654ed2c8b4..8ecc0cd0a939219c559b0f1d718834d29d9d9f58 100644 (file)
@@ -1,6 +1,3 @@
-encoding: utf-8
-
-
 extra_strings:
 - "SUBSCRIBE:W. Trevor King <wking@drexel.edu>\tall\t*"
 
@@ -15,6 +12,3 @@ inactive_status:
 - - disabled
   - Unknown meaning.  For backwards compatibility with old BE bugs.
 
-
-vcs_name: bzr
-
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
deleted file mode 100644 (file)
index 777a3f8..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Author: 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
deleted file mode 100644 (file)
index 461a5ab..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Author: 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
deleted file mode 100644 (file)
index e550f5c..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Author: wking
-
-
-Content-type: text/plain
-
-
-Date: Thu, 13 Nov 2008 20:18:02 +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
deleted file mode 100644 (file)
index 31bcacb..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Author: wking
-
-
-Content-type: text/plain
-
-
-Date: Fri, 14 Nov 2008 05:00:43 +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
deleted file mode 100644 (file)
index ab313b9..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Author: wking
-
-
-Content-type: text/plain
-
-
-Date: Thu, 13 Nov 2008 17:27:17 +0000
-
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
deleted file mode 100644 (file)
index e434e1e..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Author: wking
-
-
-Content-type: text/plain
-
-
-Date: Thu, 13 Nov 2008 15:58:18 +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
deleted file mode 100644 (file)
index ae76653..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Author: 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/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values
deleted file mode 100644 (file)
index 8103512..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Author: wking
-
-
-Content-type: text/plain
-
-
-Date: Thu, 13 Nov 2008 16:38:36 +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
deleted file mode 100644 (file)
index 405abc1..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Author: wking
-
-
-Content-type: text/plain
-
-
-Date: Sat, 15 Nov 2008 23:56:51 +0000
-
diff --git a/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/body b/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/body
deleted file mode 100644 (file)
index d2ef28c..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-Those are beautiful templates -- can you share those?  I'd love to
-study the HTML and CSS behind them.
-
-On Sat, Feb 7, 2009 at 5:48 PM, Steve Losh <steve@stevelosh.com> wrote:
-> Hey Chris, thanks for the comments.
->
->>
->> My initial impression is that this looks good enough already to merge as
->> a replacement for the turbogears site.  What does everyone else think?
->>
->
-> I'm not quite sure it's there yet.  There are a bunch of bugs I've got
-> marked as "beta" that I'd like to see fixed before it's ready for real use.
->  Hopefully they shouldn't be too tough to fix.  You can point CFBE at itself
-> to see them.  :)
->
->> Could you explain a little about how you handle authorship of bug
->> changes at the moment, and if it looks plausible to try making it
->> multiuser?  (Having it handle more than one "user" logged in at once.)
->>
->
-> That's something I need advice on.  Right now CFBE is pretty much only
-> suitable for local use - you check out whatever you're working on and use it
-> as a local interface to the bugs in the repository.  Change those, check in,
-> etc.  It's effectively just a pretty version of the command line be tool.
->
-> I haven't used CherryPy's session/authentication support before.  This might
-> be a good time for me to learn.  One way it might be able to handle multiple
-> users hitting a central server:
->
-> * Each user has to register with the server and be approved by an admin.
-> * Each account would be mapped to a contributor string, the same one that
-> would show up if you were going to commit to the repository.
-> * Once you have an account, you'd login to make any changes.
->
->
-> Aside from all that, I'm a little fuzzy on how a centralized interface to a
-> distributed bug tracking system should work.  A read-only interface to a
-> central "main" repository would be easy.  Run the server in read-only mode
-> pointing at the main repository.  People can use it to look at the bugs in
-> the tip of that repository.
->
-> If it's not read-only, what happens when a user changes/adds/whatevers a
-> bug?  Should CFBE commit that change to the repository right then and there?
->  Should it never commit, just update the bugdir and let the commits happen
-> manually?
->
-> What happens when you have multiple branches for a repository?  Should there
-> be one CFBE instance for each branch, or a single one that lets you switch
-> between branches (effectively switching between revisions)?
->
-> Those are the kind of things that don't really apply when CFBE is just a
-> local interface to a single repository.  If anyone has any advice on how a
-> multi-user interface should work I'd love to hear it!
->
-> --
-> Steve Losh
-> http://stevelosh.com/
->
->
-> _______________________________________________
-> Be-devel mailing list
-> Be-devel@bugseverywhere.org
-> http://void.printf.net/cgi-bin/mailman/listinfo/be-devel
->
-
-
-
--- 
-Matthew Wilson
-matt@tplus1.com
-http://tplus1.com
-
-_______________________________________________
-Be-devel mailing list
-Be-devel@bugseverywhere.org
-http://void.printf.net/cgi-bin/mailman/listinfo/be-devel
diff --git a/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/values b/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/values
deleted file mode 100644 (file)
index ca3efd0..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-Alt-id: <f6f643a20902071531y6aa3d7a6k7c5a4bd4aa5a04f6@mail.gmail.com>
-
-
-Author: Matthew Wilson <matt@tplus1.com>
-
-
-Content-type: text/plain
-
-
-Date: Sat, 07 Feb 2009 18:31:04 -0500
-
-
-In-reply-to: 21c90231-d7f2-49bb-97d9-99e16459d799
-
diff --git a/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/body b/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/body
deleted file mode 100644 (file)
index e160b76..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-Speaking of that interface, I changed up the look and feel a bit last  
-weekend.  It's still at http://bitbucket.org/sjl/cherryflavoredbugseverywhere/ 
-  -- if anyone has any feedback (on any aspect of it) I'd appreciate it.
-
---
-Steve
-
-On Jul 3, 2009, at 8:31 PM, Chris Ball wrote:
-
-> Hi Gianluca,
->
->> As i said in a previous mail, I am working on a "html" command
->> for be.  The goal is to be able to do something like "be html
->> /web/page" to have in the /web/page directory some static html
->> pages that basically are the dump of the be repository, much like
->> ditz have. This will enable a simple and fast publish of the bus
->> list and details on the web, at least in read only mode.
->
-> It might be a good idea for "be html" to use the CherryPy web  
-> interface
-> that Steve is working on.  The command could start up the CherryPy app
-> and scrape all of the available pages to get a stand-alone dump; this
-> would avoid having to keep two (okay, more than two at this point)
-> separate sets of HTML templates in the source tree.  What do you  
-> think?
->
->> 2) I see that every command is implemented with a python file in
->> the becommand dir. For a better code, I'd like to split the
->> command implementation into two files: a file that contain the
->> actual code and a second file that have the html related part,
->> any problem with this ? I don't like to have the html part and
->> the code part in one big and unreadable file.
->
-> I agree that becommands/*.py commands should not contain any HTML
-> layout code.  Putting it somewhere else instead sounds fine.
->
-> Thanks!
->
-> - Chris.
-> -- 
-> Chris Ball   <cjb@laptop.org>
->
-> _______________________________________________
-> Be-devel mailing list
-> Be-devel@bugseverywhere.org
-> http://void.printf.net/cgi-bin/mailman/listinfo/be-devel
-
-
-_______________________________________________
-Be-devel mailing list
-Be-devel@bugseverywhere.org
-http://void.printf.net/cgi-bin/mailman/listinfo/be-devel
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
deleted file mode 100644 (file)
index 8496f0a..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Author: wking
-
-
-Content-type: text/plain
-
-
-Date: Mon, 17 Nov 2008 15:03:58 +0000
-
diff --git a/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values b/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values
deleted file mode 100644 (file)
index 4e5613f..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-severity: fatal
-
-
-status: fixed
-
-
-summary: Can't create bugs
-
-
-target: '0.1'
-
index 7bd05c2187707950297403aa026dbec42926b878..e7aade4834f72c49755d30e93207d7a7da544957 100644 (file)
@@ -1 +1 @@
-Bugs Everywhere Directory v1.2
+Bugs Everywhere Directory v1.4
diff --git a/AUTHORS b/AUTHORS
index 6b66315856530cf987538c8f926df65722f8c53d..5445afc8fff41244aa3291fc090051f5094859e6 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,7 +1,7 @@
 Bugs Everywhere was written by:
 Aaron Bentley
-Alexander Belchenko
 Alex Miller
+Alexander Belchenko
 Ben Finney
 Chris Ball
 Gianluca Montecchi
index 7ef4e453f224918002ab19ca78b0842d3a27835d..e9e1748d76531ac029849fece555d75bed3c5fef 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -4,8 +4,9 @@
 # Makefile
 # Part of Bugs Everywhere, a distributed bug tracking system.
 #
-# Copyright (C) 2008-2009 Ben Finney <benf@cybersource.com.au>
+# Copyright (C) 2008-2010 Ben Finney <benf@cybersource.com.au>
 #                         Chris Ball <cjb@laptop.org>
+#                         Gianluca Montecchi <gian@grys.it>
 #                         W. Trevor King <wking@drexel.edu>
 #
 # This program is free software; you can redistribute it and/or modify
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
 SHELL = /bin/bash
-PATH = /usr/bin:/bin
+RM = rm
+#PATH = /usr/bin:/bin  # must include sphinx-build for 'sphinx' target.
+
+#PREFIX = /usr/local
+PREFIX = ${HOME}
+INSTALL_OPTIONS = "--prefix=${PREFIX}"
 
 # Directories with semantic meaning
 DOC_DIR := doc
+MAN_DIR := ${DOC_DIR}/man
 
-# Variables that will be extended by module include files
-GENERATED_FILES := libbe/_version.py build
-CODE_MODULES :=
-CODE_PROGRAMS :=
-
-# List of modules (directories) that comprise our 'make' project
-MODULES += ${DOC_DIR}
+MANPAGES = be.1
+GENERATED_FILES := build libbe/_version.py
 
-RM = rm
-
-PREFIX = /usr
-#PREFIX = ${HOME}
-INSTALL_OPTIONS = "--prefix=${PREFIX}"
+MANPAGE_FILES = $(patsubst %,${MAN_DIR}/%,${MANPAGES})
+GENERATED_FILES += ${MANPAGE_FILES}
 
 \f
 .PHONY: all
 all: build
 
-# Include the make data for each module
-include $(patsubst %,%/module.mk,${MODULES})
-
 \f
 .PHONY: build
 build: libbe/_version.py
        python setup.py build
 
+.PHONY: doc
+doc: sphinx man
+
 .PHONY: install
-install: doc build
+install: build doc
        python setup.py install ${INSTALL_OPTIONS}
-#cp -v interfaces/xml/* ${PREFIX}/bin
-#cp -v interfaces/email/catmutt ${PREFIX}/bin
 
-\f
+test: build
+       python test.py
+
 .PHONY: clean
 clean:
        $(RM) -rf ${GENERATED_FILES}
+       $(MAKE) -C ${DOC_DIR} clean
 
+\f
 .PHONY: libbe/_version.py
 libbe/_version.py:
        bzr version-info --format python > $@
+
+.PHONY: man
+man: ${MANPAGE_FILES}
+
+%.1: %.1.sgml
+       docbook-to-man $< > $@
+
+.PHONY: sphinx
+sphinx:
+       $(MAKE) -C ${DOC_DIR} html
diff --git a/NEWS b/NEWS
index dec3787f71b8831e41b224b896e390b56750922d..2defeb0c421b811475746840722a406d4eb4d2cc 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,125 @@
+February 20, 2010
+ * `be html` uses truncated IDs in comment and bug URLs and anchors.
+
+January 27, 2010
+ * `be html` links (<a href="...) #-delimited references in text/*
+   comment bodies.
+
+January 25, 2010
+ * Added --ssl to `be serve` using cherrypy.wsgiserver.
+
+January 23, 2010
+ * Added 'Created comment with ID .../.../...' output to `be comment`.
+ * Added --important and --mine to `be list`.
+
+January 20, 2010
+ * Renamed 'be-mbox-to-xml' -> 'be-mail-to-xml' and added support for
+   several mailbox formats.
+
+January 3, 2010
+ * Changed `be list --uuids` -> `be list --ids`
+   Instead of UUIDs, it now outputs user ids: BUGDIR/BUG
+
+January 1, 2010
+ * Added HTTP storage backend and server
+   Serve a local repo on http://localhost:8000
+     be --repo REPO serve
+   Then connect from other be calls, for example
+     be --repo http://localhost:8000 list
+
+December 31, 2009
+ * New bugdir/bug/comment ID format replaces old bug:comment format.
+ * Deprecated support for `be diff` on Arch and Darcs <= 2.3.1.  A new
+   backend abstraction (Storage) makes the former implementation
+   ungainly.
+ * Improved command completion.
+ * Removed commands close, open, email_bugs, 
+ * Flipped some arguments
+   `be assign BUG-ID [ASSIGNEE]` -> `be status ASSIGNED BUG-ID ...`
+   `be severity BUG-ID SEVERITY` -> `be severity SEVERITY BUG-ID ...`
+   `be status BUG-ID STATUS` -> `be status STATUS BUG-ID ...`
+
+December 7, 2009
+ * added --paginate and --no-pager to be.
+ * be --dir DIR COMMAND now roots the bugdir in DIR _without_ changing
+   directories.
+ * `be init --root DIR` should now be `be --dir DIR init`.
+
+December 5, 2009
+ * targets are now a special type of bug (severity 'target'), so you
+   can do all the things you do with normal bugs to them as well
+   (e.g. comment on them, link them into dependency trees, etc.)
+ * new command `be due` to get/set bug due dates.
+ * changes to `be diff`
+   * exits with an error if required revision control is not possible.
+     Previously it printed a message, but exitted with status 0.
+   * removed options --new, --removed, --modified, --all
+   * added options --uuids, --subscribe
+   Replace:
+     '--new' with '--uuids --subscribe DIR:new'
+     '--removed' with '--uuids --subscribe DIR:rem'
+     '--modified' with '--uuids --subscribe DIR:mod'
+     '--all' with '--uuids'
+ * changes to `be depend`
+   * added options --status, --severity
+ * changes to `be list`
+   * added blacklist capability to --status, --severity, --assigned
+   * removed options --target, --cur-target
+   Replace:
+     'be list --target TARGET' with
+     'be depend --status -closed,fixed,wontfix --severity -target \
+        $(be target --resolve TARGET)'
+     'be list --cur-target' with
+     'be depend --status -closed,fixed,wontfix --severity -target \
+        $(be target --resolve)'
+ * changes to `be target`
+   * added option --resolve
+   * removed option --list
+   Replace:
+     'be target --list' with 'be list --status all --severity target'
+ * assorted cleanups and bugfixes
+
+December 4, 2009
+ * new commands:
+   email-bugs
+ * broke `be comment --xml` out and extended into `be import-xml`
+ * added --dir option to `be diff'
+ * new XML format <be-xml>
+ * interfaces/email/interactive:
+   * added support for [be-bug:xml] interface
+   * improved security with restrict_file_access
+ * assorted cleanups, bugfixes, and optimizations
+
+November 17, 2009
+ * new becommands:
+   commit
+   depend
+   html
+   merge
+   remove
+   status
+   subscribe
+   tag
+ * renamed becommands:
+   set_root => init
+ * removed becommands:
+   inprogress
+   upgrade
+ * new interfaces:
+   email:
+     interactive
+     catmutt
+   xml:
+     be-mbox-to-xml
+     be-xml-to-mbox
+ * deprecated interfaces:
+   gui:
+     beg
+     wxbe
+   web:
+     Bugs-Everywhere-Web
+ * lots of bugfixes and cleanups, see `be diff 200` for details.
+
 April 10, 2006
  * Updated BeWeb to TurboGear 0.9
 
diff --git a/README b/README
index b43c15ce62d7fb39b5d2ac8a463b86c327324c47..ef597bb39c771d1960baa9408158b6f73a15e134 100644 (file)
--- a/README
+++ b/README
@@ -1,39 +1,71 @@
 Bugs Everywhere
 ===============
-This is Bugs Everywhere, a bugtracker built on distributed revision
+
+This is Bugs Everywhere (BE), a bugtracker built on distributed version
 control.  It works with Arch, Bazaar, Darcs, Git, and Mercurial at the
-moment, but is easily extensible.  It can also function with no RCS at
+moment, but is easily extensible.  It can also function with no VCS at
 all.
 
 The idea is to package the bug information with the source code, so that
-bugs can be marked 'fixed' in the branches that fix them.  So, instead of
+bugs can be marked "fixed" in the branches that fix them.  So, instead of
 numbers, bugs have globally unique ids.
 
 
+Getting BE
+==========
+
+BE is available as a bzr repository::
+
+    $ bzr branch http://bzr.bugseverywhere.org/be
+
+See the homepage_ for details.  If you do branch the bzr repo, you'll
+need to run::
+
+    $ make
+
+to build some auto-generated files (e.g. ``libbe/_version.py``), and::
+
+    $ make install
+
+to install BE.  By default BE will install into your home directory,
+but you can tweak the ``PREFIX`` variable in ``Makefile`` to install
+to another location.
+
+.. _homepage: http://bugseverywhere.org/
+
+
 Getting started
 ===============
+
 To get started, you must set the bugtracker root.  Typically, you will want to
 set the bug root to your project root, so that Bugs Everywhere works in any
-part of your project tree.
-$ be init $PROJECT_ROOT
+part of your project tree.::
 
-To create bugs, use "be new $DESCRIPTION".  To comment on bugs, you
-can can use "be comment $BUG_ID".  To close a bug, use "be close
-$BUG_ID" or "be status $BUG_ID fixed".  For more commands, see "be
-help".  You can also look at the usage examples in test_usage.sh.
+    $ be init -r $PROJECT_ROOT
 
+To create bugs, use ``be new $DESCRIPTION``.  To comment on bugs, you
+can can use ``be comment $BUG_ID``.  To close a bug, use
+``be close $BUG_ID`` or ``be status $BUG_ID fixed``.  For more
+commands, see ``be help``.  You can also look at the usage examples in
+``test_usage.sh``.
 
-Using BeWeb, the web UI
-=======================
-BeWeb uses the Turbogears framework: http://www.turbogears.org/
-Please ensure you have Turbogears 0.8a5 or a compatible release installed.
-Because it uses BE data, the web UI does not require a database.
 
-To use BeWeb, first create a configuration file, telling it which projects
-to track, and what to call them.  An example configuration file 
-(beweb/beweb/config.py.example) is provided.
+Documentation
+=============
 
-Next, cd to beweb, and run ./beweb-start.py
+If ``be help`` isn't scratching your itch, the full documentation is
+available in the doc directory as reStructuredText_ .  You can build
+the full documentation with Sphinx_ , convert single files with
+docutils_ , or browse through the doc directory by hand.
+doc/index.txt is a good place to start.  If you do use Sphinx, you'll
+need to install numpydoc_ for automatically generating API
+documentation.  See the ``NumPy/SciPy documentation guide``_ for an
+introduction to the syntax.
 
-BeWeb allows you to create, view and edit bugs, but it is in an early stage of
-development, so some features are missing.
+.. _reStructuredText:
+  http://docutils.sourceforge.net/docs/user/rst/quickref.html
+.. _Sphinx: http://sphinx.pocoo.org/
+.. _docutils: http://docutils.sourceforge.net/
+.. _numpydoc: http://pypi.python.org/pypi/numpydoc
+.. _NumPy/SciPy documentation guide:
+  http://projects.scipy.org/numpy/wiki/CodingStyleGuidelines
diff --git a/README.dev b/README.dev
deleted file mode 100644 (file)
index ddc3a88..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-Extending BE
-============
-
-To write a plugin, you simply create a new file in the becommands
-directory.  Take a look at one of the simpler plugins (e.g. open.py)
-for an example of how that looks, and to start getting a feel for the
-libbe interface.
-
-To fit into the current framework, your extension module should
-provide the following elements:
-  __desc__
-    A short string describing the purpose of your plugin
-  execute(args)
-    The entry function for your plugin.  args is everything from
-    sys.argv after the name of your plugin (e.g. for the command
-    `be open abc', args=['abc']).
-
-    Note: be supports command-completion.  To avoid raising errors you
-    need to deal with possible '--complete' options and arguments.
-    See the 'Command completion' section below for more information.
-  help()
-     Return the string to be output by `be help <yourplugin>',
-     `be <yourplugin> --help', etc.
-
-While that's all that's strictly necessary, many plugins (all the
-current ones) use libbe.cmdutil.CmdOptionParser to provide a
-consistent interface
-  get_parser()
-    Return an instance of CmdOptionParser("<usage string>").  You can
-    alter the parser (e.g. add some more options) before returning it.
-
-Again, you can just browse around in becommands to get a feel for things.
-
-
-Testing
--------
-
-Run any doctests in your plugin with
-  be$ python test.py <yourplugin>
-for example
-  be$ python test.py merge
-
-
-Command completion
-------------------
-
-BE implements a general framework to make it easy to support command
-completion for arbitrary plugins.  In order to support this system,
-all becommands should properly handle the '--complete' commandline
-argument, returning a list of possible completions.  For example
-  $ be --commands
-      lists options accepted by be and the names of all available becommands.
-  $ be list --commands
-      lists options accepted by becommand/list
-  $ be list --status --commands
-      lists arguments accepted by the becommand/list --status option
-  $ be show -- --commands
-      lists possible vals for the first positional argument of becommand/show
-This is a lot of information, but command-line completion is really
-convenient for the user.  See becommand/list.py and becommand/show.py
-for example implementations.  The basic idea is to raise
-  cmdutil.GetCompletions(['list','of','possible','completions'])
-once you've determined what that list should be.
-
-However, command completion is not critical.  The first priority is to
-implement the target functionality, with fancy shell sugar coming
-later.  In recognition of this, cmdutil provides the default_complete
-function which ensures that if '--complete' is any one of the
-arguments, options, or option-arguments, GetCompletions will be raised
-with and empty list.
-
-Profiling
-=========
-
-Find out which 20 calls take the most cumulative time (time of
-execution + childrens' times).
-
-  $ python -m cProfile -o profile be [command] [args]
-  $ python -c "import pstats; p=pstats.Stats('profile'); p.sort_stats('cumulative').print_stats(20)"
diff --git a/be b/be
index feacfb4e831d799c457400ca4abeb0c7d45e43d8..8c7f41c93e3827e8d411bab4e82b667f848fd7fb 100755 (executable)
--- a/be
+++ b/be
@@ -1,8 +1,5 @@
 #!/usr/bin/env python
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Chris Ball <cjb@laptop.org>
-#                         Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
-#                         W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
 #
 # 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
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-import os
 import sys
+import libbe.ui.command_line
 
-from libbe import cmdutil, version
-
-__doc__ = cmdutil.help()
-
-usage = "be [options] [command] [command_options ...] [command_args ...]"
-
-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.")
-
-try:
-    options,args = parser.parse_args()
-    for option,value in cmdutil.option_value_pairs(options, parser):
-        if value == "--complete":
-            if option == "dir":
-                if len(args) == 0:
-                    args = ["."]
-                paths = cmdutil.complete_path(args[0])
-                raise cmdutil.GetCompletions(paths)
-except cmdutil.GetHelp:
-    print cmdutil.help(parser=parser)
-    sys.exit(0)
-except cmdutil.GetCompletions, e:
-    print '\n'.join(e.completions)
-    sys.exit(0)
-
-if options.version == True or options.verbose_version == True:
-    print version.version(verbose=options.verbose_version)
-    sys.exit(0)
-if options.dir != None:
-    os.chdir(options.dir)
-
-try:
-    if len(args) == 0:
-        raise cmdutil.UsageError, "must supply a command"
-    sys.exit(cmdutil.execute(args[0], args[1:]))
-except cmdutil.GetHelp:
-    print cmdutil.help(sys.argv[1])
-    sys.exit(0)
-except cmdutil.GetCompletions, e:
-    print '\n'.join(e.completions)
-    sys.exit(0)
-except cmdutil.UnknownCommand, e:
-    print e
-    sys.exit(1)
-except cmdutil.UsageError, e:
-    print "Invalid usage:", e
-    if len(args) == 0:
-        print cmdutil.help(parser=parser)
-    else:
-        print "\nArgs:", args
-        print cmdutil.help(sys.argv[1])
-    sys.exit(1)
-except cmdutil.UserError, e:
-    print "ERROR:"
-    print e
-    sys.exit(1)
+sys.exit(libbe.ui.command_line.main())
diff --git a/becommands/assign.py b/becommands/assign.py
deleted file mode 100644 (file)
index 794f028..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Marien Zwart <marienz@gentoo.org>
-#                         Thomas Gerigk <tgerigk@gmx.de>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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/becommands/comment.py b/becommands/comment.py
deleted file mode 100644 (file)
index 9a614b2..0000000
+++ /dev/null
@@ -1,228 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Chris Ball <cjb@laptop.org>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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
-    <BLANKLINE>
-    >>> 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
-    <BLANKLINE>
-    >>> 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 <bug> or <comment-list>")
-        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 <bug> or <comment-list> with one or more <comment> children.  The syntax for the <comment> 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 <uuid> field, but no <alt-id> field, your <uuid> will be used as the comment's <alt-id>.  An exception is raised if <alt-id> 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 <in-reply-to> 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/becommands/commit.py b/becommands/commit.py
deleted file mode 100644 (file)
index dc70e7e..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
-#
-# 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/becommands/depend.py b/becommands/depend.py
deleted file mode 100644 (file)
index f72b8ba..0000000
+++ /dev/null
@@ -1,339 +0,0 @@
-# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
-#
-# 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:<your-bug-uuid>
-
-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
deleted file mode 100644 (file)
index b6ac5b0..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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/becommands/help.py b/becommands/help.py
deleted file mode 100644 (file)
index a8f346a..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright (C) 2006-2009 Aaron Bentley and Panometrics, Inc.
-#                         Thomas Gerigk <tgerigk@gmx.de>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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]
-    <BLANKLINE>
-    Options:
-      -h, --help  Print a help message
-      --complete  Print a list of available completions
-    <BLANKLINE>
-    Print help for specified command or list of all commands.
-    <BLANKLINE>
-    """
-    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/becommands/html.py b/becommands/html.py
deleted file mode 100644 (file)
index 908c714..0000000
+++ /dev/null
@@ -1,588 +0,0 @@
-# Copyright (C) 2009 Gianluca Montecchi <gian@grys.it>
-#                    W. Trevor King <wking@drexel.edu>
-#
-# 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 = """
-        <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-        <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-        <head>
-        <title>BugsEverywhere Issue Tracker</title>
-        <meta http-equiv="Content-Type" content="text/html; charset=%s" />
-        <link rel="stylesheet" href="style.css" type="text/css" />
-        </head>
-        <body>
-        
-        
-        <div class="main">
-        <h1>BugsEverywhere Bug List</h1>
-        <p></p>
-        <table>
-        
-        <tr>
-        <td class="%%s"><a href="index.html">Active Bugs</a></td>
-        <td class="%%s"><a href="index_inactive.html">Inactive Bugs</a></td>
-        </tr>
-        
-        </table>
-        <table class="table_bug">
-        <tbody>
-        """ % self.bd.encoding
-        
-        self.bug_line ="""
-        <tr class="%s-row">
-        <td ><a href="bugs/%s.html">%s</a></td>
-        <td ><a href="bugs/%s.html">%s</a></td>
-        <td><a href="bugs/%s.html">%s</a></td>
-        <td><a href="bugs/%s.html">%s</a></td>
-        <td><a href="bugs/%s.html">%s</a></td>
-        </tr>
-        """
-        
-        self.detail_first = """
-        <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-        <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-        <head>
-        <title>BugsEverywhere Issue Tracker</title>
-        <meta http-equiv="Content-Type" content="text/html; charset=%s" />
-        <link rel="stylesheet" href="../style.css" type="text/css" />
-        </head>
-        <body>
-        
-        
-        <div class="main">
-        <h1>BugsEverywhere Bug List</h1>
-        <h5><a href="%%s">Back to Index</a></h5>
-        <h2>Bug: _bug_id_</h2>
-        <table >
-        <tbody>
-        """ % self.bd.encoding
-        
-        
-        
-        self.detail_line ="""
-        <tr>
-        <td align="right">%s</td><td>%s</td>
-        </tr>
-        """
-        
-        self.index_last = """
-        </tbody>
-        </table>
-        
-        </div>
-        
-        <div class="footer">Generated by <a href="http://www.bugseverywhere.org/">BugsEverywhere</a> on %s</div>
-        
-        </body>
-        </html>
-        """
-        
-        self.comment_section = """
-        """
-        
-        self.begin_comment_section ="""
-        <tr>
-        <td align="right">Comments:
-        </td>
-        <td>
-        """
-        
-        
-        self.end_comment_section ="""
-        </td>
-        </tr>
-        """
-        
-        self.detail_last = """
-        </tbody>
-        </table>
-        </div>
-        <h5><a href="%s">Back to Index</a></h5>
-        <div class="footer">Generated by <a href="http://www.bugseverywhere.org/">BugsEverywhere</a>.</div>
-        </body>
-        </html>
-        """   
-        
-        
-    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("<tr><td colspan=\"2\"><hr /></td></tr>")
-        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("</div>\n") # close non-parent <div class="comment...
-            assert len(stack) == depth
-            stack.append(comment)
-            lines = ["--------- Comment ---------",
-                     "Name: %s" % comment.uuid,
-                     "From: %s" % escape(comment.author),
-                     "Date: %s" % escape(comment.date),
-                     ""]
-            lines.extend(escape(comment.body).splitlines())
-            if depth == 0:
-                FD.write('<div class="commentF">')
-            else:
-                FD.write('<div class="comment">')
-            FD.write("<br />\n".join(lines)+"<br />\n")
-        while len(stack) > 0:
-            stack.pop(-1)
-            FD.write("</div>\n") # close every remaining <div class="comment...
-        FD.write(self.end_comment_section)
-        if fileid == "active":
-            FD.write(self.detail_last%"../index.html")
-        if fileid == "inactive":
-            FD.write(self.detail_last%"../index_inactive.html")
-        FD.close()
-        
-   
diff --git a/becommands/init.py b/becommands/init.py
deleted file mode 100644 (file)
index a6098ba..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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.
-    >>> dir.cleanup()
-
-    >>> 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/becommands/list.py b/becommands/list.py
deleted file mode 100644 (file)
index 12e1e29..0000000
+++ /dev/null
@@ -1,248 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Chris Ball <cjb@laptop.org>
-#                         Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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 '<?xml version="1.0" encoding="%s" ?>' % bd.encoding
-            print "<bugs>"
-        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 "</bugs>"
-
-    # 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/becommands/merge.py b/becommands/merge.py
deleted file mode 100644 (file)
index f212b01..0000000
+++ /dev/null
@@ -1,165 +0,0 @@
-# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
-#
-# 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 <jdoe@example.com>
-         Created : ...
-    Bug A
-    --------- Comment ---------
-    Name: a:1
-    From: ...
-    Date: ...
-    <BLANKLINE>
-    Testing
-      --------- Comment ---------
-      Name: a:2
-      From: ...
-      Date: ...
-    <BLANKLINE>
-      Testing...
-    --------- Comment ---------
-    Name: a:3
-    From: ...
-    Date: ...
-    <BLANKLINE>
-    Merged from bug b
-      --------- Comment ---------
-      Name: a:4
-      From: ...
-      Date: ...
-    <BLANKLINE>
-      1 2
-        --------- Comment ---------
-        Name: a:5
-        From: ...
-        Date: ...
-    <BLANKLINE>
-        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 <jdoe@example.com>
-         Created : ...
-    Bug B
-    --------- Comment ---------
-    Name: b:1
-    From: ...
-    Date: ...
-    <BLANKLINE>
-    1 2
-      --------- Comment ---------
-      Name: b:2
-      From: ...
-      Date: ...
-    <BLANKLINE>
-      1 2 3 4
-    --------- Comment ---------
-    Name: b:3
-    From: ...
-    Date: ...
-    <BLANKLINE>
-    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/becommands/new.py b/becommands/new.py
deleted file mode 100644 (file)
index a8ee2ec..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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/becommands/remove.py b/becommands/remove.py
deleted file mode 100644 (file)
index 8d85033..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
-#
-# 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/becommands/set.py b/becommands/set.py
deleted file mode 100644 (file)
index f7e68d3..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Chris Ball <cjb@laptop.org>
-#                         Marien Zwart <marienz@gentoo.org>
-#                         Thomas Gerigk <tgerigk@gmx.de>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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/becommands/severity.py b/becommands/severity.py
deleted file mode 100644 (file)
index 660586e..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Marien Zwart <marienz@gentoo.org>
-#                         Thomas Gerigk <tgerigk@gmx.de>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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/becommands/show.py b/becommands/show.py
deleted file mode 100644 (file)
index 50bd6eb..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Chris Ball <cjb@laptop.org>
-#                         Thomas Gerigk <tgerigk@gmx.de>
-#                         Thomas Habets <thomas@habets.pp.se>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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 <jdoe@example.com>
-         Created : ...
-    Bug A
-    <BLANKLINE>
-    >>> execute (["--xml", "a"], manipulate_encodings=False) # doctest: +ELLIPSIS
-    <?xml version="1.0" encoding="..." ?>
-    <bug>
-      <uuid>a</uuid>
-      <short-name>a</short-name>
-      <severity>minor</severity>
-      <status>open</status>
-      <creator>John Doe &lt;jdoe@example.com&gt;</creator>
-      <created>...</created>
-      <summary>Bug A</summary>
-    </bug>
-    >>> 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 '<?xml version="1.0" encoding="%s" ?>' % 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/becommands/status.py b/becommands/status.py
deleted file mode 100644 (file)
index f315003..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
-#
-# 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/becommands/subscribe.py b/becommands/subscribe.py
deleted file mode 100644 (file)
index 0a23057..0000000
+++ /dev/null
@@ -1,390 +0,0 @@
-# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
-#
-# 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 "<SubscriptionType: %s>" % 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 <j@doe.com>", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
-    Subscriptions for a:
-    John Doe <j@doe.com>    all    *
-    >>> bd._clear_bugs() # resync our copy of bug
-    >>> a = bd.bug_from_shortname("a")
-    >>> print a.extra_strings
-    ['SUBSCRIBE:John Doe <j@doe.com>\\tall\\t*']
-    >>> execute(["-s","Jane Doe <J@doe.com>", "-S", "a.com,b.net", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
-    Subscriptions for a:
-    Jane Doe <J@doe.com>    all    a.com,b.net
-    John Doe <j@doe.com>    all    *
-    >>> execute(["-s","Jane Doe <J@doe.com>", "-S", "a.edu", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
-    Subscriptions for a:
-    Jane Doe <J@doe.com>    all    a.com,a.edu,b.net
-    John Doe <j@doe.com>    all    *
-    >>> execute(["-u", "-s","Jane Doe <J@doe.com>", "-S", "a.com", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
-    Subscriptions for a:
-    Jane Doe <J@doe.com>    all    a.edu,b.net
-    John Doe <j@doe.com>    all    *
-    >>> execute(["-s","Jane Doe <J@doe.com>", "-S", "*", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
-    Subscriptions for a:
-    Jane Doe <J@doe.com>    all    *
-    John Doe <j@doe.com>    all    *
-    >>> execute(["-u", "-s","Jane Doe <J@doe.com>", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
-    Subscriptions for a:
-    John Doe <j@doe.com>    all    *
-    >>> execute(["-u", "-s","John Doe <j@doe.com>", "a"], manipulate_encodings=False)
-    >>> execute(["-s","Jane Doe <J@doe.com>", "-t", "new", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
-    Subscriptions for bug directory:
-    Jane Doe <J@doe.com>    new    *
-    >>> execute(["-s","Jane Doe <J@doe.com>", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
-    Subscriptions for bug directory:
-    Jane Doe <J@doe.com>    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 <j@doe.com>", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL)
-    >>> es = subscribe(es, "Jane Doe <J@doe.com>", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL)
-    >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL)
-    ['John Doe <j@doe.com>']
-    >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL, match_descendant_types=True)
-    ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
-    >>> sgs(es, BUGDIR_TYPE_ALL, "b.net", BUGDIR_TYPE_ALL, match_descendant_types=True)
-    ['Jane Doe <J@doe.com>']
-    >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL)
-    ['Jane Doe <J@doe.com>']
-    >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL, match_ancestor_types=True)
-    ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
-    """
-    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 <j@doe.com>", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL)
-    >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe <J@doe.com>", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL)
-    >>> a.extra_strings = subscribe(a.extra_strings, "John Doe <j@doe.com>", [BUG_TYPE_ALL], ["a.com"], BUG_TYPE_ALL)
-    >>> subscribers = get_bugdir_subscribers(bd, "a.com")
-    >>> subscribers["Jane Doe <J@doe.com>"]["DIR"]
-    [<SubscriptionType: new>]
-    >>> subscribers["John Doe <j@doe.com>"]["DIR"]
-    [<SubscriptionType: all>]
-    >>> subscribers["John Doe <j@doe.com>"]["a"]
-    [<SubscriptionType: all>]
-    >>> get_bugdir_subscribers(bd, "b.net")
-    {'Jane Doe <J@doe.com>': {'DIR': [<SubscriptionType: new>]}}
-    >>> 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
deleted file mode 100644 (file)
index ecd853f..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
-#
-# 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:<your-tag>
-"""
-
-def help():
-    return get_parser().help_str() + longhelp
diff --git a/becommands/target.py b/becommands/target.py
deleted file mode 100644 (file)
index 7e41451..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Chris Ball <cjb@laptop.org>
-#                         Gianluca Montecchi <gian@grys.it>
-#                         Marien Zwart <marienz@gentoo.org>
-#                         Thomas Gerigk <tgerigk@gmx.de>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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/doc/Makefile b/doc/Makefile
new file mode 100644 (file)
index 0000000..6d7bbc5
--- /dev/null
@@ -0,0 +1,94 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = .build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+help:
+       @echo "Please use \`make <target>' where <target> is one of"
+       @echo "  html      to make standalone HTML files"
+       @echo "  dirhtml   to make HTML files named index.html in directories"
+       @echo "  pickle    to make pickle files"
+       @echo "  json      to make JSON files"
+       @echo "  htmlhelp  to make HTML files and a HTML help project"
+       @echo "  qthelp    to make HTML files and a qthelp project"
+       @echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+       @echo "  changes   to make an overview of all changed/added/deprecated items"
+       @echo "  linkcheck to check all external links for integrity"
+       @echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
+       @echo "  libbe     to autogenerate files for all libbe modules"
+
+clean:
+       -rm -rf $(BUILDDIR) libbe
+
+html: libbe
+       $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+       @echo
+       @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml: libbe
+       $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+       @echo
+       @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+pickle: libbe
+       $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+       @echo
+       @echo "Build finished; now you can process the pickle files."
+
+json: libbe
+       $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+       @echo
+       @echo "Build finished; now you can process the JSON files."
+
+htmlhelp: libbe
+       $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+       @echo
+       @echo "Build finished; now you can run HTML Help Workshop with the" \
+             ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp: libbe
+       $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+       @echo
+       @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+             ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+       @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/bugs-everywhere.qhcp"
+       @echo "To view the help file:"
+       @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/bugs-everywhere.qhc"
+
+latex: libbe
+       $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+       @echo
+       @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+       @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+             "run these through (pdf)latex."
+
+changes: libbe
+       $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+       @echo
+       @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck: libbe
+       $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+       @echo
+       @echo "Link check complete; look for any errors in the above output " \
+             "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest: libbe
+       $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+       @echo "Testing of doctests in the sources finished, look at the " \
+             "results in $(BUILDDIR)/doctest/output.txt."
+
+.PHONY : libbe
+libbe :
+       python generate-libbe-txt.py
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644 (file)
index 0000000..1090a28
--- /dev/null
@@ -0,0 +1,198 @@
+# -*- coding: utf-8 -*-
+#
+# bugs-everywhere documentation build configuration file, created by
+# sphinx-quickstart on Fri Feb  5 20:02:21 2010.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+sys.path.insert(0, os.path.abspath('..'))
+
+import libbe.version
+
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage',
+              'numpydoc']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['.templates']
+
+# The suffix of source filenames.
+source_suffix = '.txt'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'bugs-everywhere'
+copyright = u'2010, W. Trevor King'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = libbe.version.version()
+# The full version, including alpha/beta/rc tags.
+release = libbe.version.version()
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = ['.build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['.static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'bugs-everywheredoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'bugs-everywhere.tex', u'bugs-everywhere Documentation',
+   u'W. Trevor King', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
diff --git a/doc/distributed_bugtracking.txt b/doc/distributed_bugtracking.txt
new file mode 100644 (file)
index 0000000..5ca42a6
--- /dev/null
@@ -0,0 +1,57 @@
+***********************
+Distributed Bugtracking
+***********************
+
+Usage Cases
+===========
+
+Case 1: Tracking the status of bugs in remote repo branches
+-----------------------------------------------------------
+
+See the discussion in
+#bea86499-824e-4e77-b085-2d581fa9ccab/12c986be-d19a-4b8b-b1b5-68248ff4d331#.
+Here, it doesn't matter whether the remote repository is a branch of
+the local repository, or a completely separate project
+(e.g. upstream, ...).  So long as the remote project provides access
+via some REPO format, you can use::
+
+    $ be --repo REPO ...
+
+to run your query, or::
+
+    $ be diff REPO
+
+to see the changes between the local and remote repositories.
+
+
+Case 2: Importing bugs from other repositories
+----------------------------------------------
+
+Case 2.1: If the remote repository is a branch of the local repository::
+
+     $ <VCS> merge <REPO>
+
+Case 2.2: If the remote repository is not a branch of the local repository
+(Hypothetical command)::
+
+    $ be import <REPO> <ID>
+
+
+Notes
+=====
+
+Providing public repositories
+-----------------------------
+
+e.g. for non-dev users.  These are just branches that expose a public
+interface (HTML, email, ...).  Merge and query like any other
+development branch.
+
+
+Managing permissions
+--------------------
+
+Many bugtrackers implement some sort of permissions system, and they
+are certainly required for a central system with diverse user roles.
+However DVCSs also support the "pull my changes" workflow, where
+permissions are irrelevant.
diff --git a/doc/doc.txt b/doc/doc.txt
new file mode 100644 (file)
index 0000000..e1b7a3a
--- /dev/null
@@ -0,0 +1,23 @@
+****************************
+Producing this documentation
+****************************
+
+This documentation is written in reStructuredText_, and produced
+using Sphinx_ and the numpydoc_ extension.  The documentation source
+should be fairly readable without processing, but you can compile the
+documentation, you'll need to install Sphinx and numpydoc::
+
+    $ easy_install Sphinx
+    $ easy_install numpydoc
+
+.. _Sphinx: http://sphinx.pocoo.org/
+.. _numpydoc: http://pypi.python.org/pypi/numpydoc
+
+See the reStructuredText quick reference and the `NumPy/SciPy
+documentation guide`_ for an introduction to the documentation
+syntax.
+
+.. _reStructuredText:
+  http://docutils.sourceforge.net/docs/user/rst/quickref.html
+.. _NumPy/SciPy documentation guide:
+  http://projects.scipy.org/numpy/wiki/CodingStyleGuidelines
diff --git a/doc/email.txt b/doc/email.txt
new file mode 120000 (symlink)
index 0000000..b25875f
--- /dev/null
@@ -0,0 +1 @@
+../interfaces/email/interactive/README
\ No newline at end of file
diff --git a/doc/generate-libbe-txt.py b/doc/generate-libbe-txt.py
new file mode 100644 (file)
index 0000000..35eb5c4
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/python
+#
+# Copyright
+
+"""Auto-generate reStructuredText of the libbe module tree for Sphinx.
+"""
+
+import sys
+import os, os.path
+
+sys.path.insert(0, os.path.abspath('..'))
+from test import python_tree
+
+def title(modname):
+    t = ':mod:`%s`' % modname
+    delim = '*'*len(t)
+    return '\n'.join([delim, t, delim, '', ''])
+
+def automodule(modname):
+    return '\n'.join([
+            '.. automodule:: %s' % modname,
+            '   :members:',
+            '   :undoc-members:',
+            '', ''])
+
+def toctree(children):
+    if len(children) == 0:
+        return ''
+    return '\n'.join([
+            '.. toctree::',
+            '   :maxdepth: 2',
+            '',
+            ] + [
+            '   %s.txt' % c for c in sorted(children)
+            ] + ['', ''])
+
+def make_module_txt(modname, children):
+    filename = os.path.join('libbe', '%s.txt' % modname)
+    if not os.path.exists('libbe'):
+        os.mkdir('libbe')
+    if os.path.exists(filename):
+        return None # don't overwrite potentially hand-written files.
+    f = file(filename, 'w')
+    f.write(title(modname))
+    f.write(automodule(modname))
+    f.write(toctree(children))
+    f.close()
+
+if __name__ == '__main__':
+    pt = python_tree(root_path='../libbe', root_modname='libbe')
+    for node in pt.traverse():
+        make_module_txt(node.modname, [c.modname for c in node])
diff --git a/doc/hacking.txt b/doc/hacking.txt
new file mode 100644 (file)
index 0000000..5b075f9
--- /dev/null
@@ -0,0 +1,75 @@
+**********
+Hacking BE
+**********
+
+Adding commands
+===============
+
+To write a plugin, you simply create a new file in the
+``libbe/commands/`` directory.  Take a look at one of the simpler
+plugins (e.g. ``remove.py``) for an example of how that looks, and to
+start getting a feel for the libbe interface.
+
+See ``libbe/commands/base.py`` for the definition of the important
+classes ``Option``, ``Argument``, ``Command``, ``InputOutput``,
+``StorageCallbacks``, and ``UserInterface`` classes.  You'll be
+subclassing ``Command`` for your command, but all those classes will
+be important.
+
+
+Command completion
+------------------
+
+BE implements a general framework to make it easy to support command
+completion for arbitrary plugins.  In order to support this system,
+any of your completable ``Argument()`` instances (in your command's
+``.options`` or ``.args``) should be initialized with some valid
+completion_callback function.  Some common cases are defined in
+``libbe.command.util``.  If you need more flexibility, see
+``libbe.command.list``'s ``--sort`` option for an example of
+extensions via ``libbe.command.util.Completer``, or write a custom
+completion function from scratch.
+
+
+Adding user interfaces
+======================
+
+Take a look at ``libbe/ui/command_line.py`` for an example.  Basically
+you'll need to setup a ``UserInterface`` instance for running commands.
+More details to come after I write an HTML UI...
+
+
+Testing
+=======
+
+Run any tests in your module with::
+
+    be$ python test.py <python.module.name>
+
+for example:
+
+    be$ python test.py libbe.command.merge
+
+For a definition of "any tests", see ``test.py``'s
+``add_module_tests()`` function.
+
+Note that you will need to run ``make`` before testing a clean BE
+branch to auto-generate required files like ``libbe/_version.py``.
+
+
+Profiling
+=========
+
+Find out which 20 calls take the most cumulative time (time of
+execution + childrens' times)::
+
+    $ python -m cProfile -o profile be [command] [args]
+    $ python -c "import pstats; p=pstats.Stats('profile'); p.sort_stats('cumulative').print_stats(20)"
+
+It's often useful to toss::
+
+    import sys, traceback
+    print >> sys.stderr, '-'*60, '\n', '\n'.join(traceback.format_stack()[-10:])
+
+into expensive functions (e.g. ``libbe.util.subproc.invoke()``) if
+you're not sure why they're being called.
diff --git a/doc/html.txt b/doc/html.txt
new file mode 100644 (file)
index 0000000..9e7f114
--- /dev/null
@@ -0,0 +1,7 @@
+**************
+HTML Interface
+**************
+
+There's an interactive HTML interface in the works
+(http://bitbucket.org/sjl/cherryflavoredbugseverywhere/), but it's not
+ready for use as a public interface yet.
diff --git a/doc/index.txt b/doc/index.txt
new file mode 100644 (file)
index 0000000..30b0318
--- /dev/null
@@ -0,0 +1,39 @@
+Welcome to the bugs-everywhere documentation!
+=============================================
+
+Bugs Everywhere (BE) is a bugtracker built on distributed version
+control.  It works with Arch_, Bazaar_, Darcs_, Git_, and Mercurial_
+at the moment, but is easily extensible.  It can also function with no
+VCS at all.
+
+.. _Arch: http://www.gnu.org/software/gnu-arch/
+.. _Bazaar: http://bazaar.canonical.com/
+.. _Darcs: http://darcs.net/
+.. _Git: http://git-scm.com/
+.. _Mercurial: http://mercurial.selenic.com/
+
+The idea is to package the bug information with the source code, so
+that bugs can be marked "fixed" in the branches that fix them.
+
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+   install.txt
+   tutorial.txt
+   email.txt
+   html.txt
+   distributed_bugtracking.txt
+   hacking.txt
+   spam.txt
+   libbe/libbe.txt
+   doc.txt
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/doc/install.txt b/doc/install.txt
new file mode 100644 (file)
index 0000000..b1d153e
--- /dev/null
@@ -0,0 +1,55 @@
+*************
+Installing BE
+*************
+
+Bazaar repository
+=================
+
+BE is available as a bzr repository::
+
+    $ bzr branch http://bzr.bugseverywhere.org/be
+
+See the homepage_ for details.  If you do branch the bzr repo, you'll
+need to run::
+
+    $ make
+
+to build some auto-generated files (e.g. ``libbe/_version.py``), and::
+
+    $ make install
+
+to install BE.  By default BE will install into your home directory,
+but you can tweak the ``PREFIX`` variable in ``Makefile`` to install
+to another location.
+
+.. _homepage: http://bugseverywhere.org/
+
+
+Release tarballs
+================
+
+For those not interested in the cutting edge, or those who don't want
+to worry about installing Bazaar, we'll post release tarballs somewhere
+(once we actually make a release).  After you've downloaded the release
+tarball, unpack it with::
+
+    $ tar -xzvf be-<VERSION>.tar.gz
+
+And install it with:::
+
+    $ cd be-<VERSION>
+    $ make install
+
+
+Distribution packages
+=====================
+
+Some distributions (Debian_ , Ubuntu_ , others?) package BE.  If
+you're running one of those distributions, you can install the package
+with your regular package manager.  For Debian, Ubuntu, and related
+distros, that's::
+
+    $ apt-get install bugs-everywhere
+
+.. _Debian: http://packages.debian.org/sid/bugs-everywhere
+.. _Ubuntu: http://packages.ubuntu.com/lucid/bugs-everywhere
similarity index 100%
rename from doc/be.1.sgml
rename to doc/man/be.1.sgml
similarity index 89%
rename from doc/module.mk
rename to doc/man/module.mk
index 7791f48e951dc2975626733c192bcb3a213c4626..ea4eacc6a7be732dc859e8ac72a21f306a60fd65 100644 (file)
@@ -3,7 +3,8 @@
 # doc/module.mk
 # Part of Bugs Everywhere, a distributed bug tracking system.
 #
-# Copyright (C) 2008-2009 Chris Ball <cjb@laptop.org>
+# Copyright (C) 2008-2010 Chris Ball <cjb@laptop.org>
+#                         Gianluca Montecchi <gian@grys.it>
 #                         W. Trevor King <wking@drexel.edu>
 #
 # This program is free software; you can redistribute it and/or modify
@@ -22,7 +23,7 @@
 
 # Makefile module for documentation
 
-MODULE_DIR := doc
+MODULE_DIR := doc/src
 
 MANPAGES = be.1
 manpage_files = $(patsubst %,${MODULE_DIR}/%,${MANPAGES})
diff --git a/doc/spam.txt b/doc/spam.txt
new file mode 100644 (file)
index 0000000..39e7a86
--- /dev/null
@@ -0,0 +1,60 @@
+*****************
+Dealing with spam
+*****************
+
+In the case that some spam or inappropriate comment makes its way
+through you interface, you can (sometimes) remove the offending commit
+``XYZ``.
+
+
+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 <XYZ+1>..-1 --onto before:XYZ . |
++----------+-----------------------------------------------+
+| darcs    | darcs obliterate --matches 'name XYZ'         |
++----------+-----------------------------------------------+
+| git      | git rebase --onto XYZ~1 XYZ                   |
++----------+-----------------------------------------------+
+| hg [#]_  |                                               |
++----------+-----------------------------------------------+
+
+.. [#] Requires the ```bzr-rebase`` plugin`_.  Note, you have to
+   increment ``XYZ`` by hand for ``<XYZ+1>``, because ``bzr`` does not
+   support ``after:XYZ``.
+
+.. [#] From `Mercurial: The Definitive Guide`:
+
+     "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"
+
+.. _bzr-rebase plugin: http://wiki.bazaar.canonical.com/Rebase
+.. _Mercurial: The Definitive Guide:
+  http://hgbook.red-bean.com/read/finding-and-fixing-mistakes.html#id394667
+
+Warnings about changing history
+===============================
+
+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/doc/tutorial.txt b/doc/tutorial.txt
new file mode 100644 (file)
index 0000000..7932c9c
--- /dev/null
@@ -0,0 +1,393 @@
+********
+Tutorial
+********
+
+Introduction
+============
+
+Bugs Everywhere (BE) is a bugtracker built on distributed revision
+control.  The idea is to package the bug information with the source
+code, so that developers working on the code can make appropriate
+changes to the bug repository as they go.  For example, by marking a
+bug as "fixed" and applying the fixing changes in the same commit.
+This makes it easy to see what's been going on in a particular branch
+and helps keep the bug repository in sync with the code.
+
+However, there are some differences compared to centralized
+bugtrackers.  Because bugs and comments can be created by several
+users in parallel, they have globally unique IDs_ rather than numbers.
+There is also a developer-friendly command-line_ interface to
+compliment the user-friendly web_ and email_ interfaces.  This
+tutorial will focus on the command-line interface as the most
+powerful, and leave the web and email interfaces to other documents.
+
+.. _command-line: `Command-line interface`_
+.. _web: tutorial-html.txt
+.. _email: tutorial-email.txt
+.. _IDs: libbe/libbe.util.id.txt
+
+Installation
+============
+
+If your distribution packages BE, it will be easiest to use their package.
+For example, most Debian-based distributions support::
+
+    $ apt-get install bugs-everywhere
+
+Bugs
+====
+
+If you have any problems with BE, you can look for matching bugs::
+
+    $ be --repo http://bugseverywhere.org/bugs list
+
+If your bug isn't listed, please open a new bug::
+
+    $ be --repo http://bugseverywhere.org/bugs new 'bug'
+    Created bug with ID bea/abc
+    $ be --repo http://bugseverywhere.org/bugs comment bea/def
+    <editor spawned for comments>
+
+
+Command-line interface
+======================
+
+Help
+----
+
+All of the following information elaborates on the command help text,
+which is stored in the code itself, and therefore more likely to be up
+to date.  You can get a list of commands and topics with::
+
+    $ be help
+
+Or see specific help on ``COMMAND`` with
+
+    $ be help COMMAND
+
+for example::
+
+    $ be help init
+
+will give help on the ``init`` command.
+
+Initialization
+--------------
+
+You're happily coding in your Arch_ / Bazaar_ / Darcs_ / Git_ /
+Mercurial_ versioned project and you discover a bug.  "Hmm, I'll need
+a simple way to track these things", you think.  This is where BE
+comes in.  One of the benefits of distributed versioning systems is
+the ease of repository creation, and BE follows this trend.  Just
+type::
+
+    $ be init
+    Using <VCS> for revision control.
+    BE repository initialized.
+
+in your project's root directory.  This will create a ``.be``
+directory containing the bug repository and notify your VCS so it will
+be versioned starting with your next commit.  See::
+
+    $ be help init
+
+for specific details about where the ``.be`` directory will end up
+if you call it from a directory besides your project's root.
+
+.. _Arch: http://www.gnu.org/software/gnu-arch/
+.. _Bazaar: http://bazaar.canonical.com/
+.. _Darcs: http://darcs.net/
+.. _Git: http://git-scm.com/
+.. _Mercurial: http://mercurial.selenic.com/
+
+Inside the ``.be`` directory (among other things) there will be a long
+UUID_ directory.  This is your bug directory.  The idea is that you
+could keep several bug directories in the same repository, using one
+to track bugs, another to track roadmap issues, etc.  See IDs_ for
+details.  For BE itself, the bug directory is
+``bea86499-824e-4e77-b085-2d581fa9ccab``, which is why all the bug and
+comment IDs in this tutorial will start with ``bea/``.
+
+.. _UUID: http://en.wikipedia.org/wiki/Universally_Unique_Identifier
+
+
+Creating bugs
+-------------
+
+Create new bugs with::
+
+    $ be new <SUMMARY>
+
+For example::
+
+    $ be new 'Missing demuxalizer functionality'
+    Created bug with ID bea/28f
+
+If you are entering a bug reported by another person, take advantage
+of the ``--reporter`` option to give them credit::
+
+    $ be new --reporter 'John Doe <jdoe@example.com>' 'Missing whatsit...'
+    Created bug with ID bea/81a
+
+See ``be help new`` for more details.
+
+While the bug summary should include the appropriate keywords, it
+should also be brief.  You should probably add a comment immediately
+giving a more elaborate explanation of the problem so that the
+developer understands what you want and when the bug can be considered
+fixed.
+
+Commenting on bugs
+------------------
+
+Bugs are like little mailing lists, and you can comment on the bug
+itself or previous comments, attach files, etc.  For example
+
+    $ be comment abc/28f "Thoughts about demuxalizers..."
+    Created comment with ID abc/28f/97a
+    $ be comment abc/def/012 "Oops, I forgot to mention..."
+    Created comment with ID abc/28f/e88
+
+Usually comments will be long enough that you'll want to compose them
+in a text editor, not on the command line itself.  Running ``be
+comment`` without providing a ``COMMENT`` argument will try to spawn
+an editor automatically (using your environment's ``VISUAL`` or
+``EDITOR``, see `Guide to Unix, Environmental Variables`_).
+
+.. _Guide to Unix, Environmental Variables:
+   http://en.wikibooks.org/wiki/Guide_to_Unix/Environment_Variables
+
+You can also pipe the comment body in on stdin, which is especially
+useful for binary attachments, etc.::
+
+    $ cat screenshot.png | be comment --content-type image/png bea/28f
+    Created comment with ID bea/28f/35d
+
+It's polite to insert binary attachments under comments that explain
+the content and why you're attaching it, so the above should have been
+
+    $ be comment bea/28f "Whosit dissapears when you mouse-over whatsit."
+    Created comment with ID bea/28f/41d
+    $ cat screenshot.png | be comment --content-type image/png bea/28f/41d
+    Created comment with ID bea/28f/35d
+
+For more details, see ``be help comment``.
+
+Showing bugs
+------------
+
+Ok, you understand how to enter bugs, but how do you get that
+information back out?  If you know the ID of the item you're
+interested in (e.g. bug bea/28f), try::
+
+    $ be show bea/28f
+              ID : 28fb711c-5124-4128-88fe-a88a995fc519
+      Short name : bea/28f
+        Severity : minor
+          Status : open
+        Assigned :
+        Reporter :
+         Creator : ...
+         Created : ...
+    Missing demuxalizer functionality
+    --------- Comment ---------
+    Name: bea/28f/97a
+    From: ...
+    Date: ...
+    
+    Thoughts about demuxalizers...
+      --------- Comment ---------
+      Name: bea/28f/e88
+      From: ...
+      Date: ...
+      
+      Thoughts about demuxalizers...
+    --------- Comment ---------
+    Name: bea/28f/41d
+    From: ...
+    Date: ...
+    
+    Whosit dissapears when you mouse-over whatsit.
+      --------- Comment ---------
+      Name: bea/28f/35d
+      From: ...
+      Date: ...
+      
+      Content type image/png not printable.  Try XML output instead
+
+You can also get a single comment body, which is useful for extracting
+binary attachments::
+
+    $ be show --only-raw-body bea/28f/35d > screenshot.png
+
+There is also an XML output format, which can be useful for emailing
+entries around, scripting BE, etc.
+
+    $ be show --xml bea/35d
+    <?xml version="1.0" encoding="UTF-8" ?>
+    <be-xml>
+    ...
+
+Listing bugs
+------------
+
+If you *don't* know which bug you're interested in, you can query
+the whole bug directory::
+
+    $ be list
+    bea/28f:om: Missing demuxalizer functionality
+    bea/81a:om: Missing whatsit...
+
+There are a whole slew of options for filtering the list of bugs.  See
+``be help list`` for details.
+
+Showing changes
+---------------
+
+Often you will want to see what's going on in another dev's branch or
+remind yourself what you've been working on recently.  All VCSs have
+some sort of ``diff`` command that shows what's changed since revision
+``XYZ``.  BE has it's own command that formats the bug-repository
+portion of those changes in an easy-to-understand summary format.  To
+compare your working tree with the last commit::
+
+    $ be diff
+    New bugs:
+      bea/01c:om: Need command output abstraction for flexible UIs
+    Modified bugs:
+      bea/343:om: Attach tests to bugs
+        Changed bug settings:
+          creator: None -> W. Trevor King <wking@drexel.edu>
+
+Compare with a previous revision ``480``::
+
+    $ be diff 480
+    ...
+
+Compare your BE branch with the trunk::
+
+    $ be diff --repo http://bugseverywhere.org/bugs/
+
+Manipulating bugs
+-----------------
+
+There are several commands that allow to to set bug properties.  They
+are all fairly straightforward, so we will merely point them out here,
+and refer you to ``be help COMMAND`` for more details.
+
+* ``assign``, Assign an individual or group to fix a bug
+* ``depend``, Add/remove bug dependencies
+* ``due``, Set bug due dates
+* ``status``, Change a bug's status level
+* ``severity``, Change a bug's severity level
+* ``tag``, Tag a bug, or search bugs for tags
+* ``target``, Assorted bug target manipulations and queries
+
+You can also remove bugs you feel are no longer useful with
+``be remove``, and merge duplicate bugs with ``be merge``.
+
+Subscriptions
+-------------
+
+Since BE bugs act as mini mailing lists, we provide ``be subscribe``
+as a way to manage change notification.  You can subscribe to all
+the changes with::
+
+    $ be subscribe --types all DIR
+
+Subscribe only to bug creaton on bugseverywhere.org with::
+
+    $ be subscribe --server bugseverywhere.org --types new DIR
+
+Subscribe to get all the details about bug ``bea/28f``::
+
+    $ be subscribe --types new bea/28f
+
+To unsubscribe, simply repeat the subscription command adding the
+``--unsubscribe`` option, but be aware that it may take some time for
+these changes to propogate between distributed repositories.  If you
+don't feel confident in your ability to filter email, it's best to
+only subscribe to the repository for which you have direct write
+access.
+
+Managing bug directories
+------------------------
+
+``be set`` lets you configure a bug directory.  You can set
+
+* ``active_status``
+  The allowed active bug states and their descriptions.
+* ``inactive_status``
+  The allowed inactive bug states and their descriptions.
+* ``severities``
+  The allowed bug severities and their descriptions.
+* ``target``
+  The current project development target (bug UUID).
+* ``extra_strings``
+  Space for an array of extra strings.  You usually won't bother with
+  this directly.
+
+For example, to set the current target to '1.2.3'::
+
+    $ be set target $(be target --resolve '1.2.3')
+
+Import XML
+----------
+
+For serializing bug information (e.g. to email to a mailing list), use::
+
+    $ be show --xml bea/28f > bug.xml
+
+This information can be imported into (another) bug directory via
+
+    $ be import-xml bug.xml
+
+Also distributed with BE are some utilities to convert mailboxes
+into BE-XML (``be-mail-to-xml``) and convert BE-XML into mbox_
+format for reading in your mail client.
+
+.. _mbox: http://en.wikipedia.org/wiki/Mbox
+
+Export HTML
+-----------
+
+To create a static dump of your bug directory, use::
+
+    $ be html
+
+This is a fairly flexible command, see ``be help html`` for details.
+It works pretty well as the browsable part of a public interface using
+the email_ interface for interactive access.
+
+BE over HTTP
+------------
+
+Besides using BE to work directly with local VCS-based repositories,
+you can use::
+
+    $ be serve
+
+To serve a repository over HTTP.  For example::
+
+    $ be serve > server.log 2>&1 &
+    $ be --repo http://localhost:8000 list
+
+Of course, be careful about serving over insecure networks, since
+malicous users could fill your disk with endless bugs, etc.  You can
+dissable write access by using the ``--read-only`` option, which would
+make serving on a public network safer.
+
+Driving the VCS through BE
+--------------------------
+
+Since BE uses internal storage drivers for its various backends, it
+seemed useful to provide a uniform interface to some of the common
+functionality.  These commands are not intended to replace the usually
+much more powerful native VCS commands, but to provide an easy means
+of simple VCS-agnostic scripting for BE user interfaces, etc.
+
+Commit
+~~~~~~
+
+Currently, we only expose ``be commit``, which commits all currently
+pending changes.
diff --git a/interfaces/README b/interfaces/README
deleted file mode 100644 (file)
index 4d74580..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-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 <XYZ+1>..-1 --onto before:XYZ .
-      (requires bzr-rebase plugin, note, you have to increment XYZ by
-      hand for <XYZ+1>, 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.
index 79ef9a9f73ac77038e3f72c2637f2d094d52c03d..48bccdd4af1c84d899a7ec05d3bcd79f93e24bd0 100644 (file)
@@ -1,16 +1,19 @@
+***************
+Email Interface
+***************
+
 Overview
 ========
 
 The interactive email interface to Bugs Everywhere (BE) attempts to
-provide a Debian-bug-tracking-system-style interface to a BE
+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 .
+.. _Debian-bug-tracking-system-style: http://www.debian.org/Bugs
 
 Architecture
 ============
@@ -18,27 +21,34 @@ 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:<bug-id>] 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.
+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 four parsing styles:
+
+    +--------------------+----------------------------------+
+    | Style              | Subject                          |
+    +====================+==================================+
+    | creating bugs      | [be-bug:submit] new bug summary  |
+    +--------------------+----------------------------------+
+    | commenting on bugs | [be-bug:<bug-id>] 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.
+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 <jdoe@example.com>
@@ -51,22 +61,22 @@ attached as the bug's first comment.
     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
+    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.
+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.
+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 <jdoe@example.com>
@@ -85,11 +95,11 @@ 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
+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().
+shlex.split().::
 
     From jdoe@example.com Fri Apr 18 12:00:00 2008
     From: John Doe <jdoe@example.com>
@@ -109,37 +119,42 @@ shlex.split().
 Example emails
 ==============
 
-Take a look at my interfaces/email/interactive/examples for some
+Take a look at ``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.
+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.
+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::
+
+    --repo /path/to/served/repository
 
-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
+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.
+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
+Send test emails in to ``be-handle-mail`` with something like::
+
+    cat examples/blank | ./be-handle-mail -o -l - -a
index fa8069888d4734cbe681a4a7c113a9a45691c5c2..c8343fc4e17039ecba2651e40b2ed1d96b6a57e5 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 #
-# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
 #
 # 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
@@ -58,44 +58,51 @@ import shlex
 import sys
 import time
 import traceback
+import types
 import doctest
 import unittest
 
-from becommands import subscribe
-import libbe.cmdutil, libbe.encoding, libbe.utility, libbe.diff, \
-    libbe.bugdir, libbe.bug, libbe.comment
+import libbe.bugdir
+import libbe.bug
+import libbe.comment
+import libbe.diff
+import libbe.command
+import libbe.command.subscribe as subscribe
+import libbe.storage
+import libbe.ui.command_line
+import libbe.util.encoding
+import libbe.util.utility
 import send_pgp_mime
 
-THIS_SERVER = u"thor.physics.drexel.edu"
-THIS_ADDRESS = u"BE Bugs <wking@thor.physics.drexel.edu>"
-
+THIS_SERVER = u'thor.physics.drexel.edu'
+THIS_ADDRESS = u'BE Bugs <wking@thor.physics.drexel.edu>'
+UI = None
 _THIS_DIR = os.path.abspath(os.path.dirname(__file__))
-BE_DIR = _THIS_DIR
-LOGPATH = os.path.join(_THIS_DIR, u"be-handle-mail.log")
+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_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"]
+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'diff',
+                    u'due', u'help', u'list', u'merge', u'new', 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()
+ENCODING = u'utf-8'
+libbe.util.encoding.ENCODING = ENCODING # force default encoding
 
 class InvalidEmail (ValueError):
     def __init__(self, msg, message):
@@ -103,10 +110,10 @@ class InvalidEmail (ValueError):
         self.msg = msg
     def response(self):
         header = self.msg.response_header
-        body = [u"Error processing email:\n",
-                self.response_body(), u""]
+        body = [u'Error processing email:\n',
+                self.response_body(), u'']
         response_generator = \
-            send_pgp_mime.PGPMimeMessageFactory(u"\n".join(body))
+            send_pgp_mime.PGPMimeMessageFactory(u'\n'.join(body))
         response = MIMEMultipart()
         response.attach(response_generator.plain())
         response.attach(self.msg.msg)
@@ -114,44 +121,44 @@ class InvalidEmail (ValueError):
         return ret
     def response_body(self):
         err_text = [unicode(self)]
-        return u"\n".join(err_text)
+        return u'\n'.join(err_text)
 
 class InvalidSubject (InvalidEmail):
     def __init__(self, msg, message=None):
         if message == None:
-            message = u"Invalid subject"
+            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:",
+        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",
+        err_text = [u'Invalid pseudo-header:\n',
                     unicode(self)]
-        return u"\n".join(err_text)
+        return u'\n'.join(err_text)
 
 class InvalidCommand (InvalidEmail):
     def __init__(self, msg, command, message=None):
-        bigmessage = u"Invalid execution command '%s'" % command
+        bigmessage = u'Invalid execution command "%s"' % command
         if message != None:
-            bigmessage += u"\n%s" % message
+            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)
+        bigmessage = u'Invalid option "%s"' % (option)
         if message != None:
-            bigmessage += u"\n%s" % message
+            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
+        bigmessage = 'Notification failed: %s' % msg
         Exception.__init__(self, bigmessage)
         self.short_msg = msg
 
@@ -165,11 +172,11 @@ class ID (object):
     def __init__(self, command):
         self.command = command
     def extract_id(self):
-        if hasattr(self, "cached_id"):
+        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 (.*)")
+        if self.command.command.name == u'new':
+            regexp = re.compile(u'Created bug with ID (.*)')
         else:
             raise NotImplementedError, self.command.command
         match = regexp.match(self.command.stdout)
@@ -178,13 +185,12 @@ class ID (object):
         return self._cached_id
     def __str__(self):
         if self.command.ret != 0:
-            return "<id for %s>" % repr(self.command)
-        return "<id %s>" % self.extract_id()
+            return '<id for %s>' % repr(self.command)
+        return '<id %s>' % self.extract_id()
 
 class Command (object):
     """
-    A becommands command wrapper.
-    Doesn't validate input, so do that before initializing.
+    A libbe.command.Command handler.
 
     Initialize with
       Command(msg, command, args=None, stdin=None)
@@ -196,18 +202,17 @@ class Command (object):
     """
     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.command = libbe.command.get_command_class(command_name=command)()
+        self.command._setup_io = lambda i_enc,o_enc : None
         self.ret = None
+        self.stdin = stdin
         self.stdout = None
-        self.stderr = None
-        self.err = None
     def __str__(self):
-        return "<command: %s %s>" % (self.command, " ".join([str(s) for s in self.args]))
+        return '<command: %s %s>' % (self.command, ' '.join([str(s) for s in self.args]))
     def normalize_args(self):
         """
         Expand any ID placeholders in self.args.
@@ -221,60 +226,25 @@ class Command (object):
         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)
+        if self.command.name in [None, u'']: # don't accept blank commands
+            raise InvalidCommand(self.msg, self, 'Blank')
+        elif self.command.name 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)
+        UI.io.set_stdin(self.stdin)
+        self.ret = libbe.ui.command_line.dispatch(UI, self.command, self.args)
+        self.stdout = UI.io.get_stdout()
+        return (self.ret, self.stdout)
     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))]
+        response_body = [u'Results of running: (exit code %d)' % self.ret,
+                         u'  %s %s' % (self.command.name,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_body.extend([u'', u'output:', u'', self.stdout])
+        response_body.append(u'') # trailing endline
         response_generator = \
-            send_pgp_mime.PGPMimeMessageFactory(u"\n".join(response_body))
+            send_pgp_mime.PGPMimeMessageFactory(u'\n'.join(response_body))
         return response_generator.plain()
 
 class DiffTree (libbe.diff.DiffTree):
@@ -304,6 +274,8 @@ class DiffTree (libbe.diff.DiffTree):
     """
     def report_or_none(self):
         report = self.report()
+        if report == None:
+            return None
         payload = report.get_payload()
         if payload == None or len(payload) == 0:
             return None
@@ -311,27 +283,27 @@ class DiffTree (libbe.diff.DiffTree):
     def report_string(self):
         report = self.report_or_none()
         if report == None:
-            return "No changes"
+            return 'No changes'
         else:
-            return send_pgp_mime.flatten(self.report(), to_unicode=True)
+            return send_pgp_mime.flatten(report, to_unicode=True)
     def make_root(self):
         return MIMEMultipart()
     def join(self, root, parent, data_part):
-        if hasattr(parent, "attach_child_text"):
+        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))
+                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"]:
+            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"")
+                    self.data_mime_part = send_pgp_mime.encodedMIMEText(u'')
             if self.data_mime_part != None:
-                self.data_mime_part[u"Content-Description"] = self.name
+                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)
@@ -353,19 +325,19 @@ class Message (object):
             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)
+                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"
+            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"):
+        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
@@ -380,24 +352,24 @@ class Message (object):
             return self.msg[attr_name]
         return default
     def message_id(self, default=None):
-        return self.default_msg_attribute_access("message-id", default=default)
+        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"]
+        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"):
+        if hasattr(self, '_split_subject_cache'):
             return self._split_subject_cache
-        args = self.subject().split(u"]",1)
+        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)
+            self._split_subject_cache = (args[0]+u']', None)
         else:
-            self._split_subject_cache = (args[0]+u"]", args[1].strip())
+            self._split_subject_cache = (args[0]+u']', args[1].strip())
         return self._split_subject_cache
     def _subject_tag_type(self):
         """
@@ -410,13 +382,13 @@ class Message (object):
         type = None
         value = None
         if tag == SUBJECT_TAG_NEW:
-            type = u"new"
+            type = u'new'
         elif tag == SUBJECT_TAG_CONTROL:
-            type = u"control"
+            type = u'control'
         else:
             match = SUBJECT_TAG_COMMENT.match(tag)
             if len(match.groups()) == 1:
-                type = u"comment"
+                type = u'comment'
                 value = match.group(1)
         return (type, value)
     def validate_subject(self):
@@ -426,14 +398,14 @@ class Message (object):
         tag,subject = self._split_subject()
         if not tag.startswith(SUBJECT_TAG_START):
             raise InvalidSubject(
-                self, u"Subject must start with '%s'" % SUBJECT_TAG_START)
+                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")
+            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
@@ -445,7 +417,7 @@ class Message (object):
                 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/"):
+            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,
@@ -465,15 +437,15 @@ class Message (object):
             line = line.strip()
             if len(line) == 0:
                 break
-            if ":" not in line:
+            if ':' not in line:
                 raise InvalidPseudoheader(self, line)
-            key,value = line.split(":", 1)
+            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)
+                    self, u'Blank value for: %s' % key)
             dictionary[key] = value
         missing = []
         for key in required:
@@ -481,9 +453,9 @@ class Message (object):
                 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()
+                                      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()
@@ -491,7 +463,7 @@ class Message (object):
             if line.startswith(BREAK):
                 break
             i += 1 # increment past the current valid line.
-        return u"\n".join(body_lines[:i]).strip()
+        return u'\n'.join(body_lines[:i]).strip()
     def parse(self):
         """
         Parse the commands given in the email.  Raises assorted
@@ -500,22 +472,22 @@ class Message (object):
         """
         self.validate_subject()
         tag_type,value = self._subject_tag_type()
-        if tag_type == u"new":
+        if tag_type == u'new':
             commands = self.parse_new()
-        elif tag_type == u"comment":
+        elif tag_type == u'comment':
             commands = self.parse_comment(value)
-        elif tag_type == u"control":
+        elif tag_type == u'control':
             commands = self.parse_control()
         else:
-            raise Exception, u"Unrecognized tag type '%s'" % tag_type
+            raise Exception, u'Unrecognized tag type "%s"' % tag_type
         return commands
     def parse_new(self):
-        command = u"new"
+        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",
+        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 = \
@@ -523,49 +495,54 @@ class Message (object):
                                            NEW_REQUIRED_PSEUDOHEADERS,
                                            NEW_OPTIONAL_PSEUDOHEADERS,
                                            options)
-        if options[u"Confirm"].lower() == "no":
+        if options[u'Confirm'].lower() == 'no':
             self.confirm = False
-        if options[u"Subscribe"].lower() == "yes" and self.confirm == True:
+        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 = [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]
+            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))
+            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"]:
+            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":
+            if key in [u'Depend', u'Tag', u'Target', u'Subscribe']:
+                args = [id, value]
+            else:
+                args = [value, id]
+            if key == u'Subscribe':
+                if value.lower() != 'yes':
                     continue
-                args = ["--subscriber", self.author_addr(), id]
+                args = ['--subscriber', self.author_addr(), id]
             commands.append(Command(self, command, args))
         return commands
     def parse_comment(self, bug_uuid):
-        command = u"comment"
+        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":
+        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"-"]
+        args = [u'--author', author]
+        if alt_id != None:
+            args.extend([u'--alt-id', alt_id])
+        args.extend([u'--content-type', content_type, bug_id, u'-'])
         commands = [Command(self, command, args, stdin=body)]
         return commands
     def parse_control(self):
@@ -577,39 +554,46 @@ class Message (object):
                 continue
             if line.startswith(BREAK):
                 break
+            if type(line) == types.UnicodeType:
+                # work around http://bugs.python.org/issue1170
+                line = line.encode('unicode escape')
             fields = shlex.split(line)
+            if type(line) == types.UnicodeType:
+                # work around http://bugs.python.org/issue1170
+                for field in fields:
+                    field = unicode(field, 'unicode escape')
             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.")
+            raise InvalidEmail(self, u'No commands in control email.')
         return commands
-    def run(self):
+    def run(self, repo='.'):
         self._begin_response()
         commands = self.parse()
         try:
-            for command in commands:
+            for i,command in enumerate(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 = Command(self, 'commit', [subject])
                 self.commit_command.run()
                 if LOGFILE != None:
-                    LOGFILE.write(u"Autocommit:\n%s\n\n" %
+                    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)
+        response_header = [u'From: %s' % THIS_ADDRESS,
+                           u'To: %s' % self.author_addr(),
+                           u'Date: %s' % libbe.util.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())
+            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))
+            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)
@@ -625,86 +609,54 @@ class Message (object):
     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")
+                raise NotificationFailed('Autocommit dissabled')
             if len(self._response_messages) == 0:
-                raise NotificationFailed("Initial email failed.")
+                raise NotificationFailed('Initial email failed.')
             if self.commit_command.ret != 0:
                 # commit failed.  Error already logged.
-                raise NotificationFailed("Commit failed")
+                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 = UI.storage_callbacks.get_bugdir()
+        writeable = bd.storage.writeable
+        bd.storage.writeable = False
+        if bd.storage.versioned == False: # no way to tell what's changed
+            bd.storage.writeable = writeable
+            raise NotificationFailed('Not versioned')
 
         bd.load_all_bugs()
         subscribers = subscribe.get_bugdir_subscribers(bd, THIS_SERVER)
-
         if len(subscribers) == 0:
-            return [] 
+            bd.storage.writeable = writeable
+            return []
+        for subscriber,subscriptions in subscribers.items():
+            subscribers[subscriber] = []
+            for id,types in subscriptions.items():
+                for type in types:
+                    subscribers[subscriber].append(
+                        libbe.diff.Subscription(id,type))
 
         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)
+        diff.full_report(diff_tree=DiffTree)
         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)
+            header.replace_header('to', subscriber)
+            report = diff.report_tree(subscriptions, diff_tree=DiffTree)
+            root = report.report_or_none()
+            if root != None:
+                emails.append(send_pgp_mime.attach_root(header, root))
+                if LOGFILE != None:
+                    LOGFILE.write(u'Preparing to notify %s of changes\n' % subscriber)
+        bd.storage.writeable = writeable
         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)
+            assert commit_msg.startswith('Committed '), commit_msg
+            after_revision = commit_msg[len('Committed '):]
+            before_revision = bd.storage.revision_id(-2)
         else:
             before_revision = previous_revision
         if before_revision == None:
@@ -712,36 +664,36 @@ class Message (object):
             before_bd = libbe.bugdir.BugDir(from_disk=False,
                                             manipulate_encodings=False)
         else:
-            before_bd = bd.duplicate_bugdir(before_revision)
+            before_bd = libbe.bugdir.RevisionedBugDir(bd, 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)
+        root_dir = os.path.basename(bd.storage.repo)
         if previous_revision == None:
-            subject = "Changes to %s on %s by %s" \
+            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" \
+            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)
+        header = [u'From: %s' % THIS_ADDRESS,
+                  u'To: %s' % u'DUMMY-AUTHOR',
+                  u'Date: %s' % libbe.util.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))
+        return send_pgp_mime.header_from_text(text=u'\n'.join(header))
 
-def generate_global_tags(tag_base=u"be-bug"):
+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_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):
@@ -754,27 +706,25 @@ def open_logfile(logpath=None):
     """
     global LOGPATH, LOGFILE
     if logpath != None:
-        if logpath == u"-":
-            LOGPATH = u"stderr"
+        if logpath == u'-':
+            LOGPATH = u'stderr'
             LOGFILE = sys.stderr
-        elif logpath == u"none":
-            LOGPATH = u"none"
+        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)
+    if LOGFILE == None and LOGPATH != u'none':
+        LOGFILE = codecs.open(LOGPATH, u'a+',
+            libbe.util.encoding.get_filesystem_encoding())
 
 def close_logfile():
-    if LOGFILE != None and LOGPATH not in [u"stderr", u"none"]:
+    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)
@@ -783,15 +733,15 @@ def test():
 
 def main(args):
     from optparse import OptionParser
-    global AUTOCOMMIT, BE_DIR
+    global AUTOCOMMIT, UI
 
-    usage="be-handle-mail [options]\n\n%s" % (__doc__)
+    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('-r', '--repo', dest='repo', default=_THIS_DIR,
+                      metavar='REPO',
+                      help='Select the BE repository to serve (%default).')
     parser.add_option('-t', '--tag-base', dest='tag_base',
-                      default=SUBJECT_TAG_BASE, metavar="TAG",
+                      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.")
@@ -817,27 +767,28 @@ def main(args):
             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)
 
+    io = libbe.command.StringInputOutput()
+    UI = libbe.command.UserInterface(io, location=options.repo)
+
     if options.notify_since != None:
         if options.subscribers == True:
             if LOGFILE != None:
-                LOGFILE.write(u"Checking for subscribers to notify since revision %s\n"
+                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")
+                    LOGFILE.write(unicode(e) + u'\n')
             else:
                 for msg in emails:
                     if options.output == True:
@@ -845,12 +796,14 @@ def main(args):
                     else:
                         send_pgp_mime.mail(msg, send_pgp_mime.sendmail)
             close_logfile()
+        UI.cleanup()
         sys.exit(0)
 
     if len(msg_text.strip()) == 0: # blank email!?
         if LOGFILE != None:
-            LOGFILE.write(u"Blank email!\n")
+            LOGFILE.write(u'Blank email!\n')
             close_logfile()
+        UI.cleanup()
         sys.exit(1)
     try:
         m = Message(msg_text)
@@ -859,9 +812,11 @@ def main(args):
         response = e.response()
     except Exception, e:
         if LOGFILE != None:
-            LOGFILE.write(u"Uncaught exception:\n%s\n" % (e,))
+            LOGFILE.write(u'Uncaught exception:\n%s\n' % (e,))
             traceback.print_tb(sys.exc_traceback, file=LOGFILE)
             close_logfile()
+        m.commit_command.cleanup()
+        UI.cleanup()
         sys.exit(1)
     else:
         response = m.response_email()
@@ -869,21 +824,21 @@ def main(args):
         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,
+            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())
+            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")
+            LOGFILE.write(u'Checking for subscribers\n')
         try:
             emails = m.subscriber_emails()
         except NotificationFailed, e:
             if LOGFILE != None:
-                LOGFILE.write(unicode(e) + u"\n")
+                LOGFILE.write(unicode(e) + u'\n')
         else:
             for msg in emails:
                 if options.output == True:
@@ -892,7 +847,8 @@ def main(args):
                     send_pgp_mime.mail(msg, send_pgp_mime.sendmail)
 
     close_logfile()
-
+    m.commit_command.cleanup()
+    UI.cleanup()
 
 class GenerateGlobalTagsTestCase (unittest.TestCase):
     def setUp(self):
@@ -914,37 +870,40 @@ class GenerateGlobalTagsTestCase (unittest.TestCase):
     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.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")
+        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")
+        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")
+        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]")
+        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]")
+        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]")
+        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]")
+        generate_global_tags(u'projectX-bug')
+        m = SUBJECT_TAG_COMMENT.match('[projectX-bug:abc/xyz-123]')
         self.failUnlessEqual(len(m.groups()), 1)
-        self.failUnlessEqual(m.group(1), u"xyz-123")
+        self.failUnlessEqual(m.group(1), u'abc/xyz-123')
+
+unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
+suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
 
 if __name__ == "__main__":
     main(sys.argv)
diff --git a/interfaces/email/interactive/becommands b/interfaces/email/interactive/becommands
deleted file mode 120000 (symlink)
index 8af773c..0000000
+++ /dev/null
@@ -1 +0,0 @@
-../../../becommands
\ No newline at end of file
diff --git a/interfaces/email/interactive/examples/email_bugs b/interfaces/email/interactive/examples/email_bugs
new file mode 100644 (file)
index 0000000..949e1c1
--- /dev/null
@@ -0,0 +1,37 @@
+From jdoe@example.com Fri Apr 18 12:00:00 2008
+Content-Type: text/xml; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+From: jdoe@example.com
+To: a@b.com
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+Subject: [be-bug:xml] Updates to a, b
+
+<?xml version="1.0" encoding="utf-8" ?>
+<be-xml>
+ <version>
+   <tag>1.0.0</tag>
+   <branch-nick>be</branch-nick>
+   <revno>446</revno>
+   <revision-id>wking@drexel.edu-20091119214553-iqyw2cpqluww3zna</revision-id>
+ </version>
+ <bug>
+   <uuid>a</uuid>
+   <short-name>a</short-name>
+   <severity>minor</severity>
+   <status>open</status>
+   <creator>John Doe &lt;jdoe@example.com&gt;</creator>
+   <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+   <summary>Bug A</summary>
+ </bug>
+ <bug>
+   <uuid>b</uuid>
+   <short-name>b</short-name>
+   <severity>minor</severity>
+   <status>closed</status>
+   <creator>Jane Doe &lt;jdoe@example.com&gt;</creator>
+   <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+   <summary>Bug B</summary>
+ </bug>
+</be-xml>
+
index c19483ef157f609ae1178708f749cd8436b57c1e..517b1f0b1434aee8385f0fe64c60d2cc16deb173 100644 (file)
@@ -1,6 +1,6 @@
 #!/usr/bin/python
 #
-# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
 #
 # 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
diff --git a/interfaces/gui/beg/beg b/interfaces/gui/beg/beg
deleted file mode 100755 (executable)
index 55e537d..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env python
-import table
-from Tkinter import *
-from libbe import bugdir
-
-tk = Tk()
-Label(tk, text="Bug list").pack()
-mlb = table.MultiListbox(tk, (('Severity', 4), ('Creator', 8), ('Summary', 40)))
-for bug in [b for b in bugdir.tree_root(".").list() if b.active]:
-    mlb.insert(END, (bug.severity, bug.creator, bug.summary))
-mlb.pack(expand=YES,fill=BOTH)
-tk.mainloop()
diff --git a/interfaces/gui/beg/table.py b/interfaces/gui/beg/table.py
deleted file mode 100644 (file)
index 2865f28..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-from Tkinter import *
-
-class MultiListbox(Frame):
-    def __init__(self, master, lists):
-       Frame.__init__(self, master)
-       self.lists = []
-       for l,w in lists:
-           frame = Frame(self); frame.pack(side=LEFT, expand=YES, fill=BOTH)
-           Label(frame, text=l, borderwidth=1, relief=RAISED).pack(fill=X)
-           lb = Listbox(frame, width=w, borderwidth=0, selectborderwidth=0,
-                        relief=FLAT, exportselection=FALSE)
-           lb.pack(expand=YES, fill=BOTH)
-           self.lists.append(lb)
-           lb.bind('<B1-Motion>', lambda e, s=self: s._select(e.y))
-           lb.bind('<Button-1>', lambda e, s=self: s._select(e.y))
-           lb.bind('<Leave>', lambda e: 'break')
-           lb.bind('<B2-Motion>', lambda e, s=self: s._b2motion(e.x, e.y))
-           lb.bind('<Button-2>', lambda e, s=self: s._button2(e.x, e.y))
-       frame = Frame(self); frame.pack(side=LEFT, fill=Y)
-       Label(frame, borderwidth=1, relief=RAISED).pack(fill=X)
-       sb = Scrollbar(frame, orient=VERTICAL, command=self._scroll)
-       sb.pack(expand=YES, fill=Y)
-       self.lists[0]['yscrollcommand']=sb.set
-
-    def _select(self, y):
-       row = self.lists[0].nearest(y)
-       self.selection_clear(0, END)
-       self.selection_set(row)
-       return 'break'
-
-    def _button2(self, x, y):
-       for l in self.lists: l.scan_mark(x, y)
-       return 'break'
-
-    def _b2motion(self, x, y):
-       for l in self.lists: l.scan_dragto(x, y)
-       return 'break'
-
-    def _scroll(self, *args):
-       for l in self.lists:
-           apply(l.yview, args)
-
-    def curselection(self):
-       return self.lists[0].curselection()
-
-    def delete(self, first, last=None):
-       for l in self.lists:
-           l.delete(first, last)
-
-    def get(self, first, last=None):
-       result = []
-       for l in self.lists:
-           result.append(l.get(first,last))
-       if last: return apply(map, [None] + result)
-       return result
-           
-    def index(self, index):
-       self.lists[0].index(index)
-
-    def insert(self, index, *elements):
-       for e in elements:
-           i = 0
-           for l in self.lists:
-               l.insert(index, e[i])
-               i = i + 1
-
-    def size(self):
-       return self.lists[0].size()
-
-    def see(self, index):
-       for l in self.lists:
-           l.see(index)
-
-    def selection_anchor(self, index):
-       for l in self.lists:
-           l.selection_anchor(index)
-
-    def selection_clear(self, first, last=None):
-       for l in self.lists:
-           l.selection_clear(first, last)
-
-    def selection_includes(self, index):
-       return self.lists[0].selection_includes(index)
-
-    def selection_set(self, first, last=None):
-       for l in self.lists:
-           l.selection_set(first, last)
-
-if __name__ == '__main__':
-    tk = Tk()
-    Label(tk, text='MultiListbox').pack()
-    mlb = MultiListbox(tk, (('Subject', 40), ('Sender', 20), ('Date', 10)))
-    for i in range(1000):
-       mlb.insert(END, ('Important Message: %d' % i, 'John Doe', '10/10/%04d' % (1900+i)))
-    mlb.pack(expand=YES,fill=BOTH)
-    tk.mainloop()
-
diff --git a/interfaces/gui/wxbe/wxbe b/interfaces/gui/wxbe/wxbe
deleted file mode 100755 (executable)
index e71ae0c..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/usr/bin/env python
-import wx
-from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
-import sys, os.path
-from libbe import bugdir, names
-from libbe.bug import cmp_status, cmp_severity, cmp_time, cmp_full
-
-class MyApp(wx.App):
-    def OnInit(self):
-        frame = BugListFrame(None, title="Bug List")
-        frame.Show(True)
-        self.SetTopWindow(frame)
-        return True
-
-class BugListFrame(wx.Frame):
-    def __init__(self, *args, **kwargs):
-        wx.Frame.__init__(self, *args, **kwargs)
-        bugs = BugList(self)
-
-        # Widgets to display/sort/edit will go in this panel
-        # for now it is just a placeholder
-        panel = wx.Panel(self)
-        panel.SetBackgroundColour("RED")
-
-        vbox = wx.BoxSizer(wx.VERTICAL)
-        vbox.Add(panel, 0, wx.EXPAND)
-        vbox.Add(bugs, 1, wx.EXPAND)
-        
-        self.SetAutoLayout(True)
-        self.SetSizer(vbox)
-        self.Layout()
-
-class BugList(wx.ListCtrl, ListCtrlAutoWidthMixin):
-    def __init__(self, parent):
-        wx.ListCtrl.__init__(self, parent,
-                             style=wx.LC_REPORT)
-        ListCtrlAutoWidthMixin.__init__(self)
-
-        self.bugdir = bugdir.tree_root(".")
-        self.buglist = list(self.bugdir.list())
-        self.buglist.sort()
-        self.columns = ("id", "status", "severity", "summary")
-
-        dataIndex = 0
-        for x in range(len(self.columns)):
-            self.InsertColumn(x, self.columns[x].capitalize())
-            self.SetColumnWidth(x, wx.LIST_AUTOSIZE_USEHEADER)
-        for bug in [b for b in self.buglist if b.active]:
-            name = names.unique_name(bug, self.buglist)
-            id = self.InsertStringItem(self.GetItemCount(), name)
-            self.SetStringItem(id, 1, bug.status)
-            self.SetStringItem(id, 2, bug.severity)
-            self.SetStringItem(id, 3, bug.summary)
-            self.SetItemData(id, dataIndex) # set keys for each line
-            dataIndex += 1
-            self.EnsureVisible(id)
-        for x in range(len(self.columns)):
-            self.SetColumnWidth(x, wx.LIST_AUTOSIZE)
-            conts_width = self.GetColumnWidth(x)
-            self.SetColumnWidth(x, wx.LIST_AUTOSIZE_USEHEADER)
-            if conts_width > self.GetColumnWidth(x):
-                self.SetColumnWidth(x, conts_width)
-
-        self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColumnClick)
-        self.bugcmp_fn = cmp_full
-        # For reasons I don't understant, sorting is broken...
-        #self.SortItems(self.Sorter)
-        #self.Refresh()
-    def Sorter(self, key1, key2):
-        """Get bug info from the keys and pass to self.bugcmp_fn"""
-        bug1 = self.buglist[key1-1]
-        bug2 = self.buglist[key2-1]
-        # Another way of getting bug information
-        #bug1uuid = self.GetItem(key1, 0).GetText()
-        #bug2uuid = self.GetItem(key2, 0).GetText()
-        #print bug1uuid, bug2uuid
-        #bug1 = self.bugdir.get_bug(bug1uuid)
-        #bug2 = self.bugdir.get_bug(bug1uuid)
-        print self.bugcmp_fn(bug1,bug2)
-        return self.bugcmp_fn(bug1,bug2)
-    def OnColumnClick(self, event):
-        """Resort bug list depending on which column was clicked"""
-        print "TODO: sort by column %d" % event.Column
-        # change self.bugcmp_fn and resort, but I can't get it working
-
-app = MyApp()
-app.MainLoop()
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt
deleted file mode 100644 (file)
index def18b1..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-README.txt
-setup.py
-start-beweb.py
-Bugs-Everywhere-Web.egg-info/PKG-INFO
-Bugs-Everywhere-Web.egg-info/SOURCES.txt
-Bugs-Everywhere-Web.egg-info/not-zip-safe
-Bugs-Everywhere-Web.egg-info/requires.txt
-Bugs-Everywhere-Web.egg-info/sqlobject.txt
-Bugs-Everywhere-Web.egg-info/top_level.txt
-beweb/__init__.py
-beweb/config.py
-beweb/controllers.py
-beweb/formatting.py
-beweb/model.py
-beweb/prest.py
-beweb/release.py
-beweb/config/__init__.py
-beweb/templates/__init__.py
-beweb/tests/__init__.py
-beweb/tests/test_controllers.py
-beweb/tests/test_model.py
-libbe/__init__.py
-libbe/arch.py
-libbe/bugdir.py
-libbe/bzr.py
-libbe/cmdutil.py
-libbe/config.py
-libbe/diff.py
-libbe/mapfile.py
-libbe/names.py
-libbe/no_rcs.py
-libbe/plugin.py
-libbe/rcs.py
-libbe/restconvert.py
-libbe/tests.py
-libbe/utility.py
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/not-zip-safe b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/not-zip-safe
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt
deleted file mode 100644 (file)
index 88b15cb..0000000
+++ /dev/null
@@ -1 +0,0 @@
-TurboGears >= 0.9a4
\ No newline at end of file
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt
deleted file mode 100644 (file)
index 7f7cbad..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-db_module=beweb.model
-history_dir=$base/beweb/sqlobject-history
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt
deleted file mode 100644 (file)
index 6455be9..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-beweb
-libbe
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/PKG-INFO b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/PKG-INFO
deleted file mode 100644 (file)
index 6cb6ad2..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-Metadata-Version: 1.0
-Name: Bugs-Everywhere-Web
-Version: 1.0
-Summary: UNKNOWN
-Home-page: UNKNOWN
-Author: UNKNOWN
-Author-email: UNKNOWN
-License: UNKNOWN
-Description: UNKNOWN
-Platform: UNKNOWN
-Classifier: Development Status :: 3 - Alpha
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Classifier: Framework :: TurboGears
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/SOURCES.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/SOURCES.txt
deleted file mode 100644 (file)
index ab62ee4..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-README.txt
-setup.py
-start-beweb.py
-Bugs_Everywhere_Web.egg-info/PKG-INFO
-Bugs_Everywhere_Web.egg-info/SOURCES.txt
-Bugs_Everywhere_Web.egg-info/dependency_links.txt
-Bugs_Everywhere_Web.egg-info/not-zip-safe
-Bugs_Everywhere_Web.egg-info/paster_plugins.txt
-Bugs_Everywhere_Web.egg-info/requires.txt
-Bugs_Everywhere_Web.egg-info/sqlobject.txt
-Bugs_Everywhere_Web.egg-info/top_level.txt
-Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt
-Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/not-zip-safe
-Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt
-Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt
-Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt
-beweb/__init__.py
-beweb/config.py
-beweb/controllers.py
-beweb/formatting.py
-beweb/json.py
-beweb/model.py
-beweb/prest.py
-beweb/release.py
-beweb/config/__init__.py
-beweb/templates/__init__.py
-beweb/tests/__init__.py
-beweb/tests/test_controllers.py
-beweb/tests/test_model.py
-libbe/__init__.py
-libbe/arch.py
-libbe/bugdir.py
-libbe/bzr.py
-libbe/cmdutil.py
-libbe/config.py
-libbe/diff.py
-libbe/mapfile.py
-libbe/names.py
-libbe/no_rcs.py
-libbe/plugin.py
-libbe/rcs.py
-libbe/restconvert.py
-libbe/tests.py
-libbe/utility.py
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/dependency_links.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/dependency_links.txt
deleted file mode 100644 (file)
index 8b13789..0000000
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/not-zip-safe b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/not-zip-safe
deleted file mode 100644 (file)
index 8b13789..0000000
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/paster_plugins.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/paster_plugins.txt
deleted file mode 100644 (file)
index 14fec70..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-TurboGears
-PasteScript
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/requires.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/requires.txt
deleted file mode 100644 (file)
index 5fd6f71..0000000
+++ /dev/null
@@ -1 +0,0 @@
-TurboGears >= 1.0b1
\ No newline at end of file
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/sqlobject.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/sqlobject.txt
deleted file mode 100644 (file)
index 7f7cbad..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-db_module=beweb.model
-history_dir=$base/beweb/sqlobject-history
diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/top_level.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/top_level.txt
deleted file mode 100644 (file)
index 74a8358..0000000
+++ /dev/null
@@ -1 +0,0 @@
-beweb
diff --git a/interfaces/web/Bugs-Everywhere-Web/README.txt b/interfaces/web/Bugs-Everywhere-Web/README.txt
deleted file mode 100644 (file)
index 10774df..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-Bugs-Everywhere-Web
-
-This is a TurboGears (http://www.turbogears.org) project. It can be
-started by running the start-beweb.py script.
-
-Configure by creating an appropriate beweb/config.py from
-beweb/config.py.example.  The server will edit the repositories that
-it manages, so you should probably have it running on a seperate
-branch than your working repository.  You can then merge/push
-as you require to keep the branches in sync.
-
-See
-  http://docs.turbogears.org/1.0/Configuration
-For standard turbogears configuration information.
-
-Currently, you need to login for any methods with a
-@identity.require() decorator.  The only group in the current
-implementation is 'editbugs'.  Basically, anyone can browse around,
-but only registered 'editbugs' members can change things.
-
-Anonymous actions:
- * See project tree
- * See buglist
- * See comments
-Editbugs required actions:
- * Create new comments
- * Reply to comments
- * Update comment info
-
-
-All login attempts will fail unless you have added some valid users. See
-  http://docs.turbogears.org/1.0/GettingStartedWithIdentity
-For a good intro.  For the impatient, try something like
-  Bugs-Everywhere-Web$ tg-admin toolbox
-  browse to 'CatWalk' -> 'User' -> 'Add User+'
-or
-  Bugs-Everywhere-Web$ tg-admin sholl
-  >>> u = User(user_name=u'jdoe', email_address=u'jdoe@example.com',
-      display_name=u'Jane Doe', password=u'xxx')
-  >>> g = Group(group_name=u'editbugs', display_name=u'Edit Bugs')
-  >>> g.addUser(u)           # BE-Web uses SQLObject
-Exit the tg-admin shell with Ctrl-Z on MS Windows, Ctrl-D on other systems.
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/__init__.py b/interfaces/web/Bugs-Everywhere-Web/beweb/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/app.cfg b/interfaces/web/Bugs-Everywhere-Web/beweb/app.cfg
deleted file mode 100644 (file)
index 024fa8a..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-[global]
-# The settings in this file should not vary depending on the deployment
-# environment. devcfg.py and prodcfg.py are the locations for
-# the different deployment settings. Settings in this file will
-# be overridden by settings in those other files.
-
-# The commented out values below are the defaults
-
-# VIEW
-
-# which view (template engine) to use if one is not specified in the
-# template name
-# tg.defaultview = "kid"
-
-# kid.outputformat="html"
-# kid.encoding="utf-8"
-
-# The sitetemplate is used for overall styling of a site that
-# includes multiple TurboGears applications
-# tg.sitetemplate="<packagename.templates.templatename>"
-
-# Allow every exposed function to be called as json,
-# tg.allow_json = False
-
-# Set to True if you'd like all of your pages to include MochiKit
-# tg.mochikit_all = False
-
-# VISIT TRACKING
-# Each visit to your application will be assigned a unique visit ID tracked via
-# a cookie sent to the visitor's browser.
-# --------------
-
-# Enable Visit tracking
-visit.on=True
-
-# Number of minutes a visit may be idle before it expires.
-# visit.timeout=20
-
-# The name of the cookie to transmit to the visitor's browser.
-# visit.cookie.name="tg-visit"
-
-# Domain name to specify when setting the cookie (must begin with . according to
-# RFC 2109). The default (None) should work for most cases and will default to
-# the machine to which the request was made. NOTE: localhost is NEVER a valid
-# value and will NOT WORK.
-# visit.cookie.domain=None
-
-# Specific path for the cookie
-# visit.cookie.path="/"
-
-# The name of the VisitManager plugin to use for visitor tracking.
-# visit.manager="sqlobject"
-
-
-# IDENTITY
-# General configuration of the TurboGears Identity management module
-# --------
-
-# Switch to turn on or off the Identity management module
-identity.on=True
-
-# [REQUIRED] URL to which CherryPy will internally redirect when an access
-# control check fails. If Identity management is turned on, a value for this
-# option must be specified.
-identity.failure_url="/login"
-
-# The IdentityProvider to use -- defaults to the SqlObjectIdentityProvider which
-# pulls User, Group, and Permission data out of your model database.
-identity.provider="sqlobject"
-
-# The names of the fields on the login form containing the visitor's user ID
-# and password. In addition, the submit button is specified simply so its
-# existence may be stripped out prior to passing the form data to the target
-# controller.
-identity.form.user_name="user_name"
-identity.form.password="password"
-identity.form.submit="login"
-
-# What sources should the identity provider consider when determining the
-# identity associated with a request? Comma separated list of identity sources.
-# Valid sources: form, visit, http_auth
-identity.source="form,http_auth,visit"
-
-
-# SqlObjectIdentityProvider
-# Configuration options for the default IdentityProvider
-# -------------------------
-
-# The classes you wish to use for your Identity model. Leave these commented out
-# to use the default classes for SqlObjectIdentityProvider. Or set them to the
-# classes in your model. NOTE: These aren't TG_* because the TG prefix is
-# reserved for classes created by TurboGears.
-# identity.soprovider.model.user="beweb.model.User"
-# identity.soprovider.model.group="beweb.model.Group"
-# identity.soprovider.model.permission="beweb.model.Permission"
-
-# The password encryption algorithm used when comparing passwords against what's
-# stored in the database. Valid values are 'md5' or 'sha1'. If you do not
-# specify an encryption algorithm, passwords are expected to be clear text.
-#
-# The SqlObjectProvider *will* encrypt passwords supplied as part of your login
-# form.  If you set the password through the password property, like:
-# my_user.password = 'secret'
-# the password will be encrypted in the database, provided identity is up and 
-# running, or you have loaded the configuration specifying what encryption to
-# use (in situations where identity may not yet be running, like tests).
-
-# identity.soprovider.encryption_algorithm=None
-
-[/static]
-static_filter.on = True
-static_filter.dir = "."
-
-[/favicon.ico]
-static_filter.on = True
-static_filter.file = "images/favicon.ico"
-
-[/]
-decodingFilter.on = True
-static_filter.root = '%(package_dir)s/static'
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/config.py.example b/interfaces/web/Bugs-Everywhere-Web/beweb/config.py.example
deleted file mode 100644 (file)
index 8745c6d..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-# This is an example beweb configuration file.
-
-# One thing we need is a map of projects.  Projects have a beweb ID, a path,
-# and a display name.
-
-# In this example, the 'be' beweb ID is assigned the display name "Bugs 
-# Everywhere" and the path "/home/abentley/be"
-
-projects = {"be": ("Bugs Everywhere","/home/abentley/be"),
-           }
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/config/app.cfg b/interfaces/web/Bugs-Everywhere-Web/beweb/config/app.cfg
deleted file mode 100644 (file)
index 15555b7..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-[global]
-# The settings in this file should not vary depending on the deployment
-# environment. dev.cfg and prod.cfg are the locations for
-# the different deployment settings. Settings in this file will
-# be overridden by settings in those other files.
-
-# The commented out values below are the defaults
-
-# VIEW
-
-# which view (template engine) to use if one is not specified in the
-# template name
-# tg.defaultview = "kid"
-
-# The following kid settings determine the settings used by the kid serializer.
-
-# One of (html|xml|json)
-# kid.outputformat="html"
-
-# kid.encoding="utf-8"
-
-# The sitetemplate is used for overall styling of a site that
-# includes multiple TurboGears applications
-# tg.sitetemplate="<packagename.templates.templatename>"
-
-# Allow every exposed function to be called as json,
-# tg.allow_json = False
-
-# List of Widgets to include on every page.
-# for exemple ['turbogears.mochikit']
-# tg.include_widgets = []
-
-# Set to True if the scheduler should be started
-# tg.scheduler = False
-
-# IDENTITY
-# General configuration of the TurboGears Identity management module
-# --------
-
-# Switch to turn on or off the Identity management module
-identity.on=True
-
-# [REQUIRED] URL to which CherryPy will internally redirect when an access
-# control check fails. If Identity management is turned on, a value for this
-# option must be specified.
-identity.failure_url="/login"
-
-# identity.provider='sqlobject'
-
-# The names of the fields on the login form containing the visitor's user ID
-# and password. In addition, the submit button is specified simply so its
-# existence may be stripped out prior to passing the form data to the target
-# controller.
-# identity.form.user_name="user_name"
-# identity.form.password="password"
-# identity.form.submit="login"
-
-# What sources should the identity provider consider when determining the
-# identity associated with a request? Comma separated list of identity sources.
-# Valid sources: form, visit, http_auth
-# identity.source="form,http_auth,visit"
-
-# SqlObjectIdentityProvider
-# Configuration options for the default IdentityProvider
-# -------------------------
-
-# The classes you wish to use for your Identity model. Remember to not use reserved
-# SQL keywords for class names (at least unless you specify a different table
-# name using sqlmeta).
-identity.soprovider.model.user="stfa.model.User"
-identity.soprovider.model.group="stfa.model.Group"
-identity.soprovider.model.permission="stfa.model.Permission"
-
-# The password encryption algorithm used when comparing passwords against what's
-# stored in the database. Valid values are 'md5' or 'sha1'. If you do not
-# specify an encryption algorithm, passwords are expected to be clear text.
-# The SqlObjectProvider *will* encrypt passwords supplied as part of your login
-# form.  If you set the password through the password property, like:
-# my_user.password = 'secret'
-# the password will be encrypted in the database, provided identity is up and
-# running, or you have loaded the configuration specifying what encryption to
-# use (in situations where identity may not yet be running, like tests).
-
-# identity.soprovider.encryption_algorithm=None
-
-[/static]
-static_filter.on = True
-static_filter.dir = "%(top_level_dir)s/static"
-
-[/favicon.ico]
-static_filter.on = True
-static_filter.file = "%(top_level_dir)s/static/images/favicon.ico"
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/config/log.cfg b/interfaces/web/Bugs-Everywhere-Web/beweb/config/log.cfg
deleted file mode 100644 (file)
index ce776f8..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-# LOGGING
-# Logging is often deployment specific, but some handlers and
-# formatters can be defined here.
-
-[logging]
-[[formatters]]
-[[[message_only]]]
-format='*(message)s'
-
-[[[full_content]]]
-format='*(asctime)s *(name)s *(levelname)s *(message)s'
-
-[[handlers]]
-[[[debug_out]]]
-class='StreamHandler'
-level='DEBUG'
-args='(sys.stdout,)'
-formatter='full_content'
-
-[[[access_out]]]
-class='StreamHandler'
-level='INFO'
-args='(sys.stdout,)'
-formatter='message_only'
-
-[[[error_out]]]
-class='StreamHandler'
-level='ERROR'
-args='(sys.stdout,)'
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py b/interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py
deleted file mode 100644 (file)
index 50cc754..0000000
+++ /dev/null
@@ -1,240 +0,0 @@
-import logging
-
-import cherrypy
-import turbogears
-from turbogears import controllers, expose, validate, redirect, identity
-
-from libbe.bugdir import tree_root, NoRootEntry
-from config import projects
-from prest import PrestHandler, provide_action
-
-
-from beweb import json
-
-log = logging.getLogger("beweb.controllers")
-
-def project_tree(project):
-    try:
-        return tree_root(projects[project][1])
-    except KeyError:
-        raise Exception("Unknown project %s" % project)
-
-def comment_url(project, bug, comment, **kwargs):
-    return turbogears.url("/project/%s/bug/%s/comment/%s" %
-                          (project, bug, comment), kwargs)
-
-class Comment(PrestHandler):
-    @identity.require( identity.has_permission("editbugs"))
-    @provide_action("action", "New comment")
-    def new_comment(self, comment_data, comment, *args, **kwargs):
-        bug_tree = project_tree(comment_data['project'])
-        bug = bug_tree.get_bug(comment_data['bug'])
-        comment = new_comment(bug, "")
-        comment.From = identity.current.user.userId
-        comment.content_type = "text/restructured"
-        comment.save()
-        raise cherrypy.HTTPRedirect(comment_url(comment=comment.uuid, 
-                                    **comment_data))
-
-    @identity.require( identity.has_permission("editbugs"))
-    @provide_action("action", "Reply")
-    def reply_comment(self, comment_data, comment, *args, **kwargs):
-        bug_tree = project_tree(comment_data['project'])
-        bug = bug_tree.get_bug(comment_data['bug'])
-        reply_comment = new_comment(bug, "")
-        reply_comment.From = identity.current.user.userId
-        reply_comment.in_reply_to = comment.uuid
-        reply_comment.save()
-        reply_data = dict(comment_data)
-        del reply_data["comment"]
-        raise cherrypy.HTTPRedirect(comment_url(comment=reply_comment.uuid, 
-                                    **reply_data))
-
-    @identity.require( identity.has_permission("editbugs"))
-    @provide_action("action", "Update")
-    def update(self, comment_data, comment, comment_body, *args, **kwargs):
-        comment.body = comment_body
-        comment.save()
-        raise cherrypy.HTTPRedirect(bug_url(comment_data['project'], 
-                                            comment_data['bug']))
-
-    def instantiate(self, project, bug, comment):
-        bug_tree = project_tree(project)
-        bug = bug_tree.get_bug(bug)
-        return bug.get_comment(comment)
-
-    def dispatch(self, comment_data, comment, *args, **kwargs):
-        return self.edit_comment(comment_data['project'], comment)
-
-    @turbogears.expose(html="beweb.templates.edit_comment")
-    def edit_comment(self, project, comment):
-        return {"comment": comment, "project_id": project}
-
-class Bug(PrestHandler):
-    comment = Comment()
-    @turbogears.expose(html="beweb.templates.edit_bug")
-    def index(self, project, bug):
-        return {"bug": bug, "project_id": project}
-    
-    def dispatch(self, bug_data, bug, *args, **kwargs):
-        if bug is None:
-            return self.list(bug_data['project'], **kwargs)
-        else:
-            return self.index(bug_data['project'], bug)
-
-    @turbogears.expose(html="beweb.templates.bugs")
-    def list(self, project, sort_by=None, show_closed=False, action=None, 
-             search=None):
-        if action == "New bug":
-            self.new_bug()
-        if show_closed == "False":
-            show_closed = False
-        bug_tree = project_tree(project)
-        bugs = list(bug_tree.list())
-        if sort_by is None:
-            bugs.sort()
-        return {"project_id"      : project,
-                "project_name"    : projects[project][0],
-                "bugs"            : bugs,
-                "show_closed"     : show_closed,
-                "search"          : search,
-               }
-
-    @identity.require( identity.has_permission("editbugs"))
-    @provide_action("action", "New bug")
-    def new_bug(self, bug_data, bug, **kwargs):
-        bug = project_tree(bug_data['project']).new_bug()
-        bug.creator = identity.current.user.userId
-        bug.save()
-        raise cherrypy.HTTPRedirect(bug_url(bug_data['project'], bug.uuid))
-
-    @identity.require( identity.has_permission("editbugs"))
-    @provide_action("action", "Update")
-    def update(self, bug_data, bug, status, severity, summary, assigned, 
-               action):
-        bug.status = status
-        bug.severity = severity
-        bug.summary = summary
-        if assigned == "":
-            assigned = None
-        bug.assigned = assigned
-        bug.save()
-#        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):
-        return project_tree(project).get_bug(bug)
-
-    @provide_action("action", "New comment")
-    def new_comment(self, bug_data, bug, *args, **kwargs):
-        try:
-            self.update(bug_data, bug, *args, **kwargs)
-        except cherrypy.HTTPRedirect:
-            pass
-        return self.comment.new_comment(bug_data, comment=None, *args, 
-                                         **kwargs)
-
-
-def project_url(project_id=None):
-    project_url = "/project/"
-    if project_id is not None:
-        project_url += "%s/" % project_id
-    return turbogears.url(project_url)
-
-def bug_url(project_id, bug_uuid=None):
-    bug_url = "/project/%s/bug/" % project_id
-    if bug_uuid is not None:
-        bug_url += "%s/" % bug_uuid
-    return turbogears.url(bug_url)
-
-def bug_list_url(project_id, show_closed=False, search=None):
-    bug_url = "/project/%s/bug/?show_closed=%s" % (project_id, 
-                                                   str(show_closed))
-    if search is not None:
-        bug_url = "%s&search=%s" % (bug_url, search)
-    return turbogears.url(str(bug_url))
-
-
-class Project(PrestHandler):
-    bug = Bug()
-    @turbogears.expose(html="beweb.templates.projects")
-    def dispatch(self, project_data, project, *args, **kwargs):
-        if project is not None:
-            raise cherrypy.HTTPRedirect(bug_url(project)) 
-        else:
-            return {"projects": projects}
-
-    def instantiate(self, project):
-        return project
-
-
-class Root(controllers.Root):
-    prest = PrestHandler()
-    prest.project = Project()
-    @turbogears.expose()
-    def index(self):
-        raise cherrypy.HTTPRedirect(project_url()) 
-
-    @expose(template="beweb.templates.login")
-    def login(self, forward_url=None, previous_url=None, *args, **kw):
-
-        if not identity.current.anonymous and identity.was_login_attempted():
-            raise redirect(forward_url)
-
-        forward_url=None
-        previous_url= cherrypy.request.path
-
-        if identity.was_login_attempted():
-            msg=_("The credentials you supplied were not correct or "\
-                   "did not grant access to this resource.")
-        elif identity.get_identity_errors():
-            msg=_("You must provide your credentials before accessing "\
-                   "this resource.")
-        else:
-            msg=_("Please log in.")
-            forward_url= cherrypy.request.headers.get("Referer", "/")
-        cherrypy.response.status=403
-        return dict(message=msg, previous_url=previous_url, logging_in=True,
-                    original_parameters=cherrypy.request.params,
-                    forward_url=forward_url)
-
-    @expose()
-    def logout(self):
-        identity.current.logout()
-        raise redirect("/")
-
-    @turbogears.expose('beweb.templates.about')
-    def about(self, *paths, **kwargs):
-        return {}
-
-    @turbogears.expose()
-    def default(self, *args, **kwargs):
-        return self.prest.default(*args, **kwargs)
-
-    def _cp_on_error(self):
-        import traceback, StringIO
-        bodyFile = StringIO.StringIO()
-        traceback.print_exc(file = bodyFile)
-        trace_text = bodyFile.getvalue()
-        try:
-            raise
-        except cherrypy.NotFound:
-            self.handle_error('Not Found', str(e), trace_text, '404 Not Found')
-
-        except NoRootEntry, e:
-            self.handle_error('Project Misconfiguration', str(e), trace_text)
-
-        except Exception, e:
-            self.handle_error('Internal server error', str(e), trace_text)
-
-    def handle_error(self, heading, body, traceback=None, 
-                     status='500 Internal Server Error'):
-        cherrypy.response.headerMap['Status'] = status 
-        cherrypy.response.body = [self.errorpage(heading, body, traceback)]
-        
-
-    @turbogears.expose(html='beweb.templates.error')
-    def errorpage(self, heading, body, traceback):
-        return {'heading': heading, 'body': body, 'traceback': traceback}
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/formatting.py b/interfaces/web/Bugs-Everywhere-Web/beweb/formatting.py
deleted file mode 100644 (file)
index 1278414..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-from StringIO import StringIO
-
-try :
-   from xml.etree.ElementTree import XML # Python 2.5 (and greater?)
-except ImportError :
-   from elementtree.ElementTree import XML
-from libbe.restconvert import rest_xml
-
-def to_unix(text):
-   skip_newline = False
-   for ch in text:
-      if ch not in ('\r', '\n'):
-         yield ch
-      else:
-         if ch == '\n':
-            if skip_newline:
-               continue
-         else:
-            skip_newline = True
-         yield '\n'
-
-
-def soft_text(text):
-   first_space = False
-   translations = {'\n': '<br />\n', '&': '&amp;', '\x3c': '&lt;', 
-                   '\x3e': '&gt;'}
-   for ch in to_unix(text):
-      if ch == ' ' and first_space is True:
-            yield '&#160;'
-      first_space = ch in (' ')
-      try:
-         yield translations[ch]
-      except KeyError:
-         yield ch
-
-
-def soft_pre(text):
-   return XML('<div style="font-family: monospace">'+
-              ''.join(soft_text(text)).encode('utf-8')+'</div>') 
-
-
-def get_rest_body(rest):
-    xml, warnings = rest_xml(StringIO(rest))
-    return xml.find('{http://www.w3.org/1999/xhtml}body'), warnings
-
-def comment_body_xhtml(comment):
-    if comment.content_type == "text/restructured":
-        return get_rest_body(comment.body)[0]
-    else:
-        return soft_pre(comment.body)
-
-
-def select_among(name, options, default, display_names=None):
-    output = ['<select name="%s">' % name]
-    for option in options:
-        if option == default:
-            selected = ' selected="selected"'
-        else:
-            selected = ""
-        if display_names is None:
-            display_name = None
-        else:
-            display_name = display_names.get(option)
-
-        if option is None:
-            option = ""
-        if display_name is None:
-            display_name = option
-            value = ""
-        else:
-            value = ' value="%s"' % option
-        output.append("<option%s%s>%s</option>" % (selected, value, 
-                                                   display_name))
-    output.append("</select>")
-    return XML("".join(output))
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/json.py b/interfaces/web/Bugs-Everywhere-Web/beweb/json.py
deleted file mode 100644 (file)
index 6e100c3..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-# This module provides helper functions for the JSON part of your
-# view, if you are providing a JSON-based API for your app.
-
-# Here's what most rules would look like:
-# @jsonify.when("isinstance(obj, YourClass)")
-# def jsonify_yourclass(obj):
-#     return [obj.val1, obj.val2]
-#
-# The goal is to break your objects down into simple values:
-# lists, dicts, numbers and strings
-
-from turbojson.jsonify import jsonify
-
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/model.py b/interfaces/web/Bugs-Everywhere-Web/beweb/model.py
deleted file mode 100644 (file)
index aa4b6b6..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-from datetime import datetime
-
-from sqlobject import *
-from turbogears.database import PackageHub
-from turbogears import identity
-
-hub = PackageHub("beweb")
-__connection__ = hub
-
-class Visit(SQLObject):
-    class sqlmeta:
-        table = "visit"
-
-    visit_key = StringCol(length=40, alternateID=True,
-                          alternateMethodName="by_visit_key")
-    created = DateTimeCol(default=datetime.now)
-    expiry = DateTimeCol()
-
-    def lookup_visit(cls, visit_key):
-        try:
-            return cls.by_visit_key(visit_key)
-        except SQLObjectNotFound:
-            return None
-    lookup_visit = classmethod(lookup_visit)
-
-class VisitIdentity(SQLObject):
-    visit_key = StringCol(length=40, alternateID=True,
-                          alternateMethodName="by_visit_key")
-    user_id = IntCol()
-
-
-class Group(SQLObject):
-    """
-    An ultra-simple group definition.
-    """
-
-    # names like "Group", "Order" and "User" are reserved words in SQL
-    # so we set the name to something safe for SQL
-    class sqlmeta:
-        table = "tg_group"
-
-    group_name = UnicodeCol(length=16, alternateID=True,
-                            alternateMethodName="by_group_name")
-    display_name = UnicodeCol(length=255)
-    created = DateTimeCol(default=datetime.now)
-
-    # collection of all users belonging to this group
-    users = RelatedJoin("User", intermediateTable="user_group",
-                        joinColumn="group_id", otherColumn="user_id")
-
-    # collection of all permissions for this group
-    permissions = RelatedJoin("Permission", joinColumn="group_id", 
-                              intermediateTable="group_permission",
-                              otherColumn="permission_id")
-
-
-class User(SQLObject):
-    """
-    Reasonably basic User definition. Probably would want additional attributes.
-    """
-    # names like "Group", "Order" and "User" are reserved words in SQL
-    # so we set the name to something safe for SQL
-    class sqlmeta:
-        table = "tg_user"
-
-    child_name = UnicodeCol(length=255)
-    user_name = UnicodeCol(length=16, alternateID=True,
-                           alternateMethodName="by_user_name")
-    email_address = UnicodeCol(length=255, alternateID=True,
-                               alternateMethodName="by_email_address")
-    display_name = UnicodeCol(length=255)
-    password = UnicodeCol(length=40)
-    created = DateTimeCol(default=datetime.now)
-
-    # groups this user belongs to
-    groups = RelatedJoin("Group", intermediateTable="user_group",
-                         joinColumn="user_id", otherColumn="group_id")
-
-    def _get_permissions(self):
-        perms = set()
-        for g in self.groups:
-            perms = perms | set(g.permissions)
-        return perms
-
-    def _set_password(self, cleartext_password):
-        "Runs cleartext_password through the hash algorithm before saving."
-        hash = identity.encrypt_password(cleartext_password)
-        self._SO_set_password(hash)
-
-    def set_password_raw(self, password):
-        "Saves the password as-is to the database."
-        self._SO_set_password(password)
-
-
-
-class Permission(SQLObject):
-    permission_name = UnicodeCol(length=16, alternateID=True,
-                                 alternateMethodName="by_permission_name")
-    description = UnicodeCol(length=255)
-
-    groups = RelatedJoin("Group",
-                        intermediateTable="group_permission",
-                         joinColumn="permission_id", 
-                         otherColumn="group_id")
-
-def people_map():
-    return dict((u.user_name, u.display_name) for u in User.select())
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/prest.py b/interfaces/web/Bugs-Everywhere-Web/beweb/prest.py
deleted file mode 100644 (file)
index 9a6505d..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-from unittest import TestCase
-import unittest
-from cherrypy import NotFound
-"""A pseudo-REST dispatching method in which only the noun comes from the path.
-The action performed will depend on kwargs.
-"""
-
-class AmbiguousAction(Exception):
-    def __init__(self, actions):
-        Exception.__init__(self, "Supplied action is ambiguous.")
-        self.actions = actions
-    
-
-def provide_action(name, value):
-    def provider(func):
-        func._action_desc = (name, value)
-        return func
-    return provider
-
-class PrestHandler(object):
-    def __init__(self):
-        object.__init__(self)
-        self.actions = {}
-        for member in (getattr(self, m) for m in dir(self)):
-            if not hasattr(member, '_action_desc'):
-                continue
-            name, value = member._action_desc
-            if name not in self.actions:
-                self.actions[name] = {}
-            self.actions[name][value] = member
-
-    @classmethod
-    def add_action(klass, name, value, function):
-        if name not in klass.actions:
-            klass.actions[name] = {}
-        klass.actions[name][value] = function
-
-
-    def decode(self, path, data=None):
-        """Convert the path into a handler, a resource, data, and extra_path"""
-        if data is None:
-            data = {}
-        if len(path) < 2 or not (hasattr(self, path[1])):
-            if len(path) == 0:
-                resource = None
-            else:
-                try:
-                    resource = self.instantiate(**data)
-                except NotImplementedError, e:
-                    if e.args[0] is not PrestHandler.instantiate:
-                        raise NotFound()
-
-            return self, resource, data, path[1:] 
-        if len(path) > 2:
-            data[path[1]] = path[2]
-        return getattr(self, path[1]).decode(path[2:], data)
-
-    def instantiate(self, **date):
-        raise NotImplementedError(PrestHandler.instantiate)
-
-    def default(self, *args, **kwargs):
-        child, resource, data, extra = self.decode([None,] + list(args))
-        action = child.get_action(**kwargs)
-        new_args = ([data, resource]+extra)
-        if action is not None:
-            return action(*new_args, **kwargs)
-        else:
-            return child.dispatch(*new_args, **kwargs)
-
-    def get_action(self, **kwargs):
-        """Return the action requested by kwargs, if any.
-        
-        Raises AmbiguousAction if more than one action matches.
-        """
-        actions = []
-        for key in kwargs:
-            if key in self.actions:
-                if kwargs[key] in self.actions[key]:
-                    actions.append(self.actions[key][kwargs[key]])
-        if len(actions) == 0:
-            return None
-        elif len(actions) == 1:
-            return actions[0]
-        else:
-            raise AmbiguousAction(actions)
-
-
-class PrestTester(TestCase):
-    def test_decode(self):
-        class ProjectHandler(PrestHandler):
-            actions = {}
-            def dispatch(self, project_data, project, *args, **kwargs):
-                self.project_id = project_data['project']
-                self.project_data = project_data
-                self.resource = project
-                self.args = args
-                self.kwargs = kwargs
-
-            def instantiate(self, project):
-                return [project]
-
-            @provide_action('action', 'Save')
-            def save(self, project_data, project, *args, **kwargs):
-                self.action = "save"
-
-            @provide_action('behavior', 'Update')
-            def update(self, project_data, project, *args, **kwargs):
-                self.action = "update"
-            
-        foo = PrestHandler()
-        foo.project = ProjectHandler()
-        handler, resource, data, extra = foo.decode([None, 'project', '83', 
-                                                     'bloop', 'yeah'])
-        assert handler is foo.project
-        self.assertEqual({'project': '83'}, data)
-        self.assertEqual(['bloop', 'yeah'], extra)
-        foo.default(*['project', '27', 'extra'], **{'a':'b', 'b':'97'})
-        self.assertEqual(foo.project.args, ('extra',))
-        self.assertEqual(foo.project.kwargs, {'a':'b', 'b':'97'})
-        self.assertEqual(foo.project.project_data, {'project': '27'})
-        self.assertEqual(foo.project.resource, ['27'])
-        foo.default(*['project', '27', 'extra'], **{'action':'Save', 'b':'97'})
-        self.assertEqual(foo.project.action, 'save')
-        foo.default(*['project', '27', 'extra'], 
-                    **{'behavior':'Update', 'b':'97'})
-        self.assertEqual(foo.project.action, 'update')
-        self.assertRaises(AmbiguousAction, foo.default, 
-                          *['project', '27', 'extra'], 
-                          **{'behavior':'Update', 'action':'Save', 'b':'97'})
-                
-        class BugHandler(PrestHandler):
-            actions = {}
-            def dispatch(self, bug_data, bug, *args, **kwargs):
-                self.project_id = project_data['project']
-                self.project_data = project_data
-                self.resource = project
-                self.args = args
-                self.kwargs = kwargs
-
-            def instantiate(self, project, bug):
-                return [project, bug]
-
-            @provide_action('action', 'Save')
-            def save(self, project_data, project, *args, **kwargs):
-                self.action = "save"
-
-            @provide_action('behavior', 'Update')
-            def update(self, project_data, project, *args, **kwargs):
-                self.action = "update"
-
-        foo.project.bug = BugHandler()
-        handler, resource, data, extra = foo.decode([None, 'project', '83', 
-                                                     'bug', '92'])
-        assert handler is foo.project.bug
-        self.assertEqual(resource[0], '83')
-        self.assertEqual(resource[1], '92')
-        self.assertEqual([], extra)
-        self.assertEqual(data['project'], '83')
-        self.assertEqual(data['bug'], '92')
-
-def test():
-    patchesTestSuite = unittest.makeSuite(PrestTester,'test')
-    runner = unittest.TextTestRunner(verbosity=0)
-    return runner.run(patchesTestSuite)
-    
-
-if __name__ == "__main__":
-    test()
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/release.py b/interfaces/web/Bugs-Everywhere-Web/beweb/release.py
deleted file mode 100644 (file)
index 9d64bf7..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-# Release information about Bugs-Everywhere-Web
-
-version = "1.0"
-
-# description = "Your plan to rule the world"
-# long_description = "More description about your plan"
-# author = "Your Name Here"
-# email = "YourEmail@YourDomain"
-# copyright = "Vintage 2006 - a good year indeed"
-
-# if it's open source, you might want to specify these
-# url = "http://yourcool.site/"
-# download_url = "http://yourcool.site/download"
-# license = "MIT"
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/css/style.css b/interfaces/web/Bugs-Everywhere-Web/beweb/static/css/style.css
deleted file mode 100644 (file)
index 6fe197f..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-table\r
-{\r
-    background-color: black;\r
-}\r
-td\r
-{\r
-    background-color: white;\r
-}\r
-h1\r
-{\r
-    font-family: "Verdana";\r
-    font-weight: bold;\r
-    font-size: 120%;\r
-    margin-bottom:0;\r
-    color: #990;\r
-}\r
-\r
-tr.closed td\r
-{\r
-    background-color: #ccc;\r
-}\r
-tr.closedeven td\r
-{\r
-    background-color: #ccc;\r
-}\r
-tr.closedodd td\r
-{\r
-    background-color: #dda;\r
-}\r
-\r
-a:visited, a:link\r
-{\r
-    color: #990;\r
-    text-decoration: None;\r
-}\r
-td a:visited, td a:link\r
-{\r
-    display: block;\r
-}\r
-a:visited:hover, a:link:hover\r
-{\r
-    text-decoration: underline;\r
-}\r
-td a:visited:hover, td a:link:hover\r
-{\r
-    color:black;\r
-    background-color:#dda;\r
-    text-decoration: None;\r
-    display: block;\r
-}\r
-\r
-body\r
-{\r
-    font-family: "Verdana";\r
-    font-size:11pt;\r
-    background-color: white;\r
-}\r
-.comment\r
-{\r
-}\r
-.comment table\r
-{\r
-    background-color: transparent;\r
-}\r
-.comment td\r
-{\r
-    background-color: transparent;\r
-}\r
-.comment pre\r
-{\r
-    font-family: "Verdana";\r
-}\r
-#header\r
-{\r
-    color: black;\r
-    font-weight: bold;\r
-    background-image: url(/static/images/half-spiral.png);\r
-    background-position: right center;\r
-    background-repeat: no-repeat;\r
-    background-color: #ff0;\r
-}\r
-#header ul.navoption\r
-{\r
-    display: block;\r
-    float: right;\r
-    margin: 0;\r
-    padding-right: 30px;\r
-}\r
-#header li\r
-{\r
-    display: inline;\r
-    margin:0;\r
-    padding:0;\r
-}\r
-table.insetbox\r
-{\r
-  margin-top: 0.5em;\r
-  margin-bottom: 0.5em;\r
-}\r
-.insetbox tr, .insetbox td\r
-{\r
-  margin: 0;\r
-  padding: 0;\r
-}\r
-pre.traceback\r
-{\r
-  font-family: Verdana, Ariel, Helvetica, sanserif;\r
-}\r
-tr.even td\r
-{\r
-  background-color: #eee;\r
-}\r
-tr.odd td\r
-{\r
-  background-color: #ffe;\r
-}\r
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-b.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-b.png
deleted file mode 100644 (file)
index 790e438..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-b.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-bl.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-bl.png
deleted file mode 100644 (file)
index 5b43259..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-bl.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-br.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-br.png
deleted file mode 100644 (file)
index 6cfd62c..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-br.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-l.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-l.png
deleted file mode 100644 (file)
index a6ce3ce..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-l.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-r.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-r.png
deleted file mode 100644 (file)
index 1ffd6f8..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-r.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-t.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-t.png
deleted file mode 100644 (file)
index 0129b0c..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-t.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tl.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tl.png
deleted file mode 100644 (file)
index d616b77..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tl.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tr.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tr.png
deleted file mode 100644 (file)
index 18e542e..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tr.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-b.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-b.png
deleted file mode 100644 (file)
index 05a190e..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-b.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-r.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-r.png
deleted file mode 100644 (file)
index 0c3ea4c..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-r.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.ico b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.ico
deleted file mode 100644 (file)
index 339d09c..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.ico and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.png
deleted file mode 100644 (file)
index 6dc53ee..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/half-spiral.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/half-spiral.png
deleted file mode 100644 (file)
index cb4b56c..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/half-spiral.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/header_inner.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/header_inner.png
deleted file mode 100644 (file)
index 2b2d87d..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/header_inner.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/info.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/info.png
deleted file mode 100644 (file)
index 329c523..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/info.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-b.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-b.png
deleted file mode 100644 (file)
index 25d3cfa..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-b.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-bl.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-bl.png
deleted file mode 100644 (file)
index f496223..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-bl.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-br.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-br.png
deleted file mode 100644 (file)
index 74cbd91..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-br.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-l.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-l.png
deleted file mode 100644 (file)
index dd567fa..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-l.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-r.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-r.png
deleted file mode 100644 (file)
index 9ac4486..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-r.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-t.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-t.png
deleted file mode 100644 (file)
index fbb06c8..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-t.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tl.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tl.png
deleted file mode 100644 (file)
index 9336290..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tl.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tr.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tr.png
deleted file mode 100644 (file)
index de74808..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tr.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ok.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ok.png
deleted file mode 100644 (file)
index fee6751..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ok.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/shadows.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/shadows.png
deleted file mode 100644 (file)
index 9ddc676..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/shadows.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/spiral.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/spiral.png
deleted file mode 100644 (file)
index b4bcb1e..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/spiral.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/tg_under_the_hood.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/tg_under_the_hood.png
deleted file mode 100644 (file)
index bc9c79c..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/tg_under_the_hood.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/under_the_hood_blue.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/under_the_hood_blue.png
deleted file mode 100644 (file)
index 90e84b7..0000000
Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/under_the_hood_blue.png and /dev/null differ
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/__init__.py b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/about.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/about.kid
deleted file mode 100644 (file)
index fa3548a..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
-    py:extends="'master.kid'">
-<head>
-    <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
-    <title>About Bugs Everywhere</title>
-</head>
-
-<body>
-<h1>About Bugs Everywhere</h1>
-<p>Bugs Everywhere is a "distributed bugtracker", designed to complement distributed revision control systems.
-</p>
-<p>
-Bugs Everywhere was conceived and written by developers at <a href="http://panoramicfeedback.com/">Panoramic Feedback</a>, primarily Aaron Bentley. <a href="http://panoramicfeedback.com/">Panoramic Feedback</a> is no longer developing BE, and the current maintainer is <a href="http://bugseverywhere.org/be/show/ChrisBall">Chris Ball</a>.
-</p>
-<p>
-    Bugs Everywhere <a href="http://bugseverywhere.org/">web site</a>
-</p>
-<a href="/">Project List</a>
-</body>
-</html>
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/bugs.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/bugs.kid
deleted file mode 100644 (file)
index 198aa94..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<?python
-from libbe.names import unique_name
-from beweb.controllers import bug_url, project_url, bug_list_url
-from beweb.model import people_map
-people = people_map()
-def row_class(bug, num):
-    if not bug.active is True:
-        extra = "closed"
-    else:
-        extra = ""
-    if num % 2 == 0:
-        return extra+"even"
-    else:
-        return extra+"odd"
-
-
-def match(bug, show_closed, search):
-    if not show_closed and not bug.active:
-        return False
-    elif search is None:
-        return True
-    else:
-        return search.lower() in bug.summary.lower()
-?>
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
-    py:extends="'master.kid'">
-
-<head>
-    <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
-    <title>Bugs for $project_name</title>
-</head>
-
-<body>
-<h1>Bug list for ${project_name}</h1>
-<table>
-<tr><td>ID</td><td>Status</td><td>Severity</td><td>Assigned To</td><td>Comments</td><td>Summary</td></tr>
-<div py:for="num, bug in enumerate([b for b in bugs if match(b, show_closed, search)])" py:strip="True"><tr class="${row_class(bug, num)}"><td><a href="${bug_url(project_id, bug.uuid)}">${unique_name(bug, bugs[:])}</a></td><td>${bug.status}</td><td>${bug.severity}</td><td>${people.get(bug.assigned, bug.assigned)}</td><td>${len(list(bug.iter_comment_ids()))}</td><td>${bug.summary}</td></tr>
-</div>
-</table>
-<a href="${project_url()}">Project list</a>
-<a href="${bug_list_url(project_id, not show_closed, search)}">Toggle closed</a>
-<form action="${bug_list_url(project_id)}" method="post">
-<input type="submit" name="action" value="New bug"/>
-</form>
-<form action="${bug_list_url(project_id)}" method="get">
-<input type="hidden" name="show_closed" value="False" />
-<input name="search" value="$search"/>
-<input type="submit" name="action" value="Search" />
-</form>
-</body>
-</html>
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid
deleted file mode 100644 (file)
index 276f610..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<?python
-from libbe.bug import severity_values, status_values, thread_comments
-from libbe.utility import time_to_str 
-from beweb.controllers import bug_list_url, comment_url
-from beweb.formatting import comment_body_xhtml, select_among
-from beweb.model import people_map
-people = people_map()
-?>
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
-    py:extends="'master.kid'">
-
-<head>
-    <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
-    <title>Edit bug</title>
-</head>
-
-<body>
-<h1>Edit bug</h1>
-<form method="post" action=".">
-<table>
-<tr><td>Status</td><td>Severity</td><td>Assigned To</td><td>Summary</td></tr>
-<tr><td>${select_among("status", status_values, bug.status)}</td><td>${select_among("severity", severity_values, bug.severity)}</td>
-<td>${select_among("assigned", people.keys()+[None], bug.assigned, people)}</td><td><input name="summary" value="${bug.summary}" size="80" /></td></tr>
-</table>
-<div py:def="show_comment(comment, children)" class="comment">
-    <insetbox>
-    <table>
-        <tr><td>From</td><td>${comment.From}</td></tr>
-        <tr><td>Date</td><td>${time_to_str(comment.time)}</td></tr>
-    </table>
-    <div py:content="comment_body_xhtml(comment)" py:strip="True"></div>
-    <a href="${comment_url(project_id, bug.uuid, comment.uuid)}">Edit</a>
-    <a href="${comment_url(project_id, bug.uuid, comment.uuid, 
-                           action='Reply')}">Reply</a>
-    </insetbox>
-    <div style="margin-left:20px;">
-    <div py:for="child, grandchildren in children" py:strip="True">
-    ${show_comment(child, grandchildren)}
-    </div>
-    </div>
-</div>
-<div py:for="comment, children in thread_comments(bug.list_comments())" 
-     py:strip="True">
-    ${show_comment(comment, children)}
-</div>
-<p><input type="submit" name="action" value="Update"/></p>
-<p><input type="submit" name="action" value="New comment"/></p>
-</form>
-<a href="${bug_list_url(project_id)}">Bug List</a>
-</body>
-</html>
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid
deleted file mode 100644 (file)
index 2b522d4..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<?python
-from libbe.utility import time_to_str 
-from beweb.controllers import bug_list_url, bug_url
-?>
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
-    py:extends="'master.kid'">
-
-<head>
-    <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
-    <title>Edit comment</title>
-</head>
-
-<body>
-<h1>Edit comment</h1>
-<form method="post">
-<table>
-    <tr><td>From</td><td>${comment.From}</td></tr>
-    <tr><td>Date</td><td>${time_to_str(comment.time)}</td></tr>
-</table>
-<insetbox><textarea rows="15" cols="80" py:content="comment.body" name="comment_body" style="border-style: none"/></insetbox>
-<p><input type="submit" name="action" value="Update"/></p>
-</form>
-<a href="${bug_url(project_id, comment.bug.uuid)}">Up to Bug</a>
-</body>
-</html>
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/error.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/error.kid
deleted file mode 100644 (file)
index bc55615..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
-    py:extends="'master.kid'">
-<head>
-    <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
-    <title>BE Error: ${heading}</title>
-</head>
-
-<body>
-<h1 py:content="heading">Error heading</h1>
-<div py:replace="body" >Error Body</div>
-<pre py:content="traceback" class="traceback">Traceback</pre>
-</body>
-</html>
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/login.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/login.kid
deleted file mode 100644 (file)
index e7ad852..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
-    xmlns:py="http://purl.org/kid/ns#"
-    py:extends="'master.kid'">
-
-<head>
-    <meta content="text/html; charset=UTF-8"
-        http-equiv="content-type" py:replace="''"/>
-    <title>Login</title>
-    <style type="text/css">
-        #loginBox
-        {
-            width: 30%;
-            margin: auto;
-            margin-top: 10%;
-            padding-left: 10%;
-            padding-right: 10%;
-            padding-top: 5%;
-            padding-bottom: 5%;
-            font-family: verdana;
-            font-size: 10px;
-            background-color: #eee;
-            border: 2px solid #ccc;
-        }
-
-        #loginBox h1
-        {
-            font-size: 42px;
-            font-family: "Trebuchet MS";
-            margin: 0;
-            color: #ddd;
-        }
-
-        #loginBox p
-        {
-            position: relative;
-            top: -1.5em;
-            padding-left: 4em;
-            font-size: 12px;
-            margin: 0;
-            color: #666;
-        }
-
-        #loginBox table
-        {
-            table-layout: fixed;
-            border-spacing: 0;
-            width: 100%;
-        }
-
-        #loginBox td.label
-        {
-            width: 33%;
-            text-align: right;
-        }
-
-        #loginBox td.field
-        {
-            width: 66%;
-        }
-
-        #loginBox td.field input
-        {
-            width: 100%;
-        }
-
-        #loginBox td.buttons
-        {
-            text-align: right;
-        }
-
-    </style>
-</head>
-
-<body>
-    <div id="loginBox">
-        <h1>Login</h1>
-        <p>${message}</p>
-        <form action="${previous_url}" method="POST">
-            <table>
-                <tr>
-                    <td class="label">
-                        <label for="user_name">User Name:</label>
-                    </td>
-                    <td class="field">
-                        <input type="text" id="user_name" name="user_name"/>
-                    </td>
-                </tr>
-                <tr>
-                    <td class="label">
-                        <label for="password">Password:</label>
-                    </td>
-                    <td class="field">
-                        <input type="password" id="password" name="password"/>
-                    </td>
-                </tr>
-                <tr>
-                    <td colspan="2" class="buttons">
-                        <input type="submit" name="login" value="Login"/>
-                    </td>
-                </tr>
-            </table>
-
-            <input py:if="forward_url" type="hidden" name="forward_url"
-                value="${forward_url}"/>
-                
-            <input py:for="name,value in original_parameters.items()"
-                type="hidden" name="${name}" value="${value}"/>
-        </form>
-    </div>
-</body>
-</html>
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/master.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/master.kid
deleted file mode 100644 (file)
index 0772524..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\r
-<?python import sitetemplate ?>\r
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#" py:extends="sitetemplate">\r
-\r
-<head py:match="item.tag=='{http://www.w3.org/1999/xhtml}head'" py:attrs="item.items()">\r
-    <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>\r
-    <title py:if="False">Your title goes here</title>\r
-    <link rel="stylesheet" type="text/css" href="/static/css/style.css"/>\r
-    <meta py:replace="item[:]"/>\r
-    <style type="text/css">\r
-        #pageLogin\r
-        {\r
-            font-size: 10px;\r
-            font-family: verdana;\r
-            text-align: right;\r
-        }\r
-    </style>\r
-</head>\r
-\r
-<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()">\r
-<div id="header"><div style="float: left">b u g s   e v r y w h e r e</div><ul class="navoption"><li><a href="/about/">About</a></li></ul>&#160;</div> \r
-    <div py:if="tg.config('identity.on',False) and not 'logging_in' in locals()"\r
-        id="pageLogin">\r
-        <span py:if="tg.identity.anonymous">\r
-            <a href="/login">Login</a>\r
-        </span>\r
-        <span py:if="not tg.identity.anonymous">\r
-            Welcome ${tg.identity.user.display_name}.\r
-            <a href="/logout">Logout</a>\r
-        </span>\r
-    </div>\r
-\r
-    <div py:if="tg_flash" class="flash" py:content="tg_flash"></div>\r
-\r
-    <div py:replace="[item.text]+item[:]"/>\r
-\r
-<table py:match="item.tag=='{http://www.w3.org/1999/xhtml}insetbox'" cellspacing="0" cellpadding="0" border="0" class="insetbox">\r
-<tr height="19"><td background="/static/images/is-tl.png" width="19"/>\r
-    <td background="/static/images/is-t.png" />\r
-    <td background="/static/images/is-tr.png" width="11"></td>\r
-</tr>\r
-<tr>\r
-    <td background="/static/images/is-l.png"/>\r
-    <td py:content="item[:]"> Hello, this is some random text</td>\r
-    <td background="/static/images/is-r.png"/>\r
-</tr>\r
-<tr height="11">\r
-    <td background="/static/images/is-bl.png"/>\r
-    <td background="/static/images/is-b.png" />\r
-    <td background="/static/images/is-br.png"/>\r
-</tr>\r
-</table>\r
-<table py:match="item.tag=='{http://www.w3.org/1999/xhtml}dsbox'" cellspacing="0" cellpadding="0" border="0" class="dsbox">\r
-<tr height="11"><td background="/static/images/ds-tl.png" width="11"/>\r
-    <td background="/static/images/ds-t.png" />\r
-    <td background="/static/images/ds-tr.png" width="19"></td>\r
-</tr>\r
-<tr>\r
-    <td background="/static/images/ds-l.png"/>\r
-    <td py:content="item[:]"> Hello, this is some random text</td>\r
-    <td background="/static/images/ds2-r.png"/>\r
-</tr>\r
-<tr height="19">\r
-    <td background="/static/images/ds-bl.png"/>\r
-    <td background="/static/images/ds2-b.png" />\r
-    <td background="/static/images/ds-br.png"/>\r
-</tr>\r
-</table>\r
-</body>\r
-\r
-</html>\r
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/projects.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/projects.kid
deleted file mode 100644 (file)
index d5f9fd3..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<?python
-from libbe.bug import severity_values
-def select_among(name, options, default):
-    output = ['<select name="%s">' % name]
-    for option in options:
-        if option == default:
-            selected = ' selected="selected"'
-        else:
-            selected = ""
-        output.append("<option%s>%s</option>" % (selected, option))
-    output.append("</select>")
-    return XML("".join(output))
-?>
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
-    py:extends="'master.kid'">
-<?python
-project_triples = [(pn, pid, pl) for pid,(pn, pl) in projects.iteritems()]
-project_triples.sort()
-?>
-<head>
-    <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
-    <title>Project List</title>
-</head>
-
-<body>
-<h1>Project List</h1>
-<table>
-<tr py:for="project_name, project_id, project_loc in project_triples"><td><a href="/project/${project_id}/">${project_name}</a></td></tr>
-</table>
-</body>
-</html>
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/welcome.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/welcome.kid
deleted file mode 100644 (file)
index 08abd21..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\r
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"\r
-    py:extends="'master.kid'">\r
-<head>\r
-<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>\r
-<title>Welcome to TurboGears</title>\r
-</head>\r
-<body>\r
-<div id="header">&nbsp;</div>\r
-<div id="main_content">\r
-  <div id="status_block">Your TurboGears application is now running.</div>\r
-  <!--h1>Take steps to dive right in:</h1-->\r
-  <div id="sidebar">\r
-    <h2>Learn more</h2>\r
-    Learn more about TurboGears and take part in its\r
-    development\r
-    <ul class="links">\r
-      <li><a href="http://www.turbogears.org">Official website</a></li>\r
-      <li><a href="http://docs.turbogears.org">Documentation</a></li>\r
-      <li><a href="http://trac.turbogears.org/turbogears/">Trac\r
-        (bugs/suggestions)</a></li>\r
-      <li><a href="http://groups.google.com/group/turbogears"> Mailing list</a> </li>\r
-    </ul>\r
-  </div>\r
-  <div id="getting_started">\r
-    <ol id="getting_started_steps">\r
-      <li class="getting_started">\r
-        <h3>Model</h3>\r
-        <p> <a href="http://docs.turbogears.org/1.0/GettingStarted/DefineDatabase">Design models</a> in the <span class="code">model.py</span>.<br/>\r
-          Edit <span class="code">dev.cfg</span> to <a href="http://docs.turbogears.org/1.0/GettingStarted/UseDatabase">use a different backend</a>, or start with a pre-configured SQLite database. <br/>\r
-          Use script <span class="code">tg-admin sql create</span> to create the database tables.</p>\r
-      </li>\r
-      <li class="getting_started">\r
-        <h3>View</h3>\r
-        <p> Edit <a href="http://docs.turbogears.org/1.0/GettingStarted/Kid">html-like templates</a> in the <span class="code">/templates</span> folder;<br/>\r
-        Put all <a href="http://docs.turbogears.org/1.0/StaticFiles">static contents</a> in the <span class="code">/static</span> folder. </p>\r
-      </li>\r
-      <li class="getting_started">\r
-        <h3>Controller</h3>\r
-        <p> Edit <span class="code"> controllers.py</span> and <a href="http://docs.turbogears.org/1.0/GettingStarted/CherryPy">build your\r
-          website structure</a> with the simplicity of Python objects. <br/>\r
-          TurboGears will automatically reload itself when you modify your project. </p>\r
-      </li>\r
-    </ol>\r
-    <div class="notice"> If you create something cool, please <a href="http://groups.google.com/group/turbogears">let people know</a>, and consider contributing something back to the <a href="http://groups.google.com/group/turbogears">community</a>.</div>\r
-  </div>\r
-  <!-- End of getting_started -->\r
-</div>\r
-</body>\r
-</html>\r
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/tests/__init__.py b/interfaces/web/Bugs-Everywhere-Web/beweb/tests/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_controllers.py b/interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_controllers.py
deleted file mode 100644 (file)
index 0c77afe..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-from turbogears import testutil
-from beweb.controllers import Root
-import cherrypy
-
-cherrypy.root = Root()
-
-def test_method():
-    "the index method should return a string called now"
-    import types
-    result = testutil.call(cherrypy.root.index)
-    assert type(result["now"]) == types.StringType
-
-def test_indextitle():
-    "The mainpage should have the right title"
-    testutil.createRequest("/")
-    assert "<TITLE>Welcome to TurboGears</TITLE>" in cherrypy.response.body[0]
diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_model.py b/interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_model.py
deleted file mode 100644 (file)
index 74c4e83..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-# If your project uses a database, you can set up database tests
-# similar to what you see below. Be sure to set the db_uri to
-# an appropriate uri for your testing database. sqlite is a good
-# choice for testing, because you can use an in-memory database
-# which is very fast.
-
-from turbogears import testutil, database
-# from beweb.model import YourDataClass, User
-
-# database.set_db_uri("sqlite:///:memory:")
-
-# class TestUser(testutil.DBTest):
-#     def get_model(self):
-#         return User
-#
-#     def test_creation(self):
-#         "Object creation should set the name"
-#         obj = User(user_name = "creosote",
-#                       email_address = "spam@python.not",
-#                       display_name = "Mr Creosote",
-#                       password = "Wafer-thin Mint")
-#         assert obj.display_name == "Mr Creosote"
-
diff --git a/interfaces/web/Bugs-Everywhere-Web/dev.cfg b/interfaces/web/Bugs-Everywhere-Web/dev.cfg
deleted file mode 100644 (file)
index eda9e6c..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-[global]
-# This is where all of your settings go for your development environment
-# Settings that are the same for both development and production
-# (such as template engine, encodings, etc.) all go in 
-# beweb/config/app.cfg
-
-# DATABASE
-
-# pick the form for your database
-# sqlobject.dburi="postgres://username@hostname/databasename"
-# sqlobject.dburi="mysql://username:password@hostname:port/databasename"
-# sqlobject.dburi="sqlite://%(package_dir)s/database.sqlite"
-
-# If you have sqlite, here's a simple default to get you started
-# in development
-sqlobject.dburi="sqlite://%(current_dir_uri)s/devdata.sqlite"
-
-
-# if you are using a database or table type without transactions
-# (MySQL default, for example), you should turn off transactions
-# by prepending notrans_ on the uri
-# sqlobject.dburi="notrans_mysql://username:password@hostname:port/databasename"
-
-# for Windows users, sqlite URIs look like:
-# sqlobject.dburi="sqlite:///drive_letter:/path/to/file"
-
-# SERVER
-
-# Some server parameters that you may want to tweak
-# server.socket_port=8080
-
-# Enable the debug output at the end on pages.
-# log_debug_info_filter.on = False
-
-server.environment="development"
-autoreload.package="beweb"
-
-# session_filter.on = True
-
-# Set to True if you'd like to abort execution if a controller gets an
-# unexpected parameter. False by default
-tg.strict_parameters = True
-identity.on = True
-visit.on = True
-identity.soprovider.model.user="beweb.model.User"
-identity.soprovider.model.group="beweb.model.Group"
-identity.soprovider.model.permission="beweb.model.Permission"
-
-
-# LOGGING
-# Logging configuration generally follows the style of the standard
-# Python logging module configuration. Note that when specifying
-# log format messages, you need to use *() for formatting variables.
-# Deployment independent log configuration is in beweb/config/log.cfg
-[logging]
-
-[[loggers]]
-[[[beweb]]]
-level='DEBUG'
-qualname='beweb'
-handlers=['debug_out']
-
-[[[allinfo]]]
-level='INFO'
-handlers=['debug_out']
-
-[[[access]]]
-level='INFO'
-qualname='turbogears.access'
-handlers=['access_out']
-propagate=0
diff --git a/interfaces/web/Bugs-Everywhere-Web/libbe b/interfaces/web/Bugs-Everywhere-Web/libbe
deleted file mode 120000 (symlink)
index 7d18612..0000000
+++ /dev/null
@@ -1 +0,0 @@
-../../../libbe
\ No newline at end of file
diff --git a/interfaces/web/Bugs-Everywhere-Web/prod.cfg b/interfaces/web/Bugs-Everywhere-Web/prod.cfg
deleted file mode 100644 (file)
index c0d4aca..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-[global]
-# This is where all of your settings go for your production environment.
-# You'll copy this file over to your production server and provide it
-# as a command-line option to your start script.
-# Settings that are the same for both development and production
-# (such as template engine, encodings, etc.) all go in 
-# yourpackage/config/app.cfg
-
-# DATABASE
-
-# pick the form for your database
-# sqlobject.dburi="postgres://username@hostname/databasename"
-# sqlobject.dburi="mysql://username:password@hostname:port/databasename"
-# sqlobject.dburi="sqlite:///file_name_and_path"
-
-# if you are using a database or table type without transactions
-# (MySQL default, for example), you should turn off transactions
-# by prepending notrans_ on the uri
-# sqlobject.dburi="notrans_mysql://username:password@hostname:port/databasename"
-
-# for Windows users, sqlite URIs look like:
-# sqlobject.dburi="sqlite:///drive_letter|/path/to/file"
-
-
-# SERVER
-
-server.environment="production"
-server.log_file="server.log"
-server.log_to_screen=False
-
-# Sets the number of threads the server uses
-# server.thread_pool = 1
-
-# if this is part of a larger site, you can set the path
-# to the TurboGears instance here
-# server.webpath=""
-
-# Set to True if you'd like to abort execution if a controller gets an
-# unexpected parameter. False by default
-# tg.strict_parameters = False
-
diff --git a/interfaces/web/Bugs-Everywhere-Web/sample-prod.cfg b/interfaces/web/Bugs-Everywhere-Web/sample-prod.cfg
deleted file mode 100644 (file)
index d1052f8..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-[global]
-# This is where all of your settings go for your production environment.
-# You'll copy this file over to your production server and provide it
-# as a command-line option to your start script.
-# Settings that are the same for both development and production
-# (such as template engine, encodings, etc.) all go in 
-# beweb/config/app.cfg
-
-# pick the form for your database
-# sqlobject.dburi="postgres://username@hostname/databasename"
-# sqlobject.dburi="mysql://username:password@hostname:port/databasename"
-# sqlobject.dburi="sqlite:///file_name_and_path"
-
-# If you have sqlite, here's a simple default to get you started
-# in development
-sqlobject.dburi="sqlite://%(current_dir_uri)s/devdata.sqlite"
-
-
-# if you are using a database or table type without transactions
-# (MySQL default, for example), you should turn off transactions
-# by prepending notrans_ on the uri
-# sqlobject.dburi="notrans_mysql://username:password@hostname:port/databasename"
-
-# for Windows users, sqlite URIs look like:
-# sqlobject.dburi="sqlite:///drive_letter:/path/to/file"
-
-
-# SERVER
-
-server.environment="production"
-
-# Sets the number of threads the server uses
-# server.thread_pool = 1
-
-# if this is part of a larger site, you can set the path
-# to the TurboGears instance here
-# server.webpath=""
-
-# session_filter.on = True
-
-# Set to True if you'd like to abort execution if a controller gets an
-# unexpected parameter. False by default
-# tg.strict_parameters = False
-
-# LOGGING
-# Logging configuration generally follows the style of the standard
-# Python logging module configuration. Note that when specifying
-# log format messages, you need to use *() for formatting variables.
-# Deployment independent log configuration is in beweb/config/log.cfg
-[logging]
-
-[[handlers]]
-
-[[[access_out]]]
-# set the filename as the first argument below
-args="('server.log',)"
-class='FileHandler'
-level='INFO'
-formatter='message_only'
-
-[[loggers]]
-[[[beweb]]]
-level='ERROR'
-qualname='beweb'
-handlers=['error_out']
-
-[[[access]]]
-level='INFO'
-qualname='turbogears.access'
-handlers=['access_out']
-propagate=0
diff --git a/interfaces/web/Bugs-Everywhere-Web/server.log b/interfaces/web/Bugs-Everywhere-Web/server.log
deleted file mode 100644 (file)
index fe02ade..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-2005/12/01 15:44:05 CONFIG INFO Server parameters:
-2005/12/01 15:44:05 CONFIG INFO   server.environment: production
-2005/12/01 15:44:05 CONFIG INFO   server.logToScreen: False
-2005/12/01 15:44:05 CONFIG INFO   server.logFile: server.log
-2005/12/01 15:44:05 CONFIG INFO   server.protocolVersion: HTTP/1.0
-2005/12/01 15:44:05 CONFIG INFO   server.socketHost: 
-2005/12/01 15:44:05 CONFIG INFO   server.socketPort: 8080
-2005/12/01 15:44:05 CONFIG INFO   server.socketFile: 
-2005/12/01 15:44:05 CONFIG INFO   server.reverseDNS: False
-2005/12/01 15:44:05 CONFIG INFO   server.socketQueueSize: 5
-2005/12/01 15:44:05 CONFIG INFO   server.threadPool: 0
-2005/12/01 15:44:05 HTTP INFO Serving HTTP on http://localhost:8080/
-2005/12/01 15:44:17 HTTP INFO 127.0.0.1 - GET / HTTP/1.1
-2005/12/01 15:44:37 HTTP INFO 192.168.2.12 - GET / HTTP/1.1
-2005/12/01 15:44:42 HTTP INFO 192.168.2.12 - GET /be HTTP/1.1
-2005/12/01 15:44:43 HTTP INFO 192.168.2.12 - GET /be/301724b1-3853-4aff-8f23-44373df7cf1c HTTP/1.1
-2005/12/01 15:44:48 HTTP INFO 192.168.2.12 - GET /be/ HTTP/1.1
-2005/12/01 15:44:50 HTTP INFO 192.168.2.12 - GET / HTTP/1.1
-2005/12/01 15:44:53 HTTP INFO 192.168.2.12 - GET /devel/ HTTP/1.1
-2005/12/01 15:44:58 HTTP INFO 192.168.2.12 - GET / HTTP/1.1
-2005/12/01 15:52:57 HTTP INFO 127.0.0.1 - GET /devel HTTP/1.1
-2005/12/01 15:52:59 HTTP INFO 127.0.0.1 - GET /devel HTTP/1.1
-2005/12/01 15:53:25 HTTP INFO 127.0.0.1 - GET /devel HTTP/1.1
-2005/12/01 15:53:29 HTTP INFO <Ctrl-C> hit: shutting down server
-2005/12/01 15:53:29 HTTP INFO HTTP Server shut down
-2005/12/01 15:53:29 HTTP INFO CherryPy shut down
diff --git a/interfaces/web/Bugs-Everywhere-Web/setup-tables.py b/interfaces/web/Bugs-Everywhere-Web/setup-tables.py
deleted file mode 100644 (file)
index 161d7c7..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-import pkg_resources
-pkg_resources.require("TurboGears")
-
-import turbogears
-import cherrypy
-cherrypy.lowercase_api = True
-
-from os.path import *
-import sys
-
-# first look on the command line for a desired config file,
-# if it's not on the command line, then
-# look for setup.py in this directory. If it's not there, this script is
-# probably installed
-if len(sys.argv) > 1:
-    turbogears.update_config(configfile=sys.argv[1], 
-        modulename="beweb.config.app")
-elif exists(join(dirname(__file__), "setup.py")):
-    turbogears.update_config(configfile="dev.cfg",
-        modulename="beweb.config.app")
-else:
-    turbogears.update_config(configfile="prod.cfg",
-        modulename="beweb.config.app")
-
-from beweb.controllers import Root
-
-cherrypy.root = Root()
-
-
-from beweb.model import TG_Group, TG_Permission
-g = TG_Group(groupId="editors", displayName="Editors")
-p = TG_Permission(permissionId="editbugs", 
-                 description="Ability to create and edit bugs")
-g.addTG_Permission(p)
diff --git a/interfaces/web/Bugs-Everywhere-Web/setup.py b/interfaces/web/Bugs-Everywhere-Web/setup.py
deleted file mode 100644 (file)
index 8ba3da2..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-from setuptools import setup, find_packages
-from turbogears.finddata import find_package_data
-
-import os
-execfile(os.path.join("beweb", "release.py"))
-
-setup(
-    name="Bugs-Everywhere-Web",
-    version=version,
-    
-    # uncomment the following lines if you fill them out in release.py
-    #description=description,
-    #author=author,
-    #author_email=email,
-    #url=url,
-    #download_url=download_url,
-    #license=license,
-    
-    install_requires = [
-        "TurboGears >= 1.0b1",
-    ],
-    scripts = ["start-beweb.py"],
-    zip_safe=False,
-    packages=find_packages(),
-    package_data = find_package_data(where='beweb',
-                                     package='beweb'),
-    keywords = [
-        # Use keywords if you'll be adding your package to the
-        # Python Cheeseshop
-        
-        # if this has widgets, uncomment the next line
-        # 'turbogears.widgets',
-        
-        # if this has a tg-admin command, uncomment the next line
-        # 'turbogears.command',
-        
-        # if this has identity providers, uncomment the next line
-        # 'turbogears.identity.provider',
-    
-        # If this is a template plugin, uncomment the next line
-        # 'python.templating.engines',
-        
-        # If this is a full application, uncomment the next line
-        # 'turbogears.app',
-    ],
-    classifiers = [
-        'Development Status :: 3 - Alpha',
-        'Operating System :: OS Independent',
-        'Programming Language :: Python',
-        'Topic :: Software Development :: Libraries :: Python Modules',
-        'Framework :: TurboGears',
-        # if this is an application that you'll distribute through
-        # the Cheeseshop, uncomment the next line
-        # 'Framework :: TurboGears :: Applications',
-        
-        # if this is a package that includes widgets that you'll distribute
-        # through the Cheeseshop, uncomment the next line
-        # 'Framework :: TurboGears :: Widgets',
-    ],
-    test_suite = 'nose.collector',
-    )
-    
diff --git a/interfaces/web/Bugs-Everywhere-Web/start-beweb.py b/interfaces/web/Bugs-Everywhere-Web/start-beweb.py
deleted file mode 100755 (executable)
index 4070abd..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env python
-import pkg_resources
-pkg_resources.require("TurboGears")
-
-import turbogears
-import cherrypy
-cherrypy.lowercase_api = True
-
-from os.path import *
-import sys
-
-# first look on the command line for a desired config file,
-# if it's not on the command line, then
-# look for setup.py in this directory. If it's not there, this script is
-# probably installed
-if len(sys.argv) > 1:
-    turbogears.update_config(configfile=sys.argv[1], 
-        modulename="beweb.config")
-elif exists(join(dirname(__file__), "setup.py")):
-    turbogears.update_config(configfile="dev.cfg",
-        modulename="beweb.config")
-else:
-    turbogears.update_config(configfile="prod.cfg",
-        modulename="beweb.config")
-
-from beweb.controllers import Root
-
-turbogears.start_server(Root())
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d32716fe194db3e53ca8176a6fc9b854d7f85edb 100644 (file)
@@ -0,0 +1,53 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+"""The libbe module does all the legwork for bugs-everywhere_ (BE).
+
+.. _bugs-everywhere: http://bugseverywhere.org
+
+To facilitate faster loading, submodules are not imported by default.
+The available submodules are:
+
+* :mod:`libbe.bugdir`
+* :mod:`libbe.bug`
+* :mod:`libbe.comment`
+* :mod:`libbe.command`
+* :mod:`libbe.diff`
+* :mod:`libbe.error`
+* :mod:`libbe.storage`
+* :mod:`libbe.ui`
+* :mod:`libbe.util`
+* :mod:`libbe.version`
+* :mod:`libbe._version`
+"""
+
+TESTING = False
+"""Flag controlling test-suite generation.
+
+To reduce module load time, test suite generation is turned of by
+default.  If you *do* want to generate the test suites, set
+``TESTING=True`` before loading any :mod:`libbe` submodules.
+
+Examples
+--------
+
+>>> import libbe
+>>> libbe.TESTING = True
+>>> import libbe.bugdir
+>>> 'SimpleBugDir' in dir(libbe.bugdir)
+True
+"""
diff --git a/libbe/arch.py b/libbe/arch.py
deleted file mode 100644 (file)
index daa8ac6..0000000
+++ /dev/null
@@ -1,312 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Ben Finney <ben+python@benfinney.id.au>
-#                         James Rowe <jnrowe@ukfsn.org>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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_version(self):
-        status,output,error = self._u_invoke_client("--version")
-        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
-          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
-          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)
-
-
-\f
-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
deleted file mode 100644 (file)
index 490ed62..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
-#
-# 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)
index fd30ff74a74e67c38285273f366755ef0e3f1f14..8bf32dd45705fbb4984826f44ef6fbdf414c86cc 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2008-2009 Chris Ball <cjb@laptop.org>
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
 #                         Thomas Habets <thomas@habets.pp.se>
 #                         W. Trevor King <wking@drexel.edu>
 #
 # 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.
+"""Define the :class:`Bug` class for representing bugs.
 """
 
+import copy
 import os
 import os.path
 import errno
+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, \
+import libbe
+import libbe.util.id
+from libbe.storage.util.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
+import libbe.storage.util.settings_object as settings_object
+import libbe.storage.util.mapfile as mapfile
+import libbe.comment as comment
+import libbe.util.utility as utility
+
+if libbe.TESTING == True:
+    import doctest
 
 
 class DiskAccessRequired (Exception):
@@ -49,6 +57,7 @@ class DiskAccessRequired (Exception):
 
 # in order of increasing severity.  (name, description) pairs
 severity_def = (
+  ("target", "The issue is a target or milestone, not a bug."),
   ("wishlist","A feature that could improve usefulness, but not a bug."),
   ("minor","The standard bug level."),
   ("serious","A bug that requires workarounds."),
@@ -111,8 +120,12 @@ def load_status(active_status_def, inactive_status_def):
 load_status(active_status_def, inactive_status_def)
 
 
-class Bug(settings_object.SavedSettingsObject):
-    """
+class Bug (settings_object.SavedSettingsObject):
+    """A bug (or issue) is a place to store attributes and attach
+    :class:`~libbe.comment.Comment`\s.  In mailing-list terms, a bug is
+    analogous to a thread.  Bugs are normally stored in
+    :class:`~libbe.bugdir.BugDir`\s.
+
     >>> b = Bug()
     >>> print b.status
     open
@@ -122,6 +135,7 @@ class Bug(settings_object.SavedSettingsObject):
     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)
     <type 'int'>
     >>> print type(b.time_string)
@@ -161,15 +175,11 @@ class Bug(settings_object.SavedSettingsObject):
                          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 {}
@@ -215,42 +225,35 @@ class Bug(settings_object.SavedSettingsObject):
     def summary(): return {}
 
     def _get_comment_root(self, load_full=False):
-        if self.sync_with_disk:
-            return comment.loadComments(self, load_full=load_full)
+        if self.storage != None and self.storage.is_readable():
+            return comment.load_comments(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")
+    @doc_property(doc="The trunk of the comment tree.  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.")
     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,
+    def __init__(self, bugdir=None, uuid=None, from_storage=False,
                  load_comments=False, summary=None):
         settings_object.SavedSettingsObject.__init__(self)
         self.bugdir = bugdir
+        self.storage = None
         self.uuid = uuid
-        if from_disk == True:
-            self.sync_with_disk = True
-        else:
-            self.sync_with_disk = False
+        self.id = libbe.util.id.ID(self, 'bug')
+        if from_storage == False:
             if uuid == None:
-                self.uuid = uuid_gen()
+                self.uuid = libbe.util.id.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
+            dummy = self.comment_root
+        if self.bugdir != None:
+            self.storage = self.bugdir.storage
+        if from_storage == False:
+            if self.storage != None and self.storage.is_writeable():
+                self.save()
 
     def __repr__(self):
         return "Bug(uuid=%r)" % self.uuid
@@ -267,48 +270,11 @@ class Bug(settings_object.SavedSettingsObject):
         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 = '<bug>\n'
-        for (k,v) in info:
-            if v is not None:
-                ret += '  <%s>%s</%s>\n' % (k,xml.sax.saxutils.escape(v),k)
-        for estr in self.extra_strings:
-            ret += '  <extra-string>%s</extra-string>\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 += '</bug>'
-        return ret
+        if type(value) not in types.StringTypes:
+            return str(value)
+        return value
 
     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 = ""
@@ -316,11 +282,10 @@ class Bug(settings_object.SavedSettingsObject):
                 htime = utility.handy_time(self.time)
                 timestring = "%s (%s)" % (htime, self.time_string)
             info = [("ID", self.uuid),
-                    ("Short name", shortname),
+                    ("Short name", self.id.user()),
                     ("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)]
@@ -331,90 +296,399 @@ class Bug(settings_object.SavedSettingsObject):
             statuschar = self.status[0]
             severitychar = self.severity[0]
             chars = "%c%c" % (statuschar, severitychar)
-            bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
-        
+            bugout = "%s:%s: %s" % (self.id.user(),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)
+            self.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
+            comout = self.comment_root.string_thread(flatten=False)
             output = bugout + '\n' + comout.rstrip('\n')
         else :
             output = bugout
         return output
 
-    # methods for saving/loading/acessing settings and properties.
+    def xml(self, indent=0, show_comments=False):
+        if self.time == None:
+            timestring = ""
+        else:
+            timestring = utility.time_to_str(self.time)
 
-    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)
+        info = [('uuid', self.uuid),
+                ('short-name', self.id.user()),
+                ('severity', self.severity),
+                ('status', self.status),
+                ('assigned', self.assigned),
+                ('reporter', self.reporter),
+                ('creator', self.creator),
+                ('created', timestring),
+                ('summary', self.summary)]
+        lines = ['<bug>']
+        for (k,v) in info:
+            if v is not None:
+                lines.append('  <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
+        for estr in self.extra_strings:
+            lines.append('  <extra-string>%s</extra-string>' % estr)
+        if show_comments == True:
+            comout = self.comment_root.xml_thread(indent=indent+2)
+            if len(comout) > 0:
+                lines.append(comout)
+        lines.append('</bug>')
+        istring = ' '*indent
+        sep = '\n' + istring
+        return istring + sep.join(lines).rstrip('\n')
+
+    def from_xml(self, xml_string, verbose=True):
+        u"""
+        Note: If a bug uuid is given, set .alt_id to it's value.
+        >>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
+        >>> bugA.date = "Thu, 01 Jan 1970 00:00:00 +0000"
+        >>> bugA.creator = u'Fran\xe7ois'
+        >>> bugA.extra_strings += ['TAG: very helpful']
+        >>> commA = bugA.comment_root.new_reply(body='comment A')
+        >>> commB = bugA.comment_root.new_reply(body='comment B')
+        >>> commC = commA.new_reply(body='comment C')
+        >>> xml = bugA.xml(show_comments=True)
+        >>> bugB = Bug()
+        >>> bugB.from_xml(xml, verbose=True)
+        >>> bugB.xml(show_comments=True) == xml
+        False
+        >>> bugB.uuid = bugB.alt_id
+        >>> for comm in bugB.comments():
+        ...     comm.uuid = comm.alt_id
+        ...     comm.alt_id = None
+        >>> bugB.xml(show_comments=True) == xml
+        True
+        >>> bugB.explicit_attrs  # doctest: +NORMALIZE_WHITESPACE
+        ['severity', 'status', 'creator', 'created', 'summary']
+        >>> len(list(bugB.comments()))
+        3
+        """
+        if type(xml_string) == types.UnicodeType:
+            xml_string = xml_string.strip().encode('unicode_escape')
+        if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
+            bug = xml_string
+        else:
+            bug = ElementTree.XML(xml_string)
+        if bug.tag != 'bug':
+            raise utility.InvalidXML( \
+                'bug', bug, 'root element must be <comment>')
+        tags=['uuid','short-name','severity','status','assigned',
+              'reporter', 'creator','created','summary','extra-string']
+        self.explicit_attrs = []
+        uuid = None
+        estrs = []
+        comments = []
+        for child in bug.getchildren():
+            if child.tag == 'short-name':
+                pass
+            elif child.tag == 'comment':
+                comm = comment.Comment(bug=self)
+                comm.from_xml(child)
+                comments.append(comm)
+                continue
+            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 = text.decode('unicode_escape').strip()
+                if child.tag == 'uuid':
+                    uuid = text
+                    continue # don't set the bug's uuid tag.
+                elif child.tag == 'extra-string':
+                    estrs.append(text)
+                    continue # don't set the bug's extra_string yet.
+                attr_name = child.tag.replace('-','_')
+                self.explicit_attrs.append(attr_name)
+                setattr(self, attr_name, text)
+            elif verbose == True:
+                print >> sys.stderr, 'Ignoring unknown tag %s in %s' \
+                    % (child.tag, comment.tag)
+        if uuid != self.uuid:
+            if not hasattr(self, 'alt_id') or self.alt_id == None:
+                self.alt_id = uuid
+        self.extra_strings = estrs
+        self.add_comments(comments, ignore_missing_references=True)
+
+    def add_comment(self, comment, *args, **kwargs):
+        """
+        Add a comment too the current bug, under the parent specified
+        by comment.in_reply_to.
+        Note: If a bug uuid is given, set .alt_id to it's value.
+
+        >>> bugA = Bug(uuid='0123', summary='Need to test Bug.add_comment()')
+        >>> bugA.creator = 'Jack'
+        >>> commA = bugA.comment_root.new_reply(body='comment A')
+        >>> commA.uuid = 'commA'
+        >>> commB = comment.Comment(body='comment B')
+        >>> commB.uuid = 'commB'
+        >>> bugA.add_comment(commB)
+        >>> commC = comment.Comment(body='comment C')
+        >>> commC.uuid = 'commC'
+        >>> commC.in_reply_to = commA.uuid
+        >>> bugA.add_comment(commC)
+        >>> print bugA.xml(show_comments=True)  # doctest: +ELLIPSIS
+        <bug>
+          <uuid>0123</uuid>
+          <short-name>/012</short-name>
+          <severity>minor</severity>
+          <status>open</status>
+          <creator>Jack</creator>
+          <created>...</created>
+          <summary>Need to test Bug.add_comment()</summary>
+          <comment>
+            <uuid>commA</uuid>
+            <short-name>/012/commA</short-name>
+            <author></author>
+            <date>...</date>
+            <content-type>text/plain</content-type>
+            <body>comment A</body>
+          </comment>
+          <comment>
+            <uuid>commC</uuid>
+            <short-name>/012/commC</short-name>
+            <in-reply-to>commA</in-reply-to>
+            <author></author>
+            <date>...</date>
+            <content-type>text/plain</content-type>
+            <body>comment C</body>
+          </comment>
+          <comment>
+            <uuid>commB</uuid>
+            <short-name>/012/commB</short-name>
+            <author></author>
+            <date>...</date>
+            <content-type>text/plain</content-type>
+            <body>comment B</body>
+          </comment>
+        </bug>
+        """
+        self.add_comments([comment], **kwargs)
 
-    def set_sync_with_disk(self, value):
-        self.sync_with_disk = value
-        for comment in self.comments():
-            comment.set_sync_with_disk(value)
+    def add_comments(self, comments, default_parent=None,
+                     ignore_missing_references=False):
+        """
+        Convert a raw list of comments to single root comment.  If a
+        comment does not specify a parent with .in_reply_to, the
+        parent defaults to .comment_root, but you can specify another
+        default parent via default_parent.
+        """
+        uuid_map = {}
+        if default_parent == None:
+            default_parent = self.comment_root
+        for c in list(self.comments()) + comments:
+            assert c.uuid != None
+            assert c.uuid not in uuid_map
+            uuid_map[c.uuid] = c
+            if c.alt_id != None:
+                uuid_map[c.alt_id] = c
+        uuid_map[None] = self.comment_root
+        uuid_map[comment.INVALID_UUID] = self.comment_root
+        if default_parent != self.comment_root:
+            assert default_parent.uuid in uuid_map, default_parent.uuid
+        for c in comments:
+            if c.in_reply_to == None \
+                    and default_parent.uuid != comment.INVALID_UUID:
+                c.in_reply_to = default_parent.uuid
+            elif c.in_reply_to == comment.INVALID_UUID:
+                c.in_reply_to = None
+            try:
+                parent = uuid_map[c.in_reply_to]
+            except KeyError:
+                if ignore_missing_references == True:
+                    print >> sys.stderr, \
+                        'Ignoring missing reference to %s' % c.in_reply_to
+                    parent = default_parent
+                    if parent.uuid != comment.INVALID_UUID:
+                        c.in_reply_to = parent.uuid
+                else:
+                    raise comment.MissingReference(c)
+            c.bug = self
+            parent.append(c)
+
+    def merge(self, other, accept_changes=True,
+              accept_extra_strings=True, accept_comments=True,
+              change_exception=False):
+        """
+        Merge info from other into this bug.  Overrides any attributes
+        in self that are listed in other.explicit_attrs.
+
+        >>> bugA = Bug(uuid='0123', summary='Need to test Bug.merge()')
+        >>> bugA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
+        >>> bugA.creator = 'Frank'
+        >>> bugA.extra_strings += ['TAG: very helpful']
+        >>> bugA.extra_strings += ['TAG: favorite']
+        >>> commA = bugA.comment_root.new_reply(body='comment A')
+        >>> commA.uuid = 'uuid-commA'
+        >>> bugB = Bug(uuid='3210', summary='More tests for Bug.merge()')
+        >>> bugB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
+        >>> bugB.creator = 'John'
+        >>> bugB.explicit_attrs = ['creator', 'summary']
+        >>> bugB.extra_strings += ['TAG: very helpful']
+        >>> bugB.extra_strings += ['TAG: useful']
+        >>> commB = bugB.comment_root.new_reply(body='comment B')
+        >>> commB.uuid = 'uuid-commB'
+        >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
+        ...            accept_comments=False, change_exception=False)
+        >>> print bugA.creator
+        Frank
+        >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
+        ...            accept_comments=False, change_exception=True)
+        Traceback (most recent call last):
+          ...
+        ValueError: Merge would change creator "Frank"->"John" for bug 0123
+        >>> print bugA.creator
+        Frank
+        >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=False,
+        ...            accept_comments=False, change_exception=True)
+        Traceback (most recent call last):
+          ...
+        ValueError: Merge would add extra string "TAG: useful" for bug 0123
+        >>> print bugA.creator
+        John
+        >>> print bugA.extra_strings
+        ['TAG: favorite', 'TAG: very helpful']
+        >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
+        ...            accept_comments=False, change_exception=True)
+        Traceback (most recent call last):
+          ...
+        ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123
+        >>> print bugA.extra_strings
+        ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
+        >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
+        ...            accept_comments=True, change_exception=True)
+        >>> print bugA.xml(show_comments=True)  # doctest: +ELLIPSIS
+        <bug>
+          <uuid>0123</uuid>
+          <short-name>/012</short-name>
+          <severity>minor</severity>
+          <status>open</status>
+          <creator>John</creator>
+          <created>...</created>
+          <summary>More tests for Bug.merge()</summary>
+          <extra-string>TAG: favorite</extra-string>
+          <extra-string>TAG: useful</extra-string>
+          <extra-string>TAG: very helpful</extra-string>
+          <comment>
+            <uuid>uuid-commA</uuid>
+            <short-name>/012/uuid-commA</short-name>
+            <author></author>
+            <date>...</date>
+            <content-type>text/plain</content-type>
+            <body>comment A</body>
+          </comment>
+          <comment>
+            <uuid>uuid-commB</uuid>
+            <short-name>/012/uuid-commB</short-name>
+            <author></author>
+            <date>...</date>
+            <content-type>text/plain</content-type>
+            <body>comment B</body>
+          </comment>
+        </bug>
+        """
+        for attr in other.explicit_attrs:
+            old = getattr(self, attr)
+            new = getattr(other, attr)
+            if old != new:
+                if accept_changes == True:
+                    setattr(self, attr, new)
+                elif change_exception == True:
+                    raise ValueError, \
+                        'Merge would change %s "%s"->"%s" for bug %s' \
+                        % (attr, old, new, self.uuid)
+        for estr in other.extra_strings:
+            if not estr in self.extra_strings:
+                if accept_extra_strings == True:
+                    self.extra_strings.append(estr)
+                elif change_exception == True:
+                    raise ValueError, \
+                        'Merge would add extra string "%s" for bug %s' \
+                        % (estr, self.uuid)
+        for o_comm in other.comments():
+            try:
+                s_comm = self.comment_root.comment_from_uuid(o_comm.uuid)
+            except KeyError, e:
+                try:
+                    s_comm = self.comment_root.comment_from_uuid(o_comm.alt_id)
+                except KeyError, e:
+                    s_comm = None
+            if s_comm == None:
+                if accept_comments == True:
+                    o_comm_copy = copy.copy(o_comm)
+                    o_comm_copy.bug = self
+                    o_comm_copy.id = libbe.util.id.ID(o_comm_copy, 'comment')
+                    self.comment_root.add_reply(o_comm_copy)
+                elif change_exception == True:
+                    raise ValueError, \
+                        'Merge would add comment %s (alt: %s) to bug %s' \
+                        % (o_comm.uuid, o_comm.alt_id, self.uuid)
+            else:
+                s_comm.merge(o_comm, accept_changes=accept_changes,
+                             accept_extra_strings=accept_extra_strings,
+                             change_exception=change_exception)
+
+    # methods for saving/loading/acessing settings and properties.
 
-    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 load_settings(self, settings_mapfile=None):
+        if settings_mapfile == None:
+            settings_mapfile = \
+                self.storage.get(self.id.storage('values'), default='\n')
+        try:
+            settings = mapfile.parse(settings_mapfile)
+        except mapfile.InvalidMapfileContents, e:
+            raise Exception('Invalid settings file for bug %s\n'
+                            '(BE version missmatch?)' % self.id.user())
+        self._setup_saved_settings(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())
+        mf = mapfile.generate(self._get_saved_settings())
+        self.storage.set(self.id.storage('values'), mf)
 
     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).
+        Save any loaded contents to storage.  Because of lazy loading
+        of comments, this is actually not too inefficient.
+
+        However, if self.storage.is_writeable() == True, then any
+        changes are automatically written to storage as soon as they
+        happen, so calling this method will just waste time (unless
+        something else has been messing with your stored files).
         """
-        sync_with_disk = self.sync_with_disk
-        if sync_with_disk == False:
-            self.set_sync_with_disk(True)
+        assert self.storage != None, "Can't save without storage"
+        if self.bugdir != None:
+            parent = self.bugdir.id.storage()
+        else:
+            parent = None
+        self.storage.add(self.id.storage(), parent=parent, directory=True)
+        self.storage.add(self.id.storage('values'), parent=self.id.storage(),
+                         directory=False)
         self.save_settings()
         if len(self.comment_root) > 0:
-            comment.saveComments(self)
-        if sync_with_disk == False:
-            self.set_sync_with_disk(False)
+            comment.save_comments(self)
 
     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
+            # next _get_comment_root returns a fresh version.  Turn of
+            # writing temporarily so we don't write our blank comment
             # tree to disk.
-            self.sync_with_disk = False
+            w = self.storage.writeable
+            self.storage.writeable = False
             self.comment_root = None
-            self.sync_with_disk = True
+            self.storage.writeable = w
 
     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)
-    
+        self.storage.recursive_remove(self.id.storage())
+
     # methods for managing comments
 
+    def uuids(self):
+        for comment in self.comments():
+            yield comment.uuid
+
     def comments(self):
         for comment in self.comment_root.traverse():
             yield comment
@@ -423,20 +697,15 @@ class Bug(settings_object.SavedSettingsObject):
         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, *args, **kwargs):
+        return self.comment_root.comment_from_uuid(uuid, *args, **kwargs)
 
-    def comment_from_uuid(self, uuid):
-        return self.comment_root.comment_from_uuid(uuid)
+    # methods for id generation
 
-    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)
+    def sibling_uuids(self):
+        if self.bugdir != None:
+            return self.bugdir.uuids()
+        return []
 
 
 # The general rule for bug sorting is that "more important" bugs are
@@ -449,6 +718,7 @@ 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"
@@ -467,8 +737,9 @@ def cmp_severity(bug_1, bug_2):
 
 def cmp_status(bug_1, bug_2):
     """
-    Compare the status levels of two bugs, with more 'open' bugs
+    Compare the status levels of two bugs, with more "open" bugs
     comparing as less.
+
     >>> bugA = Bug()
     >>> bugB = Bug()
     >>> bugA.status = bugB.status = "open"
@@ -488,9 +759,10 @@ def cmp_status(bug_1, bug_2):
 
 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.
+    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()
@@ -510,7 +782,7 @@ def cmp_attr(bug_1, bug_2, attr, invert=False):
     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 :
@@ -520,9 +792,9 @@ def cmp_attr(bug_1, bug_2, attr, invert=False):
 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")
+cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings")
 # chronological rankings (newer < older)
 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
 
@@ -545,7 +817,7 @@ def cmp_comments(bug_1, bug_2):
 
 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)
+     cmp_reporter, cmp_comments, cmp_summary, cmp_uuid, cmp_extra_strings)
 
 class BugCompoundComparator (object):
     def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
@@ -556,7 +828,7 @@ class BugCompoundComparator (object):
             if val != 0 :
                 return val
         return 0
-        
+
 cmp_full = BugCompoundComparator()
 
 
@@ -577,4 +849,5 @@ def cmp_last_modified(bug_1, bug_2):
     return -cmp(val_1, val_2)
 
 
-suite = doctest.DocTestSuite()
+if libbe.TESTING == True:
+    suite = doctest.DocTestSuite()
index 532416315b394e60c6d17f5f2529831c026a5848..65136febf06b75143ecb97f4903f0dba76c657a7 100644 (file)
@@ -1,6 +1,7 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
 #                         Alexander Belchenko <bialix@ukr.net>
 #                         Chris Ball <cjb@laptop.org>
+#                         Gianluca Montecchi <gian@grys.it>
 #                         Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
 #                         W. Trevor King <wking@drexel.edu>
 #
 # 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.
+"""Define the :class:`BugDir` class for storing a collection of bugs.
 """
 
 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)
+
+import libbe
+import libbe.storage as storage
+from libbe.storage.util.properties import Property, doc_property, \
+    local_property, defaulting_property, checked_property, \
+    fn_checked_property, cached_property, primed_property, \
+    change_hook_property, settings_property
+import libbe.storage.util.settings_object as settings_object
+import libbe.storage.util.mapfile as mapfile
+import libbe.bug as bug
+import libbe.util.utility as utility
+import libbe.util.id
+
+if libbe.TESTING == True:
+    import doctest
+    import sys
+    import unittest
+
+    import libbe.storage.base
+
+
+class NoBugMatches(libbe.util.id.NoIDMatches):
+    def __init__(self, *args, **kwargs):
+        libbe.util.id.NoIDMatches.__init__(self, *args, **kwargs)
+    def __str__(self):
+        if self.msg == None:
+            return 'No bug matches %s' % self.id
+        return 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.
+    """A BugDir is a container for :class:`~libbe.bug.Bug`\s, with some
+    additional attributes.
+
+    Parameters
+    ----------
+    storage : :class:`~libbe.storage.base.Storage`
+       Storage instance containing the bug directory.  If
+       `from_storage` is `False`, `storage` may be `None`.
+    uuid : str, optional
+       Set the bugdir UUID (see :mod:`libbe.util.id`).
+       Useful if you are loading one of several bugdirs
+       stored in a single Storage instance.
+    from_storage : bool, optional
+       If `True`, attempt to load from storage.  Otherwise,
+       setup in memory, saving to `storage` if it is not `None`.
+
+    See Also
+    --------
+    :class:`SimpleBugDir` for some bugdir manipulation exampes.
     """
 
     settings_properties = []
@@ -165,104 +96,6 @@ class BugDir (list, settings_object.SavedSettingsObject):
                          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 <jdoe@example.com>'.  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 <jdoe@example.com>'.""")
-    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/<some_function>.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)
@@ -292,348 +125,143 @@ settings easy.  Don't set this attribute.  Set .vcs instead, and
                          change_hook=_set_inactive_status)
     def inactive_status(): 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/<some_function>.py.",
+                         default=[],
+                         check_fn=_extra_strings_check_fn,
+                         change_hook=_extra_strings_change_hook,
+                         mutable=True)
+    def extra_strings(): 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):
-                self.root = None
-                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 cleanup(self):
-        self.vcs.cleanup()
+    def _bug_map_gen(self):
+        map = {}
+        for bug in self:
+            map[bug.uuid] = bug
+        for uuid in self.uuids():
+            if uuid not in map:
+                map[uuid] = None
+        self._bug_map_value = map # ._bug_map_value used by @local_property
 
-    # methods for getting the BugDir situated in the filesystem
+    @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 _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):
-            self.root = None
-            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
+    def __init__(self, storage, uuid=None, from_storage=False):
+        list.__init__(self)
+        settings_object.SavedSettingsObject.__init__(self)
+        self.storage = storage
+        self.id = libbe.util.id.ID(self, 'bugdir')
+        self.uuid = uuid
+        if from_storage == True:
+            if self.uuid == None:
+                self.uuid = [c for c in self.storage.children()
+                             if c != 'version'][0]
+            self.load_settings()
         else:
-            beroot = utility.search_parent_directories(path, ".be")
-            if beroot == None:
-                self.root = 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
+            if self.uuid == None:
+                self.uuid = libbe.util.id.uuid_gen()
+            if self.storage != None and self.storage.is_writeable():
+                self.save()
 
     # 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")
+    def load_settings(self, settings_mapfile=None):
+        if settings_mapfile == None:
+            settings_mapfile = \
+                self.storage.get(self.id.storage('settings'), default='\n')
         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)
+            settings = mapfile.parse(settings_mapfile)
+        except mapfile.InvalidMapfileContents, e:
+            raise Exception('Invalid settings file for bugdir %s\n'
+                            '(BE version missmatch?)' % self.id.user())
+        self._setup_saved_settings(settings)
         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()
+        mf = mapfile.generate(self._get_saved_settings())
+        self.storage.set(self.id.storage('settings'), mf)
 
     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():
+        for uuid in self.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.
+        Save any loaded contents to storage.  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.
+        However, if self.storage.is_writeable() == True, then any
+        changes are automatically written to storage as soon as they
+        happen, so calling this method will just waste time (unless
+        something else has been messing with your stored files).
         """
-        sync_with_disk = self.sync_with_disk
-        if sync_with_disk == False:
-            self.set_sync_with_disk(True)
-        self.set_version()
+        self.storage.add(self.id.storage(), directory=True)
+        self.storage.add(self.id.storage('settings'), parent=self.id.storage(),
+                         directory=False)
         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 uuids(self, use_cached_disk_uuids=True):
+        if use_cached_disk_uuids==False or not hasattr(self, '_uuids_cache'):
+            self._uuids_cache = []
+            # list bugs that are in storage
+            if self.storage != None and self.storage.is_readable():
+                child_uuids = libbe.util.id.child_uuids(
+                    self.storage.children(self.id.storage()))
+                for id in child_uuids:
+                    self._uuids_cache.append(id)
+        return list(set([bug.uuid for bug in self] + self._uuids_cache))
 
     def _clear_bugs(self):
         while len(self) > 0:
             self.pop()
+        if hasattr(self, '_uuids_cache'):
+            del(self._uuids_cache)
         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)
+        bg = bug.Bug(bugdir=self, uuid=uuid, from_storage=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()
+    def new_bug(self, summary=None, _uuid=None):
+        bg = bug.Bug(bugdir=self, uuid=_uuid, summary=summary,
+                     from_storage=False)
         self.append(bg)
         self._bug_map_gen()
+        if hasattr(self, '_uuids_cache') and not bg.uuid in self._uuids_cache:
+            self._uuids_cache.append(bg.uuid)
         return bg
 
     def remove_bug(self, bug):
+        if hasattr(self, '_uuids_cache') and bug.uuid in self._uuids_cache:
+            self._uuids_cache.remove(bug.uuid)
         self.remove(bug)
-        if bug.sync_with_disk == True:
+        if self.storage != None and self.storage.is_writeable():
             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)
-        <class 'libbe.bug.Bug'>
-        >>> 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))
+            raise NoBugMatches(
+                uuid, self.uuids(),
+                'No bug matches %s in %s' % (uuid, self.storage))
         if self._bug_map[uuid] == None:
             self._load_bug(uuid)
         return self._bug_map[uuid]
@@ -645,188 +273,291 @@ settings easy.  Don't set this attribute.  Set .vcs instead, and
                 return False
         return True
 
+    # methods for id generation
+
+    def sibling_uuids(self):
+        return []
 
-class SimpleBugDir (BugDir):
+class RevisionedBugDir (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()
+    RevisionedBugDirs are read-only copies used for generating
+    diffs between revisions.
     """
-    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.cleanup() removes dir.
-            self._dir_ref = dir
-        bug_a = self.new_bug("a", summary="Bug A")
-        bug_a.creator = "John Doe <jdoe@example.com>"
-        bug_a.time = 0
-        bug_b = self.new_bug("b", summary="Bug B")
-        bug_b.creator = "Jane Doe <jdoe@example.com>"
-        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)
+    def __init__(self, bugdir, revision):
+        storage_version = bugdir.storage.storage_version(revision)
+        if storage_version != libbe.storage.STORAGE_VERSION:
+            raise libbe.storage.InvalidStorageVersion(storage_version)
+        s = copy.deepcopy(bugdir.storage)
+        s.writeable = False
+        class RevisionedStorage (object):
+            def __init__(self, storage, default_revision):
+                self.s = storage
+                self.sget = self.s.get
+                self.sancestors = self.s.ancestors
+                self.schildren = self.s.children
+                self.schanged = self.s.changed
+                self.r = default_revision
+            def get(self, *args, **kwargs):
+                if not 'revision' in kwargs or kwargs['revision'] == None:
+                    kwargs['revision'] = self.r
+                return self.sget(*args, **kwargs)
+            def ancestors(self, *args, **kwargs):
+                print 'getting ancestors', args, kwargs
+                if not 'revision' in kwargs or kwargs['revision'] == None:
+                    kwargs['revision'] = self.r
+                ret = self.sancestors(*args, **kwargs)
+                print 'got ancestors', ret
+                return ret
+            def children(self, *args, **kwargs):
+                if not 'revision' in kwargs or kwargs['revision'] == None:
+                    kwargs['revision'] = self.r
+                return self.schildren(*args, **kwargs)
+            def changed(self, *args, **kwargs):
+                if not 'revision' in kwargs or kwargs['revision'] == None:
+                    kwargs['revision'] = self.r
+                return self.schanged(*args, **kwargs)
+        rs = RevisionedStorage(s, revision)
+        s.get = rs.get
+        s.ancestors = rs.ancestors
+        s.children = rs.children
+        s.changed = rs.changed
+        BugDir.__init__(self, s, from_storage=True)
+        self.revision = revision
+    def changed(self):
+        return self.storage.changed()
+    
+
+if libbe.TESTING == True:
+    class SimpleBugDir (BugDir):
+        """
+        For testing.  Set ``memory=True`` for a memory-only bugdir.
+
+        >>> bugdir = SimpleBugDir()
+        >>> uuids = list(bugdir.uuids())
+        >>> uuids.sort()
+        >>> print uuids
+        ['a', 'b']
+        >>> bugdir.cleanup()
+        """
+        def __init__(self, memory=True, versioned=False):
+            if memory == True:
+                storage = None
             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()])
+                dir = utility.Dir()
+                self._dir_ref = dir # postpone cleanup since dir.cleanup() removes dir.
+                if versioned == False:
+                    storage = libbe.storage.base.Storage(dir.path)
+                else:
+                    storage = libbe.storage.base.VersionedStorage(dir.path)
+                storage.init()
+                storage.connect()
+            BugDir.__init__(self, storage=storage, uuid='abc123')
+            bug_a = self.new_bug(summary='Bug A', _uuid='a')
+            bug_a.creator = 'John Doe <jdoe@example.com>'
+            bug_a.time = 0
+            bug_b = self.new_bug(summary='Bug B', _uuid='b')
+            bug_b.creator = 'Jane Doe <jdoe@example.com>'
+            bug_b.time = 0
+            bug_b.status = 'closed'
+            if self.storage != None:
+                self.storage.disconnect() # flush to storage
+                self.storage.connect()
+
+        def cleanup(self):
+            if self.storage != None:
+                self.storage.writeable = True
+                self.storage.disconnect()
+                self.storage.destroy()
+            if hasattr(self, '_dir_ref'):
+                self._dir_ref.cleanup()
+
+        def flush_reload(self):
+            if self.storage != None:
+                self.storage.disconnect()
+                self.storage.connect()
+                self._clear_bugs()
+
+#    class BugDirTestCase(unittest.TestCase):
+#        def setUp(self):
+#            self.dir = utility.Dir()
+#            self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False,
+#                                 allow_storage_init=True)
+#            self.storage = self.bugdir.storage
+#        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.storage != None and self.storage.versioned == False:
+#                return
+#            original = self.bugdir.storage.commit("Began versioning")
+#            bugA = self.bugdir.bug_from_uuid("a")
+#            bugA.status = "fixed"
+#            self.bugdir.save()
+#            new = self.storage.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.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.storage = libbe.storage.base.Storage(self.dir.path)
+            self.storage.init()
+            self.storage.connect()
+            self.bugdir = BugDir(self.storage)
+            self.bugdir.new_bug(summary="Hopefully not imported",
+                                _uuid="preexisting")
+            self.storage.disconnect()
+            self.storage.connect()
+        def tearDown(self):
+            if self.storage != None:
+                self.storage.disconnect()
+                self.storage.destroy()
+            self.dir.cleanup()
+        def testOnDiskCleanLoad(self):
+            """
+            SimpleBugDir(memory==False) should not import
+            preexisting bugs.
+            """
+            bugdir = SimpleBugDir(memory=False)
+            self.failUnless(bugdir.storage.is_readable() == True,
+                            bugdir.storage.is_readable())
+            self.failUnless(bugdir.storage.is_writeable() == True,
+                            bugdir.storage.is_writeable())
+            uuids = sorted([bug.uuid for bug in bugdir])
+            self.failUnless(uuids == ['a', 'b'], uuids)
+            bugdir.flush_reload()
+            uuids = sorted(bugdir.uuids())
+            self.failUnless(uuids == ['a', 'b'], uuids)
+            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(memory==True) should not import
+            preexisting bugs.
+            """
+            bugdir = SimpleBugDir(memory=True)
+            self.failUnless(bugdir.storage == None, bugdir.storage)
+            uuids = sorted([bug.uuid for bug in bugdir])
+            self.failUnless(uuids == ['a', 'b'], uuids)
+            uuids = sorted([bug.uuid for bug in bugdir])
+            self.failUnless(uuids == ['a', 'b'], uuids)
+            bugdir._clear_bugs()
+            uuids = sorted(bugdir.uuids())
+            self.failUnless(uuids == [], uuids)
+            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()])
+
+#    def _get_settings(self, settings_path, for_duplicate_bugdir=False):
+#        allow_no_storage = not self.storage.path_in_root(settings_path)
+#        if allow_no_storage == 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.storage, settings_path, allow_no_storage)
+#        except storage.NoSuchFile:
+#            settings = {"storage_name": "None"}
+#        return settings
+
+#    def _save_settings(self, settings_path, settings,
+#                       for_duplicate_bugdir=False):
+#        allow_no_storage = not self.storage.path_in_root(settings_path)
+#        if allow_no_storage == 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.storage.mkdir(self.get_path(), allow_no_storage)
+#        mapfile.map_save(self.storage, settings_path, settings, allow_no_storage)
diff --git a/libbe/bzr.py b/libbe/bzr.py
deleted file mode 100644 (file)
index ed9e032..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Ben Finney <ben+python@benfinney.id.au>
-#                         Marien Zwart <marienz@gentoo.org>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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_version(self):
-        status,output,error = self._u_invoke_client("--version")
-        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)
-
-\f    
-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
deleted file mode 100644 (file)
index 9b64142..0000000
+++ /dev/null
@@ -1,233 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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("<module 'becommands.list' from ")
-    True
-    """
-    cmd = plugin.get_plugin("becommands", command_name.replace("-", "_"))
-    if cmd is None:
-        raise UnknownCommand(command_name)
-    return cmd
-
-
-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],
-                      manipulate_encodings=manipulate_encodings)
-    if ret == None:
-        ret = 0
-    return ret
-
-def help(cmd=None, parser=None):
-    if cmd != None:
-        return get_command(cmd).help()
-    else:
-        cmdlist = []
-        for name, module in iter_commands():
-            cmdlist.append((name, module.__desc__))
-        longest_cmd_len = max([len(name) for name,desc in cmdlist])
-        ret = ["Bugs Everywhere - Distributed bug tracking",
-               "", "Supported commands"]
-        for name, desc in cmdlist:
-            numExtraSpaces = longest_cmd_len-len(name)
-            ret.append("be %s%*s    %s" % (name, numExtraSpaces, "", desc))
-        ret.extend(["", "Run", "  be help [command]", "for more information."])
-        longhelp = "\n".join(ret)
-        if parser == None:
-            return longhelp
-        return parser.help_str() + "\n" + longhelp
-
-def completions(cmd):
-    parser = get_command(cmd).get_parser()
-    longopts = []
-    for opt in parser.option_list:
-        longopts.append(opt.get_opt_string())
-    return longopts
-
-def raise_get_help(option, opt, value, parser):
-    raise GetHelp
-
-def raise_get_completions(option, opt, value, parser):
-    print "got completion arg"
-    if hasattr(parser, "command") and parser.command == "be":
-        comps = []
-        for command, module in iter_commands():
-            comps.append(command)
-        for opt in parser.option_list:
-            comps.append(opt.get_opt_string())
-        raise GetCompletions(comps)
-    raise GetCompletions(completions(sys.argv[1]))
-
-class CmdOptionParser(optparse.OptionParser):
-    def __init__(self, usage):
-        optparse.OptionParser.__init__(self, usage)
-        self.disable_interspersed_args()
-        self.remove_option("-h")
-        self.add_option("-h", "--help", action="callback", 
-                        callback=raise_get_help, help="Print a help message")
-        self.add_option("--complete", action="callback",
-                        callback=raise_get_completions,
-                        help="Print a list of available completions")
-
-    def error(self, message):
-        raise UsageError(message)
-
-    def iter_options(self):
-        return iter_combine([self._short_opt.iterkeys(), 
-                            self._long_opt.iterkeys()])
-
-    def help_str(self):
-        f = StringIO()
-        self.print_help(f)
-        return f.getvalue()
-
-def option_value_pairs(options, parser):
-    """
-    Iterate through OptionParser (option, value) pairs.
-    """
-    for option in [o.dest for o in parser.option_list if o.dest != None]:
-        value = getattr(options, option)
-        yield (option, value)
-
-def default_complete(options, args, parser, bugid_args={}):
-    """
-    A dud complete implementation for becommands so that the
-    --complete argument doesn't cause any problems.  Use this
-    until you've set up a command-specific complete function.
-    
-    bugid_args is an optional dict where the keys are positional
-    arguments taking bug shortnames and the values are functions for
-    filtering, since that's a common enough operation.
-    e.g. for "be open [options] BUGID"
-      bugid_args = {0: lambda bug : bug.active == False}
-    A positional argument of -1 specifies all remaining arguments
-    (e.g in the case of "be show BUGID BUGID ...").
-    """
-    for option,value in option_value_pairs(options, parser):
-        if value == "--complete":
-            raise GetCompletions()
-    if len(bugid_args.keys()) > 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/libbe/command/__init__.py b/libbe/command/__init__.py
new file mode 100644 (file)
index 0000000..0c8d4ff
--- /dev/null
@@ -0,0 +1,40 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import base
+
+UserError = base.UserError
+UnknownCommand = base.UnknownCommand
+get_command = base.get_command
+get_command_class = base.get_command_class
+commands = base.commands
+Option = base.Option
+Argument = base.Argument
+Command = base.Command
+InputOutput = base.InputOutput
+StdInputOutput = base.StdInputOutput
+StringInputOutput = base.StringInputOutput
+UnconnectedStorageGetter = base.UnconnectedStorageGetter
+StorageCallbacks = base.StorageCallbacks
+UserInterface = base.UserInterface
+
+__all__ = [UserError, UnknownCommand,
+           get_command, get_command_class, commands,
+           Option, Argument, Command,
+           InputOutput, StdInputOutput, StringInputOutput,
+           StorageCallbacks, UnconnectedStorageGetter,
+           UserInterface]
diff --git a/libbe/command/assign.py b/libbe/command/assign.py
new file mode 100644 (file)
index 0000000..6abf05e
--- /dev/null
@@ -0,0 +1,98 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Marien Zwart <marienz@gentoo.org>
+#                         Thomas Gerigk <tgerigk@gmx.de>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import libbe
+import libbe.command
+import libbe.command.util
+
+
+class Assign (libbe.command.Command):
+    u"""Assign an individual or group to fix a bug
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = Assign(ui=ui)
+
+    >>> bd.bug_from_uuid('a').assigned is None
+    True
+    >>> ui._user_id = u'Fran\xe7ois'
+    >>> ret = ui.run(cmd, args=['-', '/a'])
+    >>> bd.flush_reload()
+    >>> bd.bug_from_uuid('a').assigned
+    u'Fran\\xe7ois'
+
+    >>> ret = ui.run(cmd, args=['someone', '/a', '/b'])
+    >>> bd.flush_reload()
+    >>> bd.bug_from_uuid('a').assigned
+    'someone'
+    >>> bd.bug_from_uuid('b').assigned
+    'someone'
+
+    >>> ret = ui.run(cmd, args=['none', '/a'])
+    >>> bd.flush_reload()
+    >>> bd.bug_from_uuid('a').assigned is None
+    True
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'assign'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.args.extend([
+                libbe.command.Argument(
+                    name='assigned', metavar='ASSIGNED', default=None,
+                    completion_callback=libbe.command.util.complete_assigned),
+                libbe.command.Argument(
+                    name='bug-id', metavar='BUG-ID', default=None,
+                    repeatable=True,
+                    completion_callback=libbe.command.util.complete_bug_id),
+                ])
+
+    def _run(self, **params):
+        assigned = params['assigned']
+        if assigned == 'none':
+            assigned = None
+        elif assigned == '-':
+            assigned = self._get_user_id()
+        bugdir = self._get_bugdir()
+        for bug_id in params['bug-id']:
+            bug,dummy_comment = \
+                libbe.command.util.bug_comment_from_user_id(bugdir, bug_id)
+            if bug.assigned != assigned:
+                bug.assigned = assigned
+        return 0
+
+    def _long_help(self):
+        return """
+Assign a person to fix a bug.
+
+Assigneds should be the person's Bugs Everywhere identity, the same
+string that appears in Creator fields.
+
+Special assigned strings:
+  "-"      assign the bug to yourself
+  "none"   un-assigns the bug
+"""
diff --git a/libbe/command/base.py b/libbe/command/base.py
new file mode 100644 (file)
index 0000000..6b1c050
--- /dev/null
@@ -0,0 +1,554 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import codecs
+import optparse
+import os.path
+import StringIO
+import sys
+
+import libbe
+import libbe.storage
+import libbe.ui.util.user
+import libbe.util.encoding
+import libbe.util.plugin
+
+class UserError(Exception):
+    pass
+
+class UnknownCommand(UserError):
+    def __init__(self, cmd):
+        Exception.__init__(self, "Unknown command '%s'" % cmd)
+        self.cmd = cmd
+
+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("<module 'libbe.command.list' from ")
+    True
+    """
+    try:
+        cmd = libbe.util.plugin.import_by_name(
+            'libbe.command.%s' % command_name.replace("-", "_"))
+    except ImportError, e:
+        raise UnknownCommand(command_name)
+    return cmd
+
+def get_command_class(module=None, command_name=None):
+    """Retrieves a command class from a module.
+
+    >>> import_xml_mod = get_command('import-xml')
+    >>> import_xml = get_command_class(import_xml_mod, 'import-xml')
+    >>> repr(import_xml)
+    "<class 'libbe.command.import_xml.Import_XML'>"
+    >>> import_xml = get_command_class(command_name='import-xml')
+    >>> repr(import_xml)
+    "<class 'libbe.command.import_xml.Import_XML'>"
+    """
+    if module == None:
+        module = get_command(command_name)
+    try:
+        cname = command_name.capitalize().replace('-', '_')
+        cmd = getattr(module, cname)
+    except ImportError, e:
+        raise UnknownCommand(command_name)
+    return cmd
+
+def modname_to_command_name(modname):
+    """Little hack to replicate
+    >>> import sys
+    >>> def real_modname_to_command_name(modname):
+    ...     mod = libbe.util.plugin.import_by_name(
+    ...         'libbe.command.%s' % modname)
+    ...     attrs = [getattr(mod, name) for name in dir(mod)]
+    ...     commands = []
+    ...     for attr_name in dir(mod):
+    ...         attr = getattr(mod, attr_name)
+    ...         try:
+    ...             if issubclass(attr, Command):
+    ...                 commands.append(attr)
+    ...         except TypeError, e:
+    ...             pass
+    ...     if len(commands) == 0:
+    ...         raise Exception('No Command classes in %s' % dir(mod))
+    ...     return commands[0].name
+    >>> real_modname_to_command_name('new')
+    'new'
+    >>> real_modname_to_command_name('import_xml')
+    'import-xml'
+    """
+    return modname.replace('_', '-')
+
+def commands(command_names=False):
+    for modname in libbe.util.plugin.modnames('libbe.command'):
+        if modname not in ['base', 'util']:
+            if command_names == False:
+                yield modname
+            else:
+                yield modname_to_command_name(modname)
+
+class CommandInput (object):
+    def __init__(self, name, help=''):
+        self.name = name
+        self.help = help
+
+    def __str__(self):
+        return '<%s %s>' % (self.__class__.__name__, self.name)
+
+    def __repr__(self):
+        return self.__str__()
+
+class Argument (CommandInput):
+    def __init__(self, metavar=None, default=None, type='string',
+                 optional=False, repeatable=False,
+                 completion_callback=None, *args, **kwargs):
+        CommandInput.__init__(self, *args, **kwargs)
+        self.metavar = metavar
+        self.default = default
+        self.type = type
+        self.optional = optional
+        self.repeatable = repeatable
+        self.completion_callback = completion_callback
+        if self.metavar == None:
+            self.metavar = self.name.upper()
+
+class Option (CommandInput):
+    def __init__(self, callback=None, short_name=None, arg=None,
+                 *args, **kwargs):
+        CommandInput.__init__(self, *args, **kwargs)
+        self.callback = callback
+        self.short_name = short_name
+        self.arg = arg
+        if self.arg == None and self.callback == None:
+            # use an implicit boolean argument
+            self.arg = Argument(name=self.name, help=self.help,
+                                default=False, type='bool')
+        self.validate()
+
+    def validate(self):
+        if self.arg == None:
+            assert self.callback != None, self.name
+            return
+        assert self.callback == None, '%s: %s' (self.name, self.callback)
+        assert self.arg.name == self.name, \
+            'Name missmatch: %s != %s' % (self.arg.name, self.name)
+        assert self.arg.optional == False, self.name
+        assert self.arg.repeatable == False, self.name
+
+    def __str__(self):
+        return '--%s' % self.name
+
+    def __repr__(self):
+        return '<Option %s>' % self.__str__()
+
+class _DummyParser (optparse.OptionParser):
+    def __init__(self, command):
+        optparse.OptionParser.__init__(self)
+        self.remove_option('-h')
+        self.command = command
+        self._command_opts = []
+        for option in self.command.options:
+            self._add_option(option)
+
+    def _add_option(self, option):
+        # from libbe.ui.command_line.CmdOptionParser._add_option
+        option.validate()
+        long_opt = '--%s' % option.name
+        if option.short_name != None:
+            short_opt = '-%s' % option.short_name
+        assert '_' not in option.name, \
+            'Non-reconstructable option name %s' % option.name
+        kwargs = {'dest':option.name.replace('-', '_'),
+                  'help':option.help}
+        if option.arg == None or option.arg.type == 'bool':
+            kwargs['action'] = 'store_true'
+            kwargs['metavar'] = None
+            kwargs['default'] = False
+        else:
+            kwargs['type'] = option.arg.type
+            kwargs['action'] = 'store'
+            kwargs['metavar'] = option.arg.metavar
+            kwargs['default'] = option.arg.default
+        if option.short_name != None:
+            opt = optparse.Option(short_opt, long_opt, **kwargs)
+        else:
+            opt = optparse.Option(long_opt, **kwargs)
+        #option.takes_value = lambda : option.arg != None
+        opt._option = option
+        self._command_opts.append(opt)
+        self.add_option(opt)
+
+class OptionFormatter (optparse.IndentedHelpFormatter):
+    def __init__(self, command):
+        optparse.IndentedHelpFormatter.__init__(self)
+        self.command = command
+    def option_help(self):
+        # based on optparse.OptionParser.format_option_help()
+        parser = _DummyParser(self.command)
+        self.store_option_strings(parser)
+        ret = []
+        ret.append(self.format_heading('Options'))
+        self.indent()
+        for option in parser._command_opts:
+            ret.append(self.format_option(option))
+            ret.append('\n')
+        self.dedent()
+        # Drop the last '\n', or the header if no options or option groups:
+        return ''.join(ret[:-1])
+
+class Command (object):
+    """One-line command description here.
+
+    >>> c = Command()
+    >>> print c.help()
+    usage: be command [options]
+    <BLANKLINE>
+    Options:
+      -h, --help  Print a help message.
+    <BLANKLINE>
+      --complete  Print a list of possible completions.
+    <BLANKLINE>
+    A detailed help message.
+    """
+
+    name = 'command'
+
+    def __init__(self, ui=None):
+        self.ui = ui # calling user-interface
+        self.status = None
+        self.result = None
+        self.restrict_file_access = True
+        self.options = [
+            Option(name='help', short_name='h',
+                help='Print a help message.',
+                callback=self.help),
+            Option(name='complete',
+                help='Print a list of possible completions.',
+                callback=self.complete),
+                ]
+        self.args = []
+
+    def run(self, options=None, args=None):
+        self.status = 1 # in case we raise an exception
+        params = self._parse_options_args(options, args)
+        if params['help'] == True:
+            pass
+        else:
+            params.pop('help')
+        if params['complete'] != None:
+            pass
+        else:
+            params.pop('complete')
+
+        self.status = self._run(**params)
+        return self.status
+
+    def _parse_options_args(self, options=None, args=None):
+        if options == None:
+            options = {}
+        if args == None:
+            args = []
+        params = {}
+        for option in self.options:
+            assert option.name not in params, params[option.name]
+            if option.name in options:
+                params[option.name] = options.pop(option.name)
+            elif option.arg != None:
+                params[option.name] = option.arg.default
+            else: # non-arg options are flags, set to default flag value
+                params[option.name] = False
+        assert 'user-id' not in params, params['user-id']
+        if 'user-id' in options:
+            self._user_id = options.pop('user-id')
+        if len(options) > 0:
+            raise UserError, 'Invalid option passed to command %s:\n  %s' \
+                % (self.name, '\n  '.join(['%s: %s' % (k,v)
+                                           for k,v in options.items()]))
+        in_optional_args = False
+        for i,arg in enumerate(self.args):
+            if arg.repeatable == True:
+                assert i == len(self.args)-1, arg.name
+            if in_optional_args == True:
+                assert arg.optional == True, arg.name
+            else:
+                in_optional_args = arg.optional
+            if i < len(args):
+                if arg.repeatable == True:
+                    params[arg.name] = [args[i]]
+                else:
+                    params[arg.name] = args[i]
+            else:  # no value given
+                assert in_optional_args == True, arg.name
+                params[arg.name] = arg.default
+        if len(args) > len(self.args):  # add some additional repeats
+            assert self.args[-1].repeatable == True, self.args[-1].name
+            params[self.args[-1].name].extend(args[len(self.args):])
+        return params
+
+    def _run(self, **kwargs):
+        raise NotImplementedError
+
+    def help(self, *args):
+        return '\n\n'.join([self.usage(),
+                            self._option_help(),
+                            self._long_help().rstrip('\n')])
+
+    def usage(self):
+        usage = 'usage: be %s [options]' % self.name
+        num_optional = 0
+        for arg in self.args:
+            usage += ' '
+            if arg.optional == True:
+                usage += '['
+                num_optional += 1
+            usage += arg.metavar
+            if arg.repeatable == True:
+                usage += ' ...'
+        usage += ']'*num_optional
+        return usage
+
+    def _option_help(self):
+        o = OptionFormatter(self)
+        return o.option_help().strip('\n')
+
+    def _long_help(self):
+        return "A detailed help message."
+
+    def complete(self, argument=None, fragment=None):
+        if argument == None:
+            ret = ['--%s' % o.name for o in self.options]
+            if len(self.args) > 0 and self.args[0].completion_callback != None:
+                ret.extend(self.args[0].completion_callback(self, argument, fragment))
+            return ret
+        elif argument.completion_callback != None:
+            # finish a particular argument
+            return argument.completion_callback(self, argument, fragment)
+        return [] # the particular argument doesn't supply completion info
+
+    def _check_restricted_access(self, storage, path):
+        """
+        Check that the file at path is inside bugdir.root.  This is
+        important if you allow other users to execute becommands with
+        your username (e.g. if you're running be-handle-mail through
+        your ~/.procmailrc).  If this check wasn't made, a user could
+        e.g.  run
+          be commit -b ~/.ssh/id_rsa "Hack to expose ssh key"
+        which would expose your ssh key to anyone who could read the
+        VCS log.
+
+        >>> class DummyStorage (object): pass
+        >>> s = DummyStorage()
+        >>> s.repo = os.path.expanduser('~/x/')
+        >>> c = Command()
+        >>> try:
+        ...     c._check_restricted_access(s, os.path.expanduser('~/.ssh/id_rsa'))
+        ... except UserError, e:
+        ...     assert str(e).startswith('file access restricted!'), str(e)
+        ...     print 'we got the expected error'
+        we got the expected error
+        >>> c._check_restricted_access(s, os.path.expanduser('~/x'))
+        >>> c._check_restricted_access(s, os.path.expanduser('~/x/y'))
+        >>> c.restrict_file_access = False
+        >>> c._check_restricted_access(s, os.path.expanduser('~/.ssh/id_rsa'))
+        """
+        if self.restrict_file_access == True:
+            path = os.path.abspath(path)
+            repo = os.path.abspath(storage.repo).rstrip(os.path.sep)
+            if path == repo or path.startswith(repo+os.path.sep):
+                return
+            raise UserError('file access restricted!\n  %s not in %s'
+                            % (path, repo))
+
+    def cleanup(self):
+        pass
+
+class InputOutput (object):
+    def __init__(self, stdin=None, stdout=None):
+        self.stdin = stdin
+        self.stdout = stdout
+
+    def setup_command(self, command):
+        if not hasattr(self.stdin, 'encoding'):
+            self.stdin.encoding = libbe.util.encoding.get_input_encoding()
+        if not hasattr(self.stdout, 'encoding'):
+            self.stdout.encoding = libbe.util.encoding.get_output_encoding()
+        command.stdin = self.stdin
+        command.stdin.encoding = self.stdin.encoding
+        command.stdout = self.stdout
+        command.stdout.encoding = self.stdout.encoding
+
+    def cleanup(self):
+        pass
+
+class StdInputOutput (InputOutput):
+    def __init__(self, input_encoding=None, output_encoding=None):
+        stdin,stdout = self._get_io(input_encoding, output_encoding)
+        InputOutput.__init__(self, stdin, stdout)
+
+    def _get_io(self, input_encoding=None, output_encoding=None):
+        if input_encoding == None:
+            input_encoding = libbe.util.encoding.get_input_encoding()
+        if output_encoding == None:
+            output_encoding = libbe.util.encoding.get_output_encoding()
+        stdin = codecs.getwriter(input_encoding)(sys.stdin)
+        stdin.encoding = input_encoding
+        stdout = codecs.getwriter(output_encoding)(sys.stdout)
+        stdout.encoding = output_encoding
+        return (stdin, stdout)
+
+class StringInputOutput (InputOutput):
+    """
+    >>> s = StringInputOutput()
+    >>> s.set_stdin('hello')
+    >>> s.stdin.read()
+    'hello'
+    >>> s.stdin.read()
+    ''
+    >>> print >> s.stdout, 'goodbye'
+    >>> s.get_stdout()
+    'goodbye\\n'
+    >>> s.get_stdout()
+    ''
+
+    Also works with unicode strings
+
+    >>> s.set_stdin(u'hello')
+    >>> s.stdin.read()
+    u'hello'
+    >>> print >> s.stdout, u'goodbye'
+    >>> s.get_stdout()
+    u'goodbye\\n'
+    """
+    def __init__(self):
+        stdin = StringIO.StringIO()
+        stdin.encoding = 'utf-8'
+        stdout = StringIO.StringIO()
+        stdout.encoding = 'utf-8'
+        InputOutput.__init__(self, stdin, stdout)
+
+    def set_stdin(self, stdin_string):
+        self.stdin = StringIO.StringIO(stdin_string)
+
+    def get_stdout(self):
+        ret = self.stdout.getvalue()
+        self.stdout = StringIO.StringIO() # clear stdout for next read
+        self.stdin.encoding = 'utf-8'
+        return ret
+
+class UnconnectedStorageGetter (object):
+    def __init__(self, location):
+        self.location = location
+
+    def __call__(self):
+        return libbe.storage.get_storage(self.location)
+
+class StorageCallbacks (object):
+    def __init__(self, location=None):
+        if location == None:
+            location = '.'
+        self.location = location
+        self._get_unconnected_storage = UnconnectedStorageGetter(location)
+
+    def setup_command(self, command):
+        command._get_unconnected_storage = self.get_unconnected_storage
+        command._get_storage = self.get_storage
+        command._get_bugdir = self.get_bugdir
+
+    def get_unconnected_storage(self):
+        """
+        Callback for use by commands that need it.
+        
+        The returned Storage instance is may actually be connected,
+        but commands that make use of the returned value should only
+        make use of non-connected Storage methods.  This is mainly
+        intended for the init command, which calls Storage.init().
+        """
+        if not hasattr(self, '_unconnected_storage'):
+            if self._get_unconnected_storage == None:
+                raise NotImplementedError
+            self._unconnected_storage = self._get_unconnected_storage()
+        return self._unconnected_storage
+
+    def set_unconnected_storage(self, unconnected_storage):
+        self._unconnected_storage = unconnected_storage
+
+    def get_storage(self):
+        """Callback for use by commands that need it."""
+        if not hasattr(self, '_storage'):
+            self._storage = self.get_unconnected_storage()
+            self._storage.connect()
+            version = self._storage.storage_version()
+            if version != libbe.storage.STORAGE_VERSION:
+                raise libbe.storage.InvalidStorageVersion(version)
+        return self._storage
+
+    def set_storage(self, storage):
+        self._storage = storage
+
+    def get_bugdir(self):
+        """Callback for use by commands that need it."""
+        if not hasattr(self, '_bugdir'):
+            self._bugdir = libbe.bugdir.BugDir(self.get_storage(),
+                                               from_storage=True)
+        return self._bugdir
+
+    def set_bugdir(self, bugdir):
+        self._bugdir = bugdir
+
+    def cleanup(self):
+        if hasattr(self, '_storage'):
+            self._storage.disconnect()
+
+class UserInterface (object):
+    def __init__(self, io=None, location=None):
+        if io == None:
+            io = StringInputOutput()
+        self.io = io
+        self.storage_callbacks = StorageCallbacks(location)
+        self.restrict_file_access = True
+
+    def help(self):
+        raise NotImplementedError
+
+    def run(self, command, options=None, args=None):
+        self.setup_command(command)
+        return command.run(options, args)
+
+    def setup_command(self, command):
+        if command.ui == None:
+            command.ui = self
+        if self.io != None:
+            self.io.setup_command(command)
+        if self.storage_callbacks != None:
+            self.storage_callbacks.setup_command(command)        
+        command.restrict_file_access = self.restrict_file_access
+        command._get_user_id = self._get_user_id
+
+    def _get_user_id(self):
+        """Callback for use by commands that need it."""
+        if not hasattr(self, '_user_id'):
+            self._user_id = libbe.ui.util.user.get_user_id(
+                self.storage_callbacks.get_storage())
+        return self._user_id
+
+    def cleanup(self):
+        self.storage_callbacks.cleanup()
+        self.io.cleanup()
similarity index 89%
rename from becommands/close.py
rename to libbe/command/close.py
index 0532ed2de463d89e90f90119590ff6e3e525e507..026c605a0351517f99ae622708ced662f83e5cf7 100644 (file)
@@ -1,4 +1,5 @@
 # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
 #                         Marien Zwart <marienz@gentoo.org>
 #                         Thomas Gerigk <tgerigk@gmx.de>
 #                         W. Trevor King <wking@drexel.edu>
@@ -20,7 +21,8 @@
 from libbe import cmdutil, bugdir
 __desc__ = __doc__
 
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False,
+            dir="."):
     """
     >>> from libbe import bugdir
     >>> import os
@@ -43,8 +45,9 @@ def execute(args, manipulate_encodings=True):
     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])
+                       manipulate_encodings=manipulate_encodings,
+                       root=dir)
+    bug = cmdutil.bug_from_id(bd, args[0])
     bug.status = "closed"
     bd.save()
 
diff --git a/libbe/command/comment.py b/libbe/command/comment.py
new file mode 100644 (file)
index 0000000..5bf6acf
--- /dev/null
@@ -0,0 +1,169 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+import sys
+
+import libbe
+import libbe.command
+import libbe.command.util
+import libbe.comment
+import libbe.ui.util.editor
+import libbe.util.id
+
+
+class Comment (libbe.command.Command):
+    """Add a comment to a bug
+
+    >>> import time
+    >>> import libbe.bugdir
+    >>> import libbe.util.id
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = Comment(ui=ui)
+
+    >>> uuid_gen = libbe.util.id.uuid_gen
+    >>> libbe.util.id.uuid_gen = lambda: 'X'
+    >>> ui._user_id = u'Fran\\xe7ois'
+    >>> ret = ui.run(cmd, args=['/a', 'This is a comment about a'])
+    Created comment with ID abc/a/X
+    >>> libbe.util.id.uuid_gen = uuid_gen
+    >>> bd.flush_reload()
+    >>> bug = bd.bug_from_uuid('a')
+    >>> bug.load_comments(load_full=False)
+    >>> comment = bug.comment_root[0]
+    >>> comment.id.storage() == comment.uuid
+    True
+    >>> print comment.body
+    This is a comment about a
+    <BLANKLINE>
+    >>> comment.author
+    u'Fran\\xe7ois'
+    >>> comment.time <= int(time.time())
+    True
+    >>> comment.in_reply_to is None
+    True
+
+    >>> if 'EDITOR' in os.environ:
+    ...     del os.environ['EDITOR']
+    >>> if 'VISUAL' in os.environ:
+    ...     del os.environ['VISUAL']
+    >>> ui._user_id = u'Frank'
+    >>> ret = ui.run(cmd, args=['/b'])
+    Traceback (most recent call last):
+    UserError: No comment supplied, and EDITOR not specified.
+
+    >>> os.environ['EDITOR'] = "echo 'I like cheese' > "
+    >>> libbe.util.id.uuid_gen = lambda: 'Y'
+    >>> ret = ui.run(cmd, args=['/b'])
+    Created comment with ID abc/b/Y
+    >>> libbe.util.id.uuid_gen = uuid_gen
+    >>> bd.flush_reload()
+    >>> bug = bd.bug_from_uuid('b')
+    >>> bug.load_comments(load_full=False)
+    >>> comment = bug.comment_root[0]
+    >>> print comment.body
+    I like cheese
+    <BLANKLINE>
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    >>> del os.environ["EDITOR"]
+    """
+    name = 'comment'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='author', short_name='a',
+                    help='Set the comment author',
+                    arg=libbe.command.Argument(
+                        name='author', metavar='AUTHOR')),
+                libbe.command.Option(name='alt-id',
+                    help='Set an alternate comment ID',
+                    arg=libbe.command.Argument(
+                        name='alt-id', metavar='ID')),
+                libbe.command.Option(name='content-type', short_name='c',
+                    help='Set comment content-type (e.g. text/plain)',
+                    arg=libbe.command.Argument(name='content-type',
+                        metavar='MIME')),
+                ])
+        self.args.extend([
+                libbe.command.Argument(
+                    name='id', metavar='ID', default=None,
+                    completion_callback=libbe.command.util.complete_bug_comment_id),
+                libbe.command.Argument(
+                    name='comment', metavar='COMMENT', default=None,
+                    optional=True,
+                    completion_callback=libbe.command.util.complete_assigned),
+                ])
+
+    def _run(self, **params):
+        bugdir = self._get_bugdir()
+        bug,parent = \
+            libbe.command.util.bug_comment_from_user_id(bugdir, params['id'])
+        if params['comment'] == None:
+            # 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 = libbe.ui.util.editor.editor_string(estr)
+            except libbe.ui.util.editor.CantFindEditor, e:
+                raise libbe.command.UserError(
+                    'No comment supplied, and EDITOR not specified.')
+            if body is None:
+                raise libbe.command.UserError('No comment entered.')
+        elif params['comment'] == '-': # read body from stdin
+            binary = not (params['content-type'] == None
+                          or params['content-type'].startswith("text/"))
+            if not binary:
+                body = self.stdin.read()
+                if not body.endswith('\n'):
+                    body += '\n'
+            else: # read-in without decoding
+                body = sys.stdin.read()
+        else: # body given on command line
+            body = params['comment']
+            if not body.endswith('\n'):
+                body+='\n'
+        if params['author'] == None:
+            params['author'] = self._get_user_id()
+
+        new = parent.new_reply(body=body, content_type=params['content-type'])
+        for key in ['alt-id', 'author']:
+            if params[key] != None:
+                setattr(new, new._setting_name_to_attr_name(key), params[key])
+        print >> self.stdout, 'Created comment with ID %s' % new.id.user()
+        return 0
+
+    def _long_help(self):
+        return """
+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.
+"""
diff --git a/libbe/command/commit.py b/libbe/command/commit.py
new file mode 100644 (file)
index 0000000..fd15630
--- /dev/null
@@ -0,0 +1,93 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import sys
+
+import libbe
+import libbe.bugdir
+import libbe.command
+import libbe.command.util
+import libbe.storage
+import libbe.ui.util.editor
+
+
+class Commit (libbe.command.Command):
+    """Commit the currently pending changes to the repository
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False, versioned=True)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = Commit(ui=ui)
+
+    >>> bd.extra_strings = ['hi there']
+    >>> bd.flush_reload()
+    >>> ui.run(cmd, args=['Making a commit']) # doctest: +ELLIPSIS
+    Committed ...
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'commit'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='body', short_name='b',
+                    help='Provide the 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)',
+                    arg=libbe.command.Argument(name='body', metavar='FILE',
+                        completion_callback=libbe.command.util.complete_path)),
+                libbe.command.Option(name='allow-empty', short_name='a',
+                                     help='Allow empty commits'),
+                 ])
+        self.args.extend([
+                libbe.command.Argument(
+                    name='comment', metavar='COMMENT', default=None),
+                ])
+
+    def _run(self, **params):
+        if params['comment'] == '-': # read summary from stdin
+            assert params['body'] != 'EDITOR', \
+                'Cannot spawn and editor when the summary is using stdin.'
+            summary = sys.stdin.readline()
+        else:
+            summary = params['comment']
+        storage = self._get_storage()
+        if params['body'] == None:
+            body = None
+        elif params['body'] == 'EDITOR':
+            body = libbe.ui.util.editor.editor_string(
+                'Please enter your commit message above')
+        else:
+            self._check_restricted_access(storage, params['body'])
+            body = libbe.util.encoding.get_file_contents(
+                params['body'], decode=True)
+        try:
+            revision = storage.commit(summary, body=body,
+                                      allow_empty=params['allow-empty'])
+            print >> self.stdout, 'Committed %s' % revision
+        except libbe.storage.EmptyCommit, e:
+            print >> self.stdout, e
+            return 1
+
+    def _long_help(self):
+        return """
+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.
+"""
diff --git a/libbe/command/depend.py b/libbe/command/depend.py
new file mode 100644 (file)
index 0000000..f87657b
--- /dev/null
@@ -0,0 +1,408 @@
+# Copyright (C) 2009-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import copy
+import os
+
+import libbe
+import libbe.bug
+import libbe.command
+import libbe.command.util
+import libbe.util.tree
+
+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
+
+class Depend (libbe.command.Command):
+    """Add/remove bug dependencies
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = Depend(ui=ui)
+
+    >>> ret = ui.run(cmd, args=['/a', '/b'])
+    a blocked by:
+    b
+    >>> ret = ui.run(cmd, args=['/a'])
+    a blocked by:
+    b
+    >>> ret = ui.run(cmd, {'show-status':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+    a blocked by:
+    b closed
+    >>> ret = ui.run(cmd, args=['/b', '/a'])
+    b blocked by:
+    a
+    b blocks:
+    a
+    >>> ret = ui.run(cmd, {'show-status':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+    a blocked by:
+    b closed
+    a blocks:
+    b closed
+    >>> ret = ui.run(cmd, {'repair':True})
+    >>> ret = ui.run(cmd, {'remove':True}, ['/b', '/a'])
+    b blocks:
+    a
+    >>> ret = ui.run(cmd, {'remove':True}, ['/a', '/b'])
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'depend'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='remove', short_name='r',
+                    help='Remove dependency (instead of adding it)'),
+                libbe.command.Option(name='show-status', short_name='s',
+                    help='Show status of blocking bugs'),
+                libbe.command.Option(name='status',
+                    help='Only show bugs matching the STATUS specifier',
+                    arg=libbe.command.Argument(
+                        name='status', metavar='STATUS', default=None,
+                        completion_callback=libbe.command.util.complete_status)),
+                libbe.command.Option(name='severity',
+                    help='Only show bugs matching the SEVERITY specifier',
+                    arg=libbe.command.Argument(
+                        name='severity', metavar='SEVERITY', default=None,
+                        completion_callback=libbe.command.util.complete_severity)),
+                libbe.command.Option(name='tree-depth', short_name='t',
+                    help='Print dependency tree rooted at BUG-ID with DEPTH levels of both blockers and blockees.  Set DEPTH <= 0 to disable the depth limit.',
+                    arg=libbe.command.Argument(
+                        name='tree-depth', metavar='INT', type='int',
+                        completion_callback=libbe.command.util.complete_severity)),
+                libbe.command.Option(name='repair',
+                    help='Check for and repair one-way links'),
+                ])
+        self.args.extend([
+                libbe.command.Argument(
+                    name='bug-id', metavar='BUG-ID', default=None,
+                    optional=True,
+                    completion_callback=libbe.command.util.complete_bug_id),
+                libbe.command.Argument(
+                    name='blocking-bug-id', metavar='BUG-ID', default=None,
+                    optional=True,
+                    completion_callback=libbe.command.util.complete_bug_id),
+                ])
+
+    def _run(self, **params):
+        if params['repair'] == True and params['bug-id'] != None:
+            raise libbe.command.UsageError(
+                'No arguments with --repair calls.')
+        if params['repair'] == False and params['bug-id'] == None:
+            raise libbe.command.UsageError(
+                'Must specify either --repair or a BUG-ID')
+        if params['tree-depth'] != None \
+                and params['blocking-bug-id'] != None:
+            raise libbe.command.UsageError(
+                'Only one bug id used in tree mode.')
+        bugdir = self._get_bugdir()
+        if params['repair'] == True:
+            good,fixed,broken = check_dependencies(bugdir, repair_broken_links=True)
+            assert len(broken) == 0, broken
+            if len(fixed) > 0:
+                print >> self.stdout, 'Fixed the following links:'
+                print >> self.stdout, \
+                    '\n'.join(['%s |-- %s' % (blockee.uuid, blocker.uuid)
+                               for blockee,blocker in fixed])
+            return 0
+        allowed_status_values = \
+            libbe.command.util.select_values(
+                params['status'], libbe.bug.status_values)
+        allowed_severity_values = \
+            libbe.command.util.select_values(
+                params['severity'], libbe.bug.severity_values)
+
+        bugA, dummy_comment = libbe.command.util.bug_comment_from_user_id(
+            bugdir, params['bug-id'])
+
+        if params['tree-depth'] != None:
+            dtree = DependencyTree(bugdir, bugA, params['tree-depth'],
+                                   allowed_status_values,
+                                   allowed_severity_values)
+            if len(dtree.blocked_by_tree()) > 0:
+                print >> self.stdout, '%s blocked by:' % bugA.uuid
+                for depth,node in dtree.blocked_by_tree().thread():
+                    if depth == 0: continue
+                    print >> self.stdout, \
+                        '%s%s' % (' '*(depth),
+                        node.bug.string(shortlist=True))
+            if len(dtree.blocks_tree()) > 0:
+                print >> self.stdout, '%s blocks:' % bugA.uuid
+                for depth,node in dtree.blocks_tree().thread():
+                    if depth == 0: continue
+                    print >> self.stdout, \
+                        '%s%s' % (' '*(depth),
+                        node.bug.string(shortlist=True))
+            return 0
+
+        if params['blocking-bug-id'] != None:
+            bugB,dummy_comment = libbe.command.util.bug_comment_from_user_id(
+                bugdir, params['blocking-bug-id'])
+            if params['remove'] == True:
+                remove_block(bugA, bugB)
+            else: # add the dependency
+                add_block(bugA, bugB)
+
+        blocked_by = get_blocked_by(bugdir, bugA)
+        if len(blocked_by) > 0:
+            print >> self.stdout, '%s blocked by:' % bugA.uuid
+            if params['show-status'] == True:
+                print >> self.stdout, \
+                    '\n'.join(['%s\t%s' % (_bug.uuid, _bug.status)
+                               for _bug in blocked_by])
+            else:
+                print >> self.stdout, \
+                    '\n'.join([_bug.uuid for _bug in blocked_by])
+        blocks = get_blocks(bugdir, bugA)
+        if len(blocks) > 0:
+            print >> self.stdout, '%s blocks:' % bugA.uuid
+            if params['show-status'] == True:
+                print >> self.stdout, \
+                    '\n'.join(['%s\t%s' % (_bug.uuid, _bug.status)
+                               for _bug in blocks])
+            else:
+                print >> self.stdout, \
+                    '\n'.join([_bug.uuid for _bug in blocks])
+        return 0
+
+    def _long_help(self):
+        return """
+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:<your-bug-uuid>
+
+The --status and --severity options allow you to either blacklist or
+whitelist values, for example
+  $ be list --status open,assigned
+will only follow and print dependencies with open or assigned status.
+You select blacklist mode by starting the list with a minus sign, for
+example
+  $ be list --severity -target
+which will only follow and print dependencies with non-target severity.
+
+If neither bug A nor B is specified, check for and repair the missing
+side of 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
+"""
+
+# 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.
+    """
+    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.
+
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir()
+    >>> 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.storage != None:
+        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,
+                 allowed_status_values=None,
+                 allowed_severity_values=None):
+        self.bugdir = bugdir
+        self.root_bug = root_bug
+        self.depth_limit = depth_limit
+        self.allowed_status_values = allowed_status_values
+        self.allowed_severity_values = allowed_severity_values
+
+    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):
+                if self.allowed_status_values != None \
+                        and not bug.status in self.allowed_status_values:
+                    continue
+                if self.allowed_severity_values != None \
+                        and not bug.severity in self.allowed_severity_values:
+                    continue
+                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/libbe/command/diff.py b/libbe/command/diff.py
new file mode 100644 (file)
index 0000000..967ab14
--- /dev/null
@@ -0,0 +1,139 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import libbe
+import libbe.bugdir
+import libbe.bug
+import libbe.command
+import libbe.command.util
+import libbe.storage
+
+import libbe.diff
+
+class Diff (libbe.command.Command):
+    __doc__ = """Compare bug reports with older tree
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False, versioned=True)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = Diff()
+
+    >>> original = bd.storage.commit('Original status')
+    >>> bug = bd.bug_from_uuid('a')
+    >>> bug.status = 'closed'
+    >>> changed = bd.storage.commit('Closed bug a')
+    >>> ret = ui.run(cmd, args=[original])
+    Modified bugs:
+      abc/a:cm: Bug A
+        Changed bug settings:
+          status: open -> closed
+    >>> ret = ui.run(cmd, {'subscribe':'%(bugdir_id)s:mod', 'uuids':True}, [original])
+    a
+    >>> bd.storage.versioned = False
+    >>> ret = ui.run(cmd, args=[original])
+    Traceback (most recent call last):
+      ...
+    UserError: This repository is not revision-controlled.
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """ % {'bugdir_id':libbe.diff.BUGDIR_ID}
+    name = 'diff'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='repo', short_name='r',
+                    help='Compare with repository in REPO instead'
+                         ' of the current repository.',
+                    arg=libbe.command.Argument(
+                        name='repo', metavar='REPO',
+                        completion_callback=libbe.command.util.complete_path)),
+                libbe.command.Option(name='subscribe', short_name='s',
+                    help='Only print changes matching SUBSCRIPTION, '
+                    'subscription is a comma-separated list of ID:TYPE '
+                    'tuples.  See `be subscribe --help` for descriptions '
+                    'of ID and TYPE.',
+                    arg=libbe.command.Argument(
+                        name='subscribe', metavar='SUBSCRIPTION')),
+                libbe.command.Option(name='uuids', short_name='u',
+                    help='Only print the changed bug UUIDS.'),
+                ])
+        self.args.extend([
+                libbe.command.Argument(
+                    name='revision', metavar='REVISION', default=None,
+                    optional=True)
+                ])
+
+    def _run(self, **params):
+        try:
+            subscriptions = libbe.diff.subscriptions_from_string(
+                params['subscribe'])
+        except ValueError, e:
+            raise libbe.command.UserError(e.msg)
+        bugdir = self._get_bugdir()
+        if bugdir.storage.versioned == False:
+            raise libbe.command.UserError(
+                'This repository is not revision-controlled.')
+        if params['repo'] == None:
+            if params['revision'] == None: # get the most recent revision
+                params['revision'] = bugdir.storage.revision_id(-1)
+            old_bd = libbe.bugdir.RevisionedBugDir(bugdir, params['revision'])
+        else:
+            old_storage = libbe.storage.get_storage(params['repo'])
+            old_storage.connect()
+            old_bd_current = libbe.bugdir.BugDir(old_storage, from_disk=True)
+            if params['revision'] == None: # use the current working state
+                old_bd = old_bd_current
+            else:
+                if old_bd_current.storage.versioned == False:
+                    raise libbe.command.UserError(
+                        '%s is not revision-controlled.'
+                        % storage.repo)
+                old_bd = libbe.bugdir.RevisionedBugDir(old_bd_current,revision)
+        d = libbe.diff.Diff(old_bd, bugdir)
+        tree = d.report_tree(subscriptions)
+
+        if params['uuids'] == True:
+            uuids = []
+            bugs = tree.child_by_path('/bugs')
+            for bug_type in bugs:
+                uuids.extend([bug.name for bug in bug_type])
+            print >> self.stdout, '\n'.join(uuids)
+        else :
+            rep = tree.report_string()
+            if rep != None:
+                print >> self.stdout, rep
+        return 0
+
+    def _long_help(self):
+        return """
+Uses the storage backend 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 storage backend.
+
+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 an understanding of the current status.
+"""
diff --git a/libbe/command/due.py b/libbe/command/due.py
new file mode 100644 (file)
index 0000000..4463455
--- /dev/null
@@ -0,0 +1,117 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import libbe
+import libbe.command
+import libbe.command.util
+import libbe.util.utility
+
+
+DUE_TAG = 'DUE:'
+
+
+class Due (libbe.command.Command):
+    """Set bug due dates
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = Due(ui=ui)
+
+    >>> ret = ui.run(cmd, args=['/a'])
+    No due date assigned.
+    >>> ret = ui.run(cmd, args=['/a', 'Thu, 01 Jan 1970 00:00:00 +0000'])
+    >>> ret = ui.run(cmd, args=['/a'])
+    Thu, 01 Jan 1970 00:00:00 +0000
+    >>> ret = ui.run(cmd, args=['/a', 'none'])
+    >>> ret = ui.run(cmd, args=['/a'])
+    No due date assigned.
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'due'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.args.extend([
+                libbe.command.Argument(
+                    name='bug-id', metavar='BUG-ID',
+                    completion_callback=libbe.command.util.complete_bug_id),
+                libbe.command.Argument(
+                    name='due', metavar='DUE', optional=True),
+                ])
+
+    def _run(self, **params):
+        bugdir = self._get_bugdir()
+        bug,dummy_comment = libbe.command.util.bug_comment_from_user_id(
+            bugdir, params['bug-id'])
+        if params['due'] == None:
+            due_time = get_due(bug)
+            if due_time is None:
+                print >> self.stdout, 'No due date assigned.'
+            else:
+                print >> self.stdout, libbe.util.utility.time_to_str(due_time)
+        else:
+            if params['due'] == 'none':
+                remove_due(bug)
+            else:
+                due_time = libbe.util.utility.str_to_time(params['due'])
+                set_due(bug, due_time)
+
+    def _long_help(self):
+        return """
+If no DATE is specified, the bug's current due date is printed.  If
+DATE is specified, it will be assigned to the bug.
+"""
+
+# internal helper functions
+
+def _generate_due_string(time):
+    return "%s%s" % (DUE_TAG, libbe.util.utility.time_to_str(time))
+
+def _parse_due_string(string):
+    assert string.startswith(DUE_TAG)
+    return libbe.util.utility.str_to_time(string[len(DUE_TAG):])
+
+# functions exposed to other modules
+
+def get_due(bug):
+    matched = []
+    for line in bug.extra_strings:
+        if line.startswith(DUE_TAG):
+            matched.append(_parse_due_string(line))
+    if len(matched) == 0:
+        return None
+    if len(matched) > 1:
+        raise Exception('Several due dates for %s?:\n  %s'
+                        % (bug.uuid, '\n  '.join(matched)))
+    return matched[0]
+
+def remove_due(bug):
+    estrs = bug.extra_strings
+    for due_str in [s for s in estrs if s.startswith(DUE_TAG)]:
+        estrs.remove(due_str)
+    bug.extra_strings = estrs # reassign to notice change
+
+def set_due(bug, time):
+    remove_due(bug)
+    estrs = bug.extra_strings
+    estrs.append(_generate_due_string(time))
+    bug.extra_strings = estrs # reassign to notice change
diff --git a/libbe/command/email_bugs.py b/libbe/command/email_bugs.py
new file mode 100644 (file)
index 0000000..f6641e3
--- /dev/null
@@ -0,0 +1,239 @@
+# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
+#
+# 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.
+"""Email specified bugs in a be-handle-mail compatible format."""
+
+import copy
+from cStringIO import StringIO
+from email import Message
+from email.mime.text import MIMEText
+from email.generator import Generator
+import sys
+import time
+
+from libbe import cmdutil, bugdir
+from libbe.subproc import invoke
+from libbe.utility import time_to_str
+from libbe.vcs import detect_vcs, installed_vcs
+import show
+
+__desc__ = __doc__
+
+sendmail='/usr/sbin/sendmail -t'
+
+def execute(args, manipulate_encodings=True, restrict_file_access=False,
+            dir="."):
+    """
+    >>> import os
+    >>> from libbe import bug
+    >>> bd = bugdir.SimpleBugDir()
+    >>> bd.encoding = 'utf-8'
+    >>> os.chdir(bd.root)
+    >>> import email.charset as c
+    >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8')
+    >>> execute(["-o", "--to", "a@b.com", "--from", "b@c.edu", "a", "b"],
+    ...         manipulate_encodings=False) # doctest: +ELLIPSIS
+    Content-Type: text/xml; charset="utf-8"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: quoted-printable
+    From: b@c.edu
+    To: a@b.com
+    Date: ...
+    Subject: [be-bug:xml] Updates to a, b
+    <BLANKLINE>
+    <?xml version=3D"1.0" encoding=3D"utf-8" ?>
+    <be-xml>
+      <version>
+        <tag>...</tag>
+        <branch-nick>...</branch-nick>
+        <revno>...</revno>
+        <revision-id>...
+      </version>
+      <bug>
+        <uuid>a</uuid>
+        <short-name>a</short-name>
+        <severity>minor</severity>
+        <status>open</status>
+        <creator>John Doe &lt;jdoe@example.com&gt;</creator>
+        <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+        <summary>Bug A</summary>
+      </bug>
+      <bug>
+        <uuid>b</uuid>
+        <short-name>b</short-name>
+        <severity>minor</severity>
+        <status>closed</status>
+        <creator>Jane Doe &lt;jdoe@example.com&gt;</creator>
+        <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+        <summary>Bug B</summary>
+      </bug>
+    </be-xml>
+    >>> bd.cleanup()
+
+    Note that the '=3D' bits in
+      <?xml version=3D"1.0" encoding=3D"utf-8" ?>
+    are the way quoted-printable escapes '='.
+    
+    The unclosed <revision-id>... is because revision ids can be long
+    enough to cause line wraps, and we want to ensure we match even if
+    the closing </revision-id> is split by the wrapping.
+    """
+    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,
+                       root=dir)
+    xml = show.output(args, bd, as_xml=True, with_comments=True)
+    subject = options.subject
+    if subject == None:
+        subject = '[be-bug:xml] Updates to %s' % ', '.join(args)
+    submit_email = TextEmail(to_address=options.to_address,
+                             from_address=options.from_address,
+                             subject=subject,
+                             body=xml,
+                             encoding=bd.encoding,
+                             subtype='xml')
+    if options.output == True:
+        print submit_email
+    else:
+        submit_email.send()
+
+def get_parser():
+    parser = cmdutil.CmdOptionParser("be email-bugs [options] ID [ID ...]")
+    parser.add_option("-t", "--to", metavar="EMAIL", dest="to_address",
+                      help="Submission email address (%default)",
+                      default="be-devel@bugseverywhere.org")
+    parser.add_option("-f", "--from", metavar="EMAIL", dest="from_address",
+                      help="Senders email address, overriding auto-generated default",
+                      default=None)
+    parser.add_option("-s", "--subject", metavar="STRING", dest="subject",
+                      help="Subject line, overriding auto-generated default.  If you use this option, remember that be-handle-mail probably want something like '[be-bug:xml] ...'",
+                      default=None)
+    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 functionality.")
+    return parser
+
+longhelp="""
+Email specified bugs in a be-handle-mail compatible format.  This is
+the prefered method for reporting bugs if you did not install bzr by
+branching a bzr repository.
+
+If you _did_ install bzr by branching a bzr repository, we suggest you
+commit any new bug information with
+  bzr commit --message "Reported bug in demuxulizer"
+and then email a bzr merge directive with
+  bzr send --mail-to "be-devel@bugseverywhere.org"
+rather than using this command.
+"""
+
+def help():
+    return get_parser().help_str() + longhelp
+
+class TextEmail (object):
+    """
+    Make it very easy to compose and send single-part text emails.
+    >>> msg = TextEmail(to_address='Monty <monty@a.com>',
+    ...                 from_address='Python <python@b.edu>',
+    ...                 subject='Parrots',
+    ...                 header={'x-special-header':'your info here'},
+    ...                 body="Remarkable bird, id'nit, squire?\\nLovely plumage!")
+    >>> print msg # doctest: +ELLIPSIS
+    Content-Type: text/plain; charset="utf-8"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: base64
+    From: Python <python@b.edu>
+    To: Monty <monty@a.com>
+    Date: ...
+    Subject: Parrots
+    x-special-header: your info here
+    <BLANKLINE>
+    UmVtYXJrYWJsZSBiaXJkLCBpZCduaXQsIHNxdWlyZT8KTG92ZWx5IHBsdW1hZ2Uh
+    <BLANKLINE>
+    >>> import email.charset as c
+    >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8')
+    >>> print msg # doctest: +ELLIPSIS
+    Content-Type: text/plain; charset="utf-8"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: quoted-printable
+    From: Python <python@b.edu>
+    To: Monty <monty@a.com>
+    Date: ...
+    Subject: Parrots
+    x-special-header: your info here
+    <BLANKLINE>
+    Remarkable bird, id'nit, squire?
+    Lovely plumage!
+    """
+    def __init__(self, to_address, from_address=None, subject=None,
+                 header=None, body=None, encoding='utf-8', subtype='plain'):
+        self.to_address = to_address
+        self.from_address = from_address
+        if self.from_address == None:
+            self.from_address = self._guess_from_address()
+        self.subject = subject
+        self.header = header
+        if self.header == None:
+            self.header = {}
+        self.body = body
+        self.encoding = encoding
+        self.subtype = subtype
+    def _guess_from_address(self):
+        vcs = detect_vcs('.')
+        if vcs.name == "None":
+            vcs = installed_vcs()
+        return vcs.get_user_id()
+    def encoded_MIME_body(self):
+        return MIMEText(self.body.encode(self.encoding),
+                        self.subtype,
+                        self.encoding)
+    def message(self):
+        response = self.encoded_MIME_body()
+        response['From'] = self.from_address
+        response['To'] = self.to_address
+        response['Date'] = time_to_str(time.time())
+        response['Subject'] = self.subject
+        for k,v in self.header.items():
+            response[k] = v
+        return response
+    def flatten(self, to_unicode=False):
+        """
+        This is a simplified version of send_pgp_mime.flatten().        
+        """
+        fp = StringIO()
+        g = Generator(fp, mangle_from_=False)
+        g.flatten(self.message())
+        text = fp.getvalue()
+        if to_unicode == True:
+            encoding = msg.get_content_charset() or "utf-8"
+            text = unicode(text, encoding=encoding)
+        return text
+    def __str__(self):
+        return self.flatten()
+    def __unicode__(self):
+        return self.flatten(to_unicode=True)        
+    def send(self, sendmail=None):
+        """
+        This is a simplified version of send_pgp_mime.mail().
+    
+        Send an email Message instance on its merry way by shelling
+        out to the user specified sendmail.
+        """
+        if sendmail == None:
+            sendmail = SENDMAIL
+        invoke(sendmail, stdin=self.flatten())
diff --git a/libbe/command/help.py b/libbe/command/help.py
new file mode 100644 (file)
index 0000000..1fc88f0
--- /dev/null
@@ -0,0 +1,82 @@
+# Copyright (C) 2006-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Thomas Gerigk <tgerigk@gmx.de>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import libbe
+import libbe.command
+import libbe.command.util
+
+TOPICS = {}
+
+class Help (libbe.command.Command):
+    """Print help for given command or topic
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> cmd = Help()
+
+    >>> ret = ui.run(cmd, args=['help'])
+    usage: be help [options] [TOPIC]
+    <BLANKLINE>
+    Options:
+      -h, --help  Print a help message.
+    <BLANKLINE>
+      --complete  Print a list of possible completions.
+    <BLANKLINE>
+    <BLANKLINE>
+    Print help for specified command/topic or list of all commands.
+    """
+    name = 'help'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.args.extend([
+                libbe.command.Argument(
+                    name='topic', metavar='TOPIC', default=None,
+                    optional=True,
+                    completion_callback=self.complete_topic)
+                ])
+
+    def _run(self, **params):
+        if params['topic'] == None:
+            if hasattr(self.ui, 'help'):
+                print >> self.stdout, self.ui.help().rstrip('\n')
+        elif params['topic'] in libbe.command.commands():
+            module = libbe.command.get_command(params['topic'])
+            Class = libbe.command.get_command_class(module,params['topic'])
+            c = Class(ui=self.ui)
+            print >> self.stdout, c.help().rstrip('\n')
+        elif params['topic'] in TOPICS:
+            print >> self.stdout, TOPICS[params['topic']].rstrip('\n')
+        else:
+            raise libbe.command.UserError(
+                '"%s" is neither a command nor topic' % params['topic'])
+        return 0
+
+    def _long_help(self):
+        return """
+Print help for specified command/topic or list of all commands.
+"""
+
+    def complete_topic(self, command, argument, fragment=None):
+        commands = libbe.command.util.complete_command()
+        topics = sorted(TOPICS.keys())
+        return commands + topics
diff --git a/libbe/command/html.py b/libbe/command/html.py
new file mode 100644 (file)
index 0000000..c9f89f3
--- /dev/null
@@ -0,0 +1,719 @@
+# Copyright (C) 2009-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import codecs
+import htmlentitydefs
+import os
+import os.path
+import re
+import string
+import time
+import xml.sax.saxutils
+
+import libbe
+import libbe.command
+import libbe.command.util
+import libbe.comment
+import libbe.util.encoding
+import libbe.util.id
+
+
+class HTML (libbe.command.Command):
+    """Generate a static HTML dump of the current repository status
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = HTML(ui=ui)
+
+    >>> ret = ui.run(cmd, {'output':os.path.join(bd.storage.repo, 'html_export')})
+    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export'))
+    True
+    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index.html'))
+    True
+    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index_inactive.html'))
+    True
+    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs'))
+    True
+    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'a', 'index.html'))
+    True
+    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'b', 'index.html'))
+    True
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'html'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='output', short_name='o',
+                    help='Set the output path (%default)',
+                    arg=libbe.command.Argument(
+                        name='output', metavar='DIR', default='./html_export',
+                        completion_callback=libbe.command.util.complete_path)),
+                libbe.command.Option(name='template-dir', short_name='t',
+                    help='Use a different template.  Defaults to internal templates',
+                    arg=libbe.command.Argument(
+                        name='template-dir', metavar='DIR',
+                        completion_callback=libbe.command.util.complete_path)),
+                libbe.command.Option(name='title',
+                    help='Set the bug repository title (%default)',
+                    arg=libbe.command.Argument(
+                        name='title', metavar='STRING',
+                        default='BugsEverywhere Issue Tracker')),
+                libbe.command.Option(name='index-header',
+                    help='Set the index page headers (%default)',
+                    arg=libbe.command.Argument(
+                        name='index-header', metavar='STRING',
+                        default='BugsEverywhere Bug List')),
+                libbe.command.Option(name='export-template', short_name='e',
+                    help='Export the default template and exit.'),
+                libbe.command.Option(name='export-template-dir', short_name='d',
+                    help='Set the directory for the template export (%default)',
+                    arg=libbe.command.Argument(
+                        name='export-template-dir', metavar='DIR',
+                        default='./default-templates/',
+                        completion_callback=libbe.command.util.complete_path)),
+                libbe.command.Option(name='min-id-length', short_name='l',
+                    help='Attempt to truncate bug and comment IDs to this length.  Set to -1 for non-truncated IDs (%default)',
+                    arg=libbe.command.Argument(
+                        name='min-id-length', metavar='INT',
+                        default=-1, type='int')),
+                libbe.command.Option(name='verbose', short_name='v',
+                    help='Verbose output, default is %default'),
+                ])
+
+    def _run(self, **params):
+        if params['export-template'] == True:
+            html_gen.write_default_template(params['export-template-dir'])
+            return 0
+        bugdir = self._get_bugdir()
+        bugdir.load_all_bugs()
+        html_gen = HTMLGen(bugdir,
+                           template=params['template-dir'],
+                           title=params['title'],
+                           index_header=params['index-header'],
+                           min_id_length=params['min-id-length'],
+                           verbose=params['verbose'],
+                           stdout=self.stdout)
+        html_gen.run(params['output'])
+        return 0
+
+    def _long_help(self):
+        return """
+Generate a set of html pages representing the current state of the bug
+directory.
+"""
+
+Html = HTML # alias for libbe.command.base.get_command_class()
+
+class HTMLGen (object):
+    def __init__(self, bd, template=None,
+                 title="Site Title", index_header="Index Header",
+                 min_id_length=-1,
+                 verbose=False, encoding=None, stdout=None,
+                 ):
+        self.generation_time = time.ctime()
+        self.bd = bd
+        if template == None:
+            self.template = "default"
+        else:
+            self.template = os.path.abspath(os.path.expanduser(template))
+        self.title = title
+        self.index_header = index_header
+        self.verbose = verbose
+        self.stdout = stdout
+        if encoding != None:
+            self.encoding = encoding
+        else:
+            self.encoding = libbe.util.encoding.get_filesystem_encoding()
+        self._load_default_templates()
+        if template != None:
+            self._load_user_templates()
+        self.min_id_length = min_id_length
+
+    def run(self, out_dir):
+        if self.verbose == True:
+            print >> self.stdout, \
+                'Creating the html output in %s using templates in %s' \
+                % (out_dir, self.template)
+
+        bugs_active = []
+        bugs_inactive = []
+        bugs = [b for b in self.bd]
+        bugs.sort()
+        bugs_active = [b for b in bugs if b.active == True]
+        bugs_inactive = [b for b in bugs if b.active != True]
+
+        self._create_output_directories(out_dir)
+        self._write_css_file()
+        for b in bugs:
+            if b.active:
+                up_link = '../../index.html'
+            else:
+                up_link = '../../index_inactive.html'
+            self._write_bug_file(b, up_link)
+        self._write_index_file(
+            bugs_active, title=self.title,
+            index_header=self.index_header, bug_type='active')
+        self._write_index_file(
+            bugs_inactive, title=self.title,
+            index_header=self.index_header, bug_type='inactive')
+
+    def _truncated_bug_id(self, bug):
+        return libbe.util.id._truncate(
+            bug.uuid, bug.sibling_uuids(),
+            min_length=self.min_id_length)
+
+    def _truncated_comment_id(self, comment):
+        return libbe.util.id._truncate(
+            comment.uuid, comment.sibling_uuids(),
+            min_length=self.min_id_length)
+
+    def _create_output_directories(self, out_dir):
+        if self.verbose:
+            print >> self.stdout, 'Creating output directories'
+        self.out_dir = self._make_dir(out_dir)
+        self.out_dir_bugs = self._make_dir(
+            os.path.join(self.out_dir, 'bugs'))
+
+    def _write_css_file(self):
+        if self.verbose:
+            print >> self.stdout, 'Writing css file'
+        assert hasattr(self, 'out_dir'), \
+            'Must run after ._create_output_directories()'
+        self._write_file(self.css_file,
+                         [self.out_dir,'style.css'])
+
+    def _write_bug_file(self, bug, up_link):
+        if self.verbose:
+            print >> self.stdout, '\tCreating bug file for %s' % bug.id.user()
+        assert hasattr(self, 'out_dir_bugs'), \
+            'Must run after ._create_output_directories()'
+
+        bug.load_comments(load_full=True)
+        comment_entries = self._generate_bug_comment_entries(bug)
+        dirname = self._truncated_bug_id(bug)
+        fullpath = os.path.join(self.out_dir_bugs, dirname, 'index.html')
+        template_info = {'title':self.title,
+                         'charset':self.encoding,
+                         'up_link':up_link,
+                         'shortname':bug.id.user(),
+                         'comment_entries':comment_entries,
+                         'generation_time':self.generation_time}
+        for attr in ['uuid', 'severity', 'status', 'assigned',
+                     'reporter', 'creator', 'time_string', 'summary']:
+            template_info[attr] = self._escape(getattr(bug, attr))
+        fulldir = os.path.join(self.out_dir_bugs, dirname)
+        if not os.path.exists(fulldir):
+            os.mkdir(fulldir)
+        self._write_file(self.bug_file % template_info, [fullpath])
+
+    def _generate_bug_comment_entries(self, bug):
+        assert hasattr(self, 'out_dir_bugs'), \
+            'Must run after ._create_output_directories()'
+
+        stack = []
+        comment_entries = []
+        bug.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
+        for depth,comment in bug.comment_root.thread(flatten=False):
+            while len(stack) > depth:
+                # pop non-parents off the stack
+                stack.pop(-1)
+                # close non-parent <div class="comment...
+                comment_entries.append('</div>\n')
+            assert len(stack) == depth
+            stack.append(comment)
+            template_info = {
+                'shortname': comment.id.user(),
+                'truncated_id': self._truncated_comment_id(comment)}
+            if depth == 0:
+                comment_entries.append('<div class="comment root">')
+            else:
+                comment_entries.append(
+                    '<div class="comment" id="%s">'
+                    % template_info['truncated_id'])
+            for attr in ['uuid', 'author', 'date', 'body']:
+                value = getattr(comment, attr)
+                if attr == 'body':
+                    link_long_ids = False
+                    save_body = False
+                    if comment.content_type == 'text/html':
+                        link_long_ids = True
+                    elif comment.content_type.startswith('text/'):
+                        value = '<pre>\n'+self._escape(value)+'\n</pre>'
+                        link_long_ids = True
+                    elif comment.content_type.startswith('image/'):
+                        save_body = True
+                        value = '<img src="./%s/%s" />' \
+                            % (self._truncated_bug_id(bug),
+                               self._truncated_comment_id(comment))
+                    else:
+                        save_body = True
+                        value = '<a href="./%s/%s">Link to %s file</a>.' \
+                            % (self._truncated_bug_id(bug),
+                               self._truncated_comment_id(comment),
+                               comment.content_type)
+                    if link_long_ids == True:
+                        value = self._long_to_linked_user(value)
+                    if save_body == True:
+                        per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
+                        if not os.path.exists(per_bug_dir):
+                            os.mkdir(per_bug_dir)
+                        comment_path = os.path.join(per_bug_dir, comment.uuid)
+                        self._write_file(
+                            '<Files %s>\n  ForceType %s\n</Files>' \
+                                % (comment.uuid, comment.content_type),
+                            [per_bug_dir, '.htaccess'], mode='a')
+                        self._write_file(comment.body,
+                            [per_bug_dir, comment.uuid], mode='wb')
+                else:
+                    value = self._escape(value)
+                template_info[attr] = value
+            comment_entries.append(self.bug_comment_entry % template_info)
+        while len(stack) > 0:
+            stack.pop(-1)
+            comment_entries.append('</div>\n') # close every remaining <div class='comment...
+        return '\n'.join(comment_entries)
+
+    def _long_to_linked_user(self, text):
+        """
+        >>> import libbe.bugdir
+        >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+        >>> h = HTMLGen(bd)
+        >>> h._long_to_linked_user('A link #abc123/a#, and a non-link #x#y#.')
+        'A link <a href="./a/">abc/a</a>, and a non-link #x#y#.'
+        >>> bd.cleanup()
+        """
+        replacer = libbe.util.id.IDreplacer(
+            [self.bd], self._long_to_linked_user_replacer, wrap=False)
+        return re.sub(
+            libbe.util.id.REGEXP, replacer, text)
+
+    def _long_to_linked_user_replacer(self, bugdirs, long_id):
+        """
+        >>> import libbe.bugdir
+        >>> import libbe.util.id
+        >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+        >>> a = bd.bug_from_uuid('a')
+        >>> uuid_gen = libbe.util.id.uuid_gen
+        >>> libbe.util.id.uuid_gen = lambda : '0123'
+        >>> c = a.new_comment('comment for link testing')
+        >>> libbe.util.id.uuid_gen = uuid_gen
+        >>> c.uuid
+        '0123'
+        >>> h = HTMLGen(bd)
+        >>> h._long_to_linked_user_replacer([bd], 'abc123')
+        '#abc123#'
+        >>> h._long_to_linked_user_replacer([bd], 'abc123/a')
+        '<a href="./a/">abc/a</a>'
+        >>> h._long_to_linked_user_replacer([bd], 'abc123/a/0123')
+        '<a href="./a/#0123">abc/a/012</a>'
+        >>> h._long_to_linked_user_replacer([bd], 'x')
+        '#x#'
+        >>> h._long_to_linked_user_replacer([bd], '')
+        '##'
+        >>> bd.cleanup()
+        """
+        try:
+            p = libbe.util.id.parse_user(bugdirs[0], long_id)
+        except (libbe.util.id.MultipleIDMatches,
+                libbe.util.id.NoIDMatches,
+                libbe.util.id.InvalidIDStructure), e:
+            return '#%s#' % long_id # re-wrap failures
+        if p['type'] == 'bugdir':
+            return '#%s#' % long_id
+        elif p['type'] == 'bug':
+            bug,comment = libbe.command.util.bug_comment_from_user_id(
+                bugdirs[0], long_id)
+            return '<a href="./%s/">%s</a>' \
+                % (self._truncated_bug_id(bug), bug.id.user())
+        elif p['type'] == 'comment':
+            bug,comment = libbe.command.util.bug_comment_from_user_id(
+                bugdirs[0], long_id)
+            return '<a href="./%s/#%s">%s</a>' \
+                % (self._truncated_bug_id(bug),
+                   self._truncated_comment_id(comment),
+                   comment.id.user())
+        raise Exception('Invalid id type %s for "%s"'
+                        % (p['type'], long_id))
+
+    def _write_index_file(self, bugs, title, index_header, bug_type='active'):
+        if self.verbose:
+            print >> self.stdout, 'Writing %s index file for %d bugs' % (bug_type, len(bugs))
+        assert hasattr(self, 'out_dir'), 'Must run after ._create_output_directories()'
+        esc = self._escape
+
+        bug_entries = self._generate_index_bug_entries(bugs)
+
+        if bug_type == 'active':
+            filename = 'index.html'
+        elif bug_type == 'inactive':
+            filename = 'index_inactive.html'
+        else:
+            raise Exception, 'Unrecognized bug_type: "%s"' % bug_type
+        template_info = {'title':title,
+                         'index_header':index_header,
+                         'charset':self.encoding,
+                         'active_class':'tab sel',
+                         'inactive_class':'tab nsel',
+                         'bug_entries':bug_entries,
+                         'generation_time':self.generation_time}
+        if bug_type == 'inactive':
+            template_info['active_class'] = 'tab nsel'
+            template_info['inactive_class'] = 'tab sel'
+
+        self._write_file(self.index_file % template_info,
+                         [self.out_dir, filename])
+
+    def _generate_index_bug_entries(self, bugs):
+        bug_entries = []
+        for bug in bugs:
+            if self.verbose:
+                print >> self.stdout, '\tCreating bug entry for %s' % bug.id.user()
+            template_info = {'shortname':bug.id.user()}
+            for attr in ['uuid', 'severity', 'status', 'assigned',
+                         'reporter', 'creator', 'time_string', 'summary']:
+                template_info[attr] = self._escape(getattr(bug, attr))
+            template_info['dir'] = self._truncated_bug_id(bug)
+            bug_entries.append(self.index_bug_entry % template_info)
+        return '\n'.join(bug_entries)
+
+    def _escape(self, string):
+        if string == None:
+            return ''
+        return xml.sax.saxutils.escape(string)
+
+    def _load_user_templates(self):
+        for filename,attr in [('style.css','css_file'),
+                              ('index_file.tpl','index_file'),
+                              ('index_bug_entry.tpl','index_bug_entry'),
+                              ('bug_file.tpl','bug_file'),
+                              ('bug_comment_entry.tpl','bug_comment_entry')]:
+            fullpath = os.path.join(self.template, filename)
+            if os.path.exists(fullpath):
+                setattr(self, attr, self._read_file([fullpath]))
+
+    def _make_dir(self, dir_path):
+        dir_path = os.path.abspath(os.path.expanduser(dir_path))
+        if not os.path.exists(dir_path):
+            try:
+                os.makedirs(dir_path)
+            except:
+                raise libbe.command.UserError(
+                    'Cannot create output directory "%s".' % dir_path)
+        return dir_path
+
+    def _write_file(self, content, path_array, mode='w'):
+        return libbe.util.encoding.set_file_contents(
+            os.path.join(*path_array), content, mode, self.encoding)
+
+    def _read_file(self, path_array, mode='r'):
+        return libbe.util.encoding.get_file_contents(
+            os.path.join(*path_array), mode, self.encoding, decode=True)
+
+    def write_default_template(self, out_dir):
+        if self.verbose:
+            print >> self.stdout, 'Creating output directories'
+        self.out_dir = self._make_dir(out_dir)
+        if self.verbose:
+            print >> self.stdout, 'Creating css file'
+        self._write_css_file()
+        if self.verbose:
+            print >> self.stdout, 'Creating index_file.tpl file'
+        self._write_file(self.index_file,
+                         [self.out_dir, 'index_file.tpl'])
+        if self.verbose:
+            print >> self.stdout, 'Creating index_bug_entry.tpl file'
+        self._write_file(self.index_bug_entry,
+                         [self.out_dir, 'index_bug_entry.tpl'])
+        if self.verbose:
+            print >> self.stdout, 'Creating bug_file.tpl file'
+        self._write_file(self.bug_file,
+                         [self.out_dir, 'bug_file.tpl'])
+        if self.verbose:
+            print >> self.stdout, 'Creating bug_comment_entry.tpl file'
+        self._write_file(self.bug_comment_entry,
+                         [self.out_dir, 'bug_comment_entry.tpl'])
+
+    def _load_default_templates(self):
+        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;
+            }
+
+            div.footer {
+              font-size: small;
+              padding-left: 20px;
+              padding-right: 20px;
+              padding-top: 5px;
+              padding-bottom: 5px;
+              margin: auto;
+              background: #305275;
+              color: #fffee7;
+            }
+
+            table {
+              border-style: solid;
+              border: 10px #313131;
+              border-spacing: 0;
+              width: auto;
+            }
+
+            tb { border: 1px; }
+
+            tr {
+              vertical-align: top;
+              width: auto;
+            }
+
+            td {
+              border-width: 0;
+              border-style: none;
+              padding-right: 0.5em;
+              padding-left: 0.5em;
+              width: auto;
+            }
+
+            img { border-style: none; }
+
+            h1 {
+              padding: 0.5em;
+              background-color: #305275;
+              margin-top: 0;
+              margin-bottom: 0;
+              color: #fff;
+              margin-left: -20px;
+              margin-right: -20px;
+            }
+
+            ul {
+              list-style-type: none;
+              padding: 0;
+            }
+
+            p { width: auto; }
+
+            a, a:visited {
+              background: inherit;
+              text-decoration: none;
+            }
+
+            a { color: #003d41; }
+            a:visited { color: #553d41; }
+            .footer a { color: #508d91; }
+
+            /* bug index pages */
+
+            td.tab {
+              padding-right: 1em;
+              padding-left: 1em;
+            }
+
+            td.sel.tab {
+              background-color: #afafaf;
+              border: 1px solid #afafaf;
+              font-weight:bold;
+            }
+
+            td.nsel.tab { border: 0px; }
+
+            table.bug_list {
+              background-color: #afafaf;
+              border: 2px solid #afafaf;
+            }
+
+            .bug_list tr { width: auto; }
+            tr.wishlist { background-color: #B4FF9B; }
+            tr.minor { background-color: #FCFF98; }
+            tr.serious { background-color: #FFB648; }
+            tr.critical { background-color: #FF752A; }
+            tr.fatal { background-color: #FF3300; }
+
+            /* bug detail pages */
+
+            td.bug_detail_label { text-align: right; }
+            td.bug_detail { }
+            td.bug_comment_label { text-align: right; vertical-align: top; }
+            td.bug_comment { }
+
+            div.comment {
+              padding: 20px;
+              padding-top: 20px;
+              margin: auto;
+              margin-top: 0;
+            }
+
+            div.root.comment {
+              padding: 0px;
+              /* padding-top: 0px; */
+              padding-bottom: 20px;
+            }
+       """
+
+        self.index_file = """
+            <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+              "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+            <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+            <head>
+            <title>%(title)s</title>
+            <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
+            <link rel="stylesheet" href="style.css" type="text/css" />
+            </head>
+            <body>
+
+            <div class="main">
+            <h1>%(index_header)s</h1>
+            <p></p>
+            <table>
+
+            <tr>
+            <td class="%(active_class)s"><a href="index.html">Active Bugs</a></td>
+            <td class="%(inactive_class)s"><a href="index_inactive.html">Inactive Bugs</a></td>
+            </tr>
+
+            </table>
+            <table class="bug_list">
+            <tbody>
+
+            %(bug_entries)s
+
+            </tbody>
+            </table>
+            </div>
+
+            <div class="footer">
+            <p>Generated by <a href="http://www.bugseverywhere.org/">
+            BugsEverywhere</a> on %(generation_time)s</p>
+            <p>
+            <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
+            <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
+            </p>
+            </div>
+
+            </body>
+            </html>
+        """
+
+        self.index_bug_entry ="""
+            <tr class="%(severity)s">
+              <td><a href="bugs/%(dir)s/">%(shortname)s</a></td>
+              <td><a href="bugs/%(dir)s/">%(status)s</a></td>
+              <td><a href="bugs/%(dir)s/">%(severity)s</a></td>
+              <td><a href="bugs/%(dir)s/">%(summary)s</a></td>
+              <td><a href="bugs/%(dir)s/">%(time_string)s</a></td>
+            </tr>
+        """
+
+        self.bug_file = """
+            <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+              "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+            <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+            <head>
+            <title>%(title)s</title>
+            <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
+            <link rel="stylesheet" href="../../style.css" type="text/css" />
+            </head>
+            <body>
+
+            <div class="main">
+            <h1>BugsEverywhere Bug List</h1>
+            <h5><a href="%(up_link)s">Back to Index</a></h5>
+            <h2>Bug: %(shortname)s</h2>
+            <table>
+            <tbody>
+
+            <tr><td class="bug_detail_label">ID :</td>
+                <td class="bug_detail">%(uuid)s</td></tr>
+            <tr><td class="bug_detail_label">Short name :</td>
+                <td class="bug_detail">%(shortname)s</td></tr>
+            <tr><td class="bug_detail_label">Status :</td>
+                <td class="bug_detail">%(status)s</td></tr>
+            <tr><td class="bug_detail_label">Severity :</td>
+                <td class="bug_detail">%(severity)s</td></tr>
+            <tr><td class="bug_detail_label">Assigned :</td>
+                <td class="bug_detail">%(assigned)s</td></tr>
+            <tr><td class="bug_detail_label">Reporter :</td>
+                <td class="bug_detail">%(reporter)s</td></tr>
+            <tr><td class="bug_detail_label">Creator :</td>
+                <td class="bug_detail">%(creator)s</td></tr>
+            <tr><td class="bug_detail_label">Created :</td>
+                <td class="bug_detail">%(time_string)s</td></tr>
+            <tr><td class="bug_detail_label">Summary :</td>
+                <td class="bug_detail">%(summary)s</td></tr>
+            </tbody>
+            </table>
+
+            <hr/>
+
+            %(comment_entries)s
+
+            </div>
+            <h5><a href="%(up_link)s">Back to Index</a></h5>
+
+            <div class="footer">
+            <p>Generated by <a href="http://www.bugseverywhere.org/">
+            BugsEverywhere</a> on %(generation_time)s</p>
+            <p>
+            <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
+            <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
+            </p>
+            </div>
+
+            </body>
+            </html>
+        """
+
+        self.bug_comment_entry ="""
+            <table>
+            <tr>
+              <td class="bug_comment_label">Comment:</td>
+              <td class="bug_comment">
+            --------- Comment ---------<br/>
+            ID: %(uuid)s<br/>
+            Short name: %(shortname)s<br/>
+            From: %(author)s<br/>
+            Date: %(date)s<br/>
+            <br/>
+            %(body)s
+              </td>
+            </tr>
+            </table>
+        """
+
+        # strip leading whitespace
+        for attr in ['css_file', 'index_file', 'index_bug_entry', 'bug_file',
+                     'bug_comment_entry']:
+            value = getattr(self, attr)
+            value = value.replace('\n'+' '*12, '\n')
+            setattr(self, attr, value.strip()+'\n')
diff --git a/libbe/command/import_xml.py b/libbe/command/import_xml.py
new file mode 100644 (file)
index 0000000..287d8b7
--- /dev/null
@@ -0,0 +1,541 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import copy
+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
+
+import libbe
+import libbe.bug
+import libbe.command
+import libbe.command.util
+import libbe.comment
+import libbe.util.encoding
+import libbe.util.utility
+
+if libbe.TESTING == True:
+    import doctest
+    import StringIO
+    import unittest
+
+    import libbe.bugdir
+
+class Import_XML (libbe.command.Command):
+    """Import comments and bugs from XML
+
+    >>> import time
+    >>> import StringIO
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = Import_XML(ui=ui)
+
+    >>> ui.io.set_stdin('<be-xml><comment><uuid>c</uuid><body>This is a comment about a</body></comment></be-xml>')
+    >>> ret = ui.run(cmd, {'comment-root':'/a'}, ['-'])
+    >>> bd.flush_reload()
+    >>> bug = bd.bug_from_uuid('a')
+    >>> bug.load_comments(load_full=False)
+    >>> comment = bug.comment_root[0]
+    >>> print comment.body
+    This is a comment about a
+    <BLANKLINE>
+    >>> comment.time <= int(time.time())
+    True
+    >>> comment.in_reply_to is None
+    True
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'import-xml'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='ignore-missing-references', short_name='i',
+                    help="If any comment's <in-reply-to> refers to a non-existent comment, ignore it (instead of raising an exception)."),
+                libbe.command.Option(name='add-only', short_name='a',
+                    help='If any bug or comment listed in the XML file already exists in the bug repository, do not alter the repository version.'),
+                libbe.command.Option(name='comment-root', short_name='c',
+                    help='Supply a bug or comment ID as the root of any <comment> elements that are direct children of the <be-xml> element.  If any such <comment> elements exist, you are required to set this option.',
+                    arg=libbe.command.Argument(
+                        name='comment-root', metavar='ID',
+                        completion_callback=libbe.command.util.complete_bug_comment_id)),
+                ])
+        self.args.extend([
+                libbe.command.Argument(
+                    name='xml-file', metavar='XML-FILE'),
+                ])
+
+    def _run(self, **params):
+        bugdir = self._get_bugdir()
+        writeable = bugdir.storage.writeable
+        bugdir.storage.writeable = False
+        if params['comment-root'] != None:
+            croot_bug,croot_comment = \
+                libbe.command.util.bug_comment_from_user_id(
+                    bugdir, params['comment-root'])
+            croot_bug.load_comments(load_full=True)
+            if croot_comment.uuid == libbe.comment.INVALID_UUID:
+                croot_comment = croot_bug.comment_root
+            else:
+                croot_comment = croot_bug.comment_from_uuid(croot_comment.uuid)
+            new_croot_bug = libbe.bug.Bug(bugdir=bugdir, uuid=croot_bug.uuid)
+            new_croot_bug.explicit_attrs = []
+            new_croot_bug.comment_root = copy.deepcopy(croot_bug.comment_root)
+            if croot_comment.uuid == libbe.comment.INVALID_UUID:
+                new_croot_comment = new_croot_bug.comment_root
+            else:
+                new_croot_comment = \
+                    new_croot_bug.comment_from_uuid(croot_comment.uuid)
+            for new in new_croot_bug.comments():
+                new.explicit_attrs = []
+        else:
+            croot_bug,croot_comment = (None, None)
+
+        if params['xml-file'] == '-':
+            xml = self.stdin.read().encode(self.stdin.encoding)
+        else:
+            self._check_restricted_access(bugdir.storage, params['xml-file'])
+            xml = libbe.util.encoding.get_file_contents(
+                params['xml-file'])
+
+        # parse the xml
+        root_bugs = []
+        root_comments = []
+        version = {}
+        be_xml = ElementTree.XML(xml)
+        if be_xml.tag != 'be-xml':
+            raise libbe.util.utility.InvalidXML(
+                'import-xml', be_xml, 'root element must be <be-xml>')
+        for child in be_xml.getchildren():
+            if child.tag == 'bug':
+                new = libbe.bug.Bug(bugdir=bugdir)
+                new.from_xml(child)
+                root_bugs.append(new)
+            elif child.tag == 'comment':
+                new = libbe.comment.Comment(croot_bug)
+                new.from_xml(child)
+                root_comments.append(new)
+            elif child.tag == 'version':
+                for gchild in child.getchildren():
+                    if child.tag in ['tag', 'nick', 'revision', 'revision-id']:
+                        text = xml.sax.saxutils.unescape(child.text)
+                        text = text.decode('unicode_escape').strip()
+                        version[child.tag] = text
+                    else:
+                        print >> sys.stderr, 'ignoring unknown tag %s in %s' \
+                            % (gchild.tag, child.tag)
+            else:
+                print >> sys.stderr, 'ignoring unknown tag %s in %s' \
+                    % (child.tag, comment_list.tag)
+
+        # merge the new root_comments
+        if params['add-only'] == True:
+            accept_changes = False
+            accept_extra_strings = False
+        else:
+            accept_changes = True
+            accept_extra_strings = True
+        accept_comments = True
+        if len(root_comments) > 0:
+            if croot_bug == None:
+                raise UserError(
+                    '--comment-root option is required for your root comments:\n%s'
+                    % '\n\n'.join([c.string() for c in root_comments]))
+            try:
+                # link new comments
+                new_croot_bug.add_comments(root_comments,
+                                           default_parent=new_croot_comment,
+                                           ignore_missing_references= \
+                                               params['ignore-missing-references'])
+            except libbe.comment.MissingReference, e:
+                raise libbe.command.UserError(e)
+            croot_bug.merge(new_croot_bug, accept_changes=accept_changes,
+                            accept_extra_strings=accept_extra_strings,
+                            accept_comments=accept_comments)
+
+        # merge the new croot_bugs
+        merged_bugs = []
+        old_bugs = []
+        for new in root_bugs:
+            try:
+                old = bugdir.bug_from_uuid(new.alt_id)
+            except KeyError:
+                old = None
+            if old == None:
+                bd.append(new)
+            else:
+                old.load_comments(load_full=True)
+                old.merge(new, accept_changes=accept_changes,
+                          accept_extra_strings=accept_extra_strings,
+                          accept_comments=accept_comments)
+                merged_bugs.append(new)
+                old_bugs.append(old)
+
+        # protect against programmer error causing data loss:
+        if croot_bug != None:
+            comms = []
+            for c in croot_comment.traverse():
+                comms.append(c.uuid)
+                if c.alt_id != None:
+                    comms.append(c.alt_id)
+            if croot_comment.uuid == libbe.comment.INVALID_UUID:
+                root_text = croot_bug.id.user()
+            else:
+                root_text = croot_comment.id.user()
+            for new in root_comments:
+                assert new.uuid in comms or new.alt_id in comms, \
+                    "comment %s (alt: %s) wasn't added to %s" \
+                    % (new.uuid, new.alt_id, root_text)
+        for new in root_bugs:
+            if not new in merged_bugs:
+                assert bugdir.has_bug(new.uuid), \
+                    "bug %s wasn't added" % (new.uuid)
+
+        # save new information
+        bugdir.storage.writeable = writeable
+        if croot_bug != None:
+            croot_bug.save()
+        for new in root_bugs:
+            if not new in merged_bugs:
+                new.save()
+        for old in old_bugs:
+            old.save()
+
+    def _long_help(self):
+        return """
+Import comments and bugs from XMLFILE.  If XMLFILE is '-', the file is
+read from stdin.
+
+This command provides a fallback mechanism for passing bugs between
+repositories, in case the repositories VCSs are incompatible.  If the
+VCSs are compatible, it's better to use their builtin merge/push/pull
+to share this information, as that will preserve a more detailed
+history.
+
+The XML file should be formatted similarly to
+  <be-xml>
+    <version>
+      <tag>1.0.0</tag>
+      <branch-nick>be</branch-nick>
+      <revno>446</revno>
+      <revision-id>a@b.com-20091119214553-iqyw2cpqluww3zna</revision-id>
+    <version>
+    <bug>
+      ...
+      <comment>...</comment>
+      <comment>...</comment>
+    </bug>
+    <bug>...</bug>
+    <comment>...</comment>
+    <comment>...</comment>
+  </be-xml>
+where the ellipses mark output commpatible with Bug.xml() and
+Comment.xml().  Take a look at the output of `be show --xml` for some
+explicit examples.  Unrecognized tags are ignored.  Missing tags are
+left at the default value.  The version tag is not required, but is
+strongly recommended.
+
+The bug and comment UUIDs are always auto-generated, so if you set a
+<uuid> field, but no <alt-id> field, your <uuid> will be used as the
+comment's <alt-id>.  An exception is raised if <alt-id> conflicts with
+an existing comment.  Bugs do not have a permantent alt-id, so they
+the <uuid>s you specify are not saved.  The <uuid>s _are_ used to
+match agains prexisting bug and comment uuids, and comment alt-ids,
+and fields explicitly given in the XML file will replace old versions
+unless the --add-only flag.
+
+*.extra_strings recieves special treatment, and if --add-only is not
+set, the resulting list concatenates both source lists and removes
+repeats.
+
+Here's an example of import activity:
+  Repository
+   bug (uuid=B, creator=John, status=open)
+     estr (don't forget your towel)
+     estr (helps with space travel)
+     com (uuid=C1, author=Jane, body=Hello)
+     com (uuid=C2, author=Jess, body=World)
+  XML
+   bug (uuid=B, status=fixed)
+     estr (don't forget your towel)
+     estr (watch out for flying dolphins)
+     com (uuid=C1, body=So long)
+     com (uuid=C3, author=Jed, body=And thanks)
+  Result
+   bug (uuid=B, creator=John, status=fixed)
+     estr (don't forget your towel)
+     estr (helps with space travel)
+     estr (watch out for flying dolphins)
+     com (uuid=C1, author=Jane, body=So long)
+     com (uuid=C2, author=Jess, body=World)
+     com (uuid=C4, alt-id=C3, author=Jed, body=And thanks)
+  Result, with --add-only
+   bug (uuid=B, creator=John, status=open)
+     estr (don't forget your towel)
+     estr (helps with space travel)
+     com (uuid=C1, author=Jane, body=Hello)
+     com (uuid=C2, author=Jess, body=World)
+     com (uuid=C4, alt-id=C3, author=Jed, body=And thanks)
+
+Examples:
+
+Import comments (e.g. emails from an mbox) and append to bug XYZ
+  $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ -
+Or you can append those emails underneath the prexisting comment XYZ-3
+  $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ-3 -
+
+User creates a new bug
+  user$ be new "The demuxulizer is broken"
+  Created bug with ID 48f
+  user$ be comment 48f
+  <Describe bug>
+  ...
+User exports bug as xml and emails it to the developers
+  user$ be show --xml 48f > 48f.xml
+  user$ cat 48f.xml | mail -s "Demuxulizer bug xml" devs@b.com
+or equivalently (with a slightly fancier be-handle-mail compatible
+email):
+  user$ be email-bugs 48f
+Devs recieve email, and save it's contents as demux-bug.xml
+  dev$ cat demux-bug.xml | be import-xml -
+"""
+
+
+Import_xml = Import_XML # alias for libbe.command.base.get_command_class()
+
+if libbe.TESTING == True:
+    class LonghelpTestCase (unittest.TestCase):
+        """
+        Test import scenarios given in longhelp.
+        """
+        def setUp(self):
+            self.bugdir = libbe.bugdir.SimpleBugDir(memory=False)
+            io = libbe.command.StringInputOutput()
+            self.ui = libbe.command.UserInterface(io=io)
+            self.ui.storage_callbacks.set_storage(self.bugdir.storage)
+            self.cmd = Import_XML(ui=self.ui)
+            self.cmd._storage = self.bugdir.storage
+            self.cmd._setup_io = lambda i_enc,o_enc : None
+            bugA = self.bugdir.bug_from_uuid('a')
+            self.bugdir.remove_bug(bugA)
+            self.bugdir.storage.writeable = False
+            bugB = self.bugdir.bug_from_uuid('b')
+            bugB.creator = 'John'
+            bugB.status = 'open'
+            bugB.extra_strings += ["don't forget your towel"]
+            bugB.extra_strings += ['helps with space travel']
+            comm1 = bugB.comment_root.new_reply(body='Hello\n')
+            comm1.uuid = 'c1'
+            comm1.author = 'Jane'
+            comm2 = bugB.comment_root.new_reply(body='World\n')
+            comm2.uuid = 'c2'
+            comm2.author = 'Jess'
+            self.bugdir.storage.writeable = True
+            bugB.save()
+            self.xml = """
+            <be-xml>
+              <bug>
+                <uuid>b</uuid>
+                <status>fixed</status>
+                <summary>a test bug</summary>
+                <extra-string>don't forget your towel</extra-string>
+                <extra-string>watch out for flying dolphins</extra-string>
+                <comment>
+                  <uuid>c1</uuid>
+                  <body>So long</body>
+                </comment>
+                <comment>
+                  <uuid>c3</uuid>
+                  <author>Jed</author>
+                  <body>And thanks</body>
+                </comment>
+              </bug>
+            </be-xml>
+            """
+            self.root_comment_xml = """
+            <be-xml>
+              <comment>
+                <uuid>c1</uuid>
+                <body>So long</body>
+              </comment>
+              <comment>
+                <uuid>c3</uuid>
+                <author>Jed</author>
+                <body>And thanks</body>
+              </comment>
+            </be-xml>
+            """
+        def tearDown(self):
+            self.bugdir.cleanup()
+            self.ui.cleanup()
+        def _execute(self, xml, params={}, args=[]):
+            self.ui.io.set_stdin(xml)
+            self.ui.run(self.cmd, params, args)
+            self.bugdir.flush_reload()
+        def testCleanBugdir(self):
+            uuids = list(self.bugdir.uuids())
+            self.failUnless(uuids == ['b'], uuids)
+        def testNotAddOnly(self):
+            bugB = self.bugdir.bug_from_uuid('b')
+            self._execute(self.xml, {}, ['-'])
+            uuids = list(self.bugdir.uuids())
+            self.failUnless(uuids == ['b'], uuids)
+            bugB = self.bugdir.bug_from_uuid('b')
+            self.failUnless(bugB.uuid == 'b', bugB.uuid)
+            self.failUnless(bugB.creator == 'John', bugB.creator)
+            self.failUnless(bugB.status == 'fixed', bugB.status)
+            self.failUnless(bugB.summary == 'a test bug', bugB.summary)
+            estrs = ["don't forget your towel",
+                     'helps with space travel',
+                     'watch out for flying dolphins']
+            self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings)
+            comments = list(bugB.comments())
+            self.failUnless(len(comments) == 3,
+                            ['%s (%s, %s)' % (c.uuid, c.alt_id, c.body)
+                             for c in comments])
+            c1 = bugB.comment_from_uuid('c1')
+            comments.remove(c1)
+            self.failUnless(c1.uuid == 'c1', c1.uuid)
+            self.failUnless(c1.alt_id == None, c1.alt_id)
+            self.failUnless(c1.author == 'Jane', c1.author)
+            self.failUnless(c1.body == 'So long\n', c1.body)
+            c2 = bugB.comment_from_uuid('c2')
+            comments.remove(c2)
+            self.failUnless(c2.uuid == 'c2', c2.uuid)
+            self.failUnless(c2.alt_id == None, c2.alt_id)
+            self.failUnless(c2.author == 'Jess', c2.author)
+            self.failUnless(c2.body == 'World\n', c2.body)
+            c4 = comments[0]
+            self.failUnless(len(c4.uuid) == 36, c4.uuid)
+            self.failUnless(c4.alt_id == 'c3', c4.alt_id)
+            self.failUnless(c4.author == 'Jed', c4.author)
+            self.failUnless(c4.body == 'And thanks\n', c4.body)
+        def testAddOnly(self):
+            bugB = self.bugdir.bug_from_uuid('b')
+            initial_bugB_summary = bugB.summary
+            self._execute(self.xml, {'add-only':True}, ['-'])
+            uuids = list(self.bugdir.uuids())
+            self.failUnless(uuids == ['b'], uuids)
+            bugB = self.bugdir.bug_from_uuid('b')
+            self.failUnless(bugB.uuid == 'b', bugB.uuid)
+            self.failUnless(bugB.creator == 'John', bugB.creator)
+            self.failUnless(bugB.status == 'open', bugB.status)
+            self.failUnless(bugB.summary == initial_bugB_summary, bugB.summary)
+            estrs = ["don't forget your towel",
+                     'helps with space travel']
+            self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings)
+            comments = list(bugB.comments())
+            self.failUnless(len(comments) == 3,
+                            ['%s (%s)' % (c.uuid, c.alt_id) for c in comments])
+            c1 = bugB.comment_from_uuid('c1')
+            comments.remove(c1)
+            self.failUnless(c1.uuid == 'c1', c1.uuid)
+            self.failUnless(c1.alt_id == None, c1.alt_id)
+            self.failUnless(c1.author == 'Jane', c1.author)
+            self.failUnless(c1.body == 'Hello\n', c1.body)
+            c2 = bugB.comment_from_uuid('c2')
+            comments.remove(c2)
+            self.failUnless(c2.uuid == 'c2', c2.uuid)
+            self.failUnless(c2.alt_id == None, c2.alt_id)
+            self.failUnless(c2.author == 'Jess', c2.author)
+            self.failUnless(c2.body == 'World\n', c2.body)
+            c4 = comments[0]
+            self.failUnless(len(c4.uuid) == 36, c4.uuid)
+            self.failUnless(c4.alt_id == 'c3', c4.alt_id)
+            self.failUnless(c4.author == 'Jed', c4.author)
+            self.failUnless(c4.body == 'And thanks\n', c4.body)
+        def testRootCommentsNotAddOnly(self):
+            bugB = self.bugdir.bug_from_uuid('b')
+            initial_bugB_summary = bugB.summary
+            self._execute(self.root_comment_xml, {'comment-root':'/b'}, ['-'])
+            uuids = list(self.bugdir.uuids())
+            uuids = list(self.bugdir.uuids())
+            self.failUnless(uuids == ['b'], uuids)
+            bugB = self.bugdir.bug_from_uuid('b')
+            self.failUnless(bugB.uuid == 'b', bugB.uuid)
+            self.failUnless(bugB.creator == 'John', bugB.creator)
+            self.failUnless(bugB.status == 'open', bugB.status)
+            self.failUnless(bugB.summary == initial_bugB_summary, bugB.summary)
+            estrs = ["don't forget your towel",
+                     'helps with space travel']
+            self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings)
+            comments = list(bugB.comments())
+            self.failUnless(len(comments) == 3,
+                            ['%s (%s, %s)' % (c.uuid, c.alt_id, c.body)
+                             for c in comments])
+            c1 = bugB.comment_from_uuid('c1')
+            comments.remove(c1)
+            self.failUnless(c1.uuid == 'c1', c1.uuid)
+            self.failUnless(c1.alt_id == None, c1.alt_id)
+            self.failUnless(c1.author == 'Jane', c1.author)
+            self.failUnless(c1.body == 'So long\n', c1.body)
+            c2 = bugB.comment_from_uuid('c2')
+            comments.remove(c2)
+            self.failUnless(c2.uuid == 'c2', c2.uuid)
+            self.failUnless(c2.alt_id == None, c2.alt_id)
+            self.failUnless(c2.author == 'Jess', c2.author)
+            self.failUnless(c2.body == 'World\n', c2.body)
+            c4 = comments[0]
+            self.failUnless(len(c4.uuid) == 36, c4.uuid)
+            self.failUnless(c4.alt_id == 'c3', c4.alt_id)
+            self.failUnless(c4.author == 'Jed', c4.author)
+            self.failUnless(c4.body == 'And thanks\n', c4.body)
+        def testRootCommentsAddOnly(self):
+            bugB = self.bugdir.bug_from_uuid('b')
+            initial_bugB_summary = bugB.summary
+            self._execute(self.root_comment_xml,
+                          {'comment-root':'/b', 'add-only':True}, ['-'])
+            uuids = list(self.bugdir.uuids())
+            self.failUnless(uuids == ['b'], uuids)
+            bugB = self.bugdir.bug_from_uuid('b')
+            self.failUnless(bugB.uuid == 'b', bugB.uuid)
+            self.failUnless(bugB.creator == 'John', bugB.creator)
+            self.failUnless(bugB.status == 'open', bugB.status)
+            self.failUnless(bugB.summary == initial_bugB_summary, bugB.summary)
+            estrs = ["don't forget your towel",
+                     'helps with space travel']
+            self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings)
+            comments = list(bugB.comments())
+            self.failUnless(len(comments) == 3,
+                            ['%s (%s)' % (c.uuid, c.alt_id) for c in comments])
+            c1 = bugB.comment_from_uuid('c1')
+            comments.remove(c1)
+            self.failUnless(c1.uuid == 'c1', c1.uuid)
+            self.failUnless(c1.alt_id == None, c1.alt_id)
+            self.failUnless(c1.author == 'Jane', c1.author)
+            self.failUnless(c1.body == 'Hello\n', c1.body)
+            c2 = bugB.comment_from_uuid('c2')
+            comments.remove(c2)
+            self.failUnless(c2.uuid == 'c2', c2.uuid)
+            self.failUnless(c2.alt_id == None, c2.alt_id)
+            self.failUnless(c2.author == 'Jess', c2.author)
+            self.failUnless(c2.body == 'World\n', c2.body)
+            c4 = comments[0]
+            self.failUnless(len(c4.uuid) == 36, c4.uuid)
+            self.failUnless(c4.alt_id == 'c3', c4.alt_id)
+            self.failUnless(c4.author == 'Jed', c4.author)
+            self.failUnless(c4.body == 'And thanks\n', c4.body)
+
+    unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
+    suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
diff --git a/libbe/command/init.py b/libbe/command/init.py
new file mode 100644 (file)
index 0000000..7b83645
--- /dev/null
@@ -0,0 +1,132 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os.path
+
+import libbe
+import libbe.bugdir
+import libbe.command
+import libbe.storage
+
+class Init (libbe.command.Command):
+    """Create an on-disk bug repository
+
+    >>> import os, sys
+    >>> import libbe.storage.vcs
+    >>> import libbe.storage.vcs.base
+    >>> import libbe.util.utility
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> cmd = Init()
+
+    >>> dir = libbe.util.utility.Dir()
+    >>> vcs = libbe.storage.vcs.vcs_by_name('None')
+    >>> vcs.repo = dir.path
+    >>> try:
+    ...     vcs.connect()
+    ... except libbe.storage.ConnectionError:
+    ...     'got error'
+    'got error'
+    >>> ui.storage_callbacks.set_unconnected_storage(vcs)
+    >>> ui.run(cmd)
+    No revision control detected.
+    BE repository initialized.
+    >>> bd = libbe.bugdir.BugDir(vcs)
+    >>> vcs.disconnect()
+    >>> vcs.connect()
+    >>> bugdir = libbe.bugdir.BugDir(vcs, from_storage=True)
+    >>> vcs.disconnect()
+    >>> vcs.destroy()
+    >>> dir.cleanup()
+
+    >>> dir = libbe.util.utility.Dir()
+    >>> vcs = libbe.storage.vcs.installed_vcs()
+    >>> vcs.repo = dir.path
+    >>> vcs._vcs_init(vcs.repo)
+    >>> ui.storage_callbacks.set_unconnected_storage(vcs)
+    >>> if vcs.name in libbe.storage.vcs.base.VCS_ORDER:
+    ...     ui.run(cmd) # doctest: +ELLIPSIS
+    ... else:
+    ...     vcs.init()
+    ...     vcs.connect()
+    ...     print 'Using ... for revision control.\\nDirectory initialized.'
+    Using ... for revision control.
+    BE repository initialized.
+    >>> vcs.disconnect()
+    >>> vcs.connect()
+    >>> bugdir = libbe.bugdir.BugDir(vcs, from_storage=True)
+    >>> vcs.disconnect()
+    >>> vcs.destroy()
+    >>> dir.cleanup()
+    """
+    name = 'init'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+
+    def _run(self, **params):
+        storage = self._get_unconnected_storage()
+        if not os.path.isdir(storage.repo):
+            raise libbe.command.UserError(
+                'No such directory: %s' % storage.repo)
+        try:
+            storage.connect()
+            raise libbe.command.UserError(
+                'Directory already initialized: %s' % storage.repo)
+        except libbe.storage.ConnectionError:
+            pass
+        storage.init()
+        storage.connect()
+        self.ui.storage_callbacks.set_storage(storage)
+        bd = libbe.bugdir.BugDir(storage, from_storage=False)
+        self.ui.storage_callbacks.set_bugdir(bd)
+        if bd.storage.name is not 'None':
+            print >> self.stdout, \
+                'Using %s for revision control.' % storage.name
+        else:
+            print >> self.stdout, 'No revision control detected.'
+        print >> self.stdout, 'BE repository initialized.'
+
+    def _long_help(self):
+        return """
+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, but you can
+change that by passing the --repo option to be
+  $ be --repo path/to/new/bug/root init
+
+When initialized in a version-controlled directory, BE sinks to the
+version-control root.  In that case, the BE repository will be created
+under that directory, rather than the current directory or the one
+passed in --repo.  Consider the following tree, versioned in Git.
+  ~
+  `--projectX
+     |-- .git
+     `-- src
+Calling
+  ~$ be --repo ./projectX/src init
+will create the BE repository rooted in projectX:
+  ~
+  `--projectX
+     |-- .be
+     |-- .git
+     `-- src
+"""
diff --git a/libbe/command/list.py b/libbe/command/list.py
new file mode 100644 (file)
index 0000000..3803257
--- /dev/null
@@ -0,0 +1,279 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+import re
+
+import libbe
+import libbe.bug
+import libbe.command
+import libbe.command.depend
+import libbe.command.target
+import libbe.command.util
+
+# get a list of * for cmp_*() comparing two bugs.
+AVAILABLE_CMPS = [fn[4:] for fn in dir(libbe.bug) if fn[:4] == 'cmp_']
+AVAILABLE_CMPS.remove('attr') # a cmp_* template.
+
+class Filter (object):
+    def __init__(self, status='all', severity='all', assigned='all',
+                 target='all', extra_strings_regexps=[]):
+        self.status = status
+        self.severity = severity
+        self.assigned = assigned
+        self.target = target
+        self.extra_strings_regexps = extra_strings_regexps
+
+    def __call__(self, bugdir, bug):
+        if self.status != 'all' and not bug.status in self.status:
+            return False
+        if self.severity != 'all' and not bug.severity in self.severity:
+            return False
+        if self.assigned != 'all' and not bug.assigned in self.assigned:
+            return False
+        if self.target == 'all':
+            pass
+        else:
+            target_bug = libbe.command.target.bug_target(bugdir, bug)
+            if self.target in ['none', None]:
+                if target_bug.summary != None:
+                    return False
+            else:
+                if target_bug.summary != self.target:
+                    return False
+        if len(bug.extra_strings) == 0:
+            if len(self.extra_strings_regexps) > 0:
+                return False
+        else:
+            for string in bug.extra_strings:
+                for regexp in self.extra_strings_regexps:
+                    if not regexp.match(string):
+                        return False
+        return True
+
+class List (libbe.command.Command):
+    """List bugs
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = List(ui=ui)
+
+    >>> ret = ui.run(cmd)
+    abc/a:om: Bug A
+    >>> ret = ui.run(cmd, {'status':'closed'})
+    abc/b:cm: Bug B
+    >>> bd.storage.writeable
+    True
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+
+    name = 'list'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='status',
+                    help='Only show bugs matching the STATUS specifier',
+                    arg=libbe.command.Argument(
+                        name='status', metavar='STATUS', default='active',
+                        completion_callback=libbe.command.util.complete_status)),
+                libbe.command.Option(name='severity',
+                    help='Only show bugs matching the SEVERITY specifier',
+                    arg=libbe.command.Argument(
+                        name='severity', metavar='SEVERITY', default='all',
+                        completion_callback=libbe.command.util.complete_severity)),
+                libbe.command.Option(name='important',
+                    help='List bugs with >= "serious" severity'),
+                libbe.command.Option(name='assigned', short_name='a',
+                    help='Only show bugs matching ASSIGNED',
+                    arg=libbe.command.Argument(
+                        name='assigned', metavar='ASSIGNED', default=None,
+                        completion_callback=libbe.command.util.complete_assigned)),
+                libbe.command.Option(name='mine', short_name='m',
+                    help='List bugs assigned to you'),
+                libbe.command.Option(name='extra-strings', short_name='e',
+                    help='Only show bugs matching STRINGS, e.g. --extra-strings'
+                         ' TAG:working,TAG:xml',
+                    arg=libbe.command.Argument(
+                        name='extra-strings', metavar='STRINGS', default=None,
+                        completion_callback=libbe.command.util.complete_extra_strings)),
+                libbe.command.Option(name='sort', short_name='S',
+                    help='Adjust bug-sort criteria with comma-separated list '
+                         'SORT.  e.g. "--sort creator,time".  '
+                         'Available criteria: %s' % ','.join(AVAILABLE_CMPS),
+                    arg=libbe.command.Argument(
+                        name='sort', metavar='SORT', default=None,
+                        completion_callback=libbe.command.util.Completer(AVAILABLE_CMPS))),
+                libbe.command.Option(name='ids', short_name='i',
+                    help='Only print the bug IDS'),
+                libbe.command.Option(name='xml', short_name='x',
+                    help='Dump output in XML format'),
+                ])
+#    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 ids and xml are special cases of long forms
+#             ("w", "wishlist", "List bugs with 'wishlist' severity"),
+#             ("A", "active", "List all active bugs"),
+#             ("U", "unconfirmed", "List unconfirmed bugs"),
+#             ("o", "open", "List open bugs"),
+#             ("T", "test", "List bugs in testing"),
+#    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, default=False)
+#    return parser
+#
+#                ])
+
+    def _run(self, **params):
+        bugdir = self._get_bugdir()
+        writeable = bugdir.storage.writeable
+        bugdir.storage.writeable = False
+        cmp_list, status, severity, assigned, extra_strings_regexps = \
+            self._parse_params(bugdir, params)
+        filter = Filter(status, severity, assigned,
+                        extra_strings_regexps=extra_strings_regexps)
+        bugs = [bugdir.bug_from_uuid(uuid) for uuid in bugdir.uuids()]
+        bugs = [b for b in bugs if filter(bugdir, b) == True]
+        self.result = bugs
+        if len(bugs) == 0 and params['xml'] == False:
+            print >> self.stdout, 'No matching bugs found'
+
+        # sort bugs
+        bugs = self._sort_bugs(bugs, cmp_list)
+
+        # print list of bugs
+        if params['ids'] == True:
+            for bug in bugs:
+                print >> self.stdout, bug.id.user()
+        else:
+            self._list_bugs(bugs, xml=params['xml'])
+        bugdir.storage.writeable = writeable
+        return 0
+
+    def _parse_params(self, bugdir, params):
+        cmp_list = []
+        if params['sort'] != None:
+            for cmp in params['sort'].sort_by.split(','):
+                if cmp not in AVAILABLE_CMPS:
+                    raise libbe.command.UserError(
+                        'Invalid sort on "%s".\nValid sorts:\n  %s'
+                    % (cmp, '\n  '.join(AVAILABLE_CMPS)))
+            cmp_list.append(eval('libbe.bug.cmp_%s' % cmp))
+        # select status
+        if params['status'] == 'all':
+            status = libbe.bug.status_values
+        elif params['status'] == 'active':
+            status = list(libbe.bug.active_status_values)
+        elif params['status'] == 'inactive':
+            status = list(libbe.bug.inactive_status_values)
+        else:
+            status = libbe.command.util.select_values(
+                params['status'], libbe.bug.status_values)
+        # select severity
+        if params['severity'] == 'all':
+            severity = libbe.bug.severity_values
+        elif params['important'] == True:
+            serious = libbe.bug.severity_values.index('serious')
+            severity.append(list(libbe.bug.severity_values[serious:]))
+        else:
+            severity = libbe.command.util.select_values(
+                params['severity'], libbe.bug.severity_values)
+        # select assigned
+        if params['assigned'] == None:
+            if params['mine'] == True:
+                assigned = [self._get_user_id()]
+            else:
+                assigned = 'all'
+        else:
+            assigned = libbe.command.util.select_values(
+                params['assigned'], libbe.command.util.assignees(bugdir))
+        for i in range(len(assigned)):
+            if assigned[i] == '-':
+                assigned[i] = params['user-id']
+        if params['extra-strings'] == None:
+            extra_strings_regexps = []
+        else:
+            extra_strings_regexps = [re.compile(x)
+                                     for x in params['extra-strings'].split(',')]
+        return (cmp_list, status, severity, assigned, extra_strings_regexps)
+
+    def _sort_bugs(self, bugs, cmp_list=[]):
+        cmp_list.extend(libbe.bug.DEFAULT_CMP_FULL_CMP_LIST)
+        cmp_fn = libbe.bug.BugCompoundComparator(cmp_list=cmp_list)
+        bugs.sort(cmp_fn)
+        return bugs
+
+    def _list_bugs(self, bugs, xml=False):
+        if xml == True:
+            print >> self.stdout, \
+                '<?xml version="1.0" encoding="%s" ?>' % self.stdout.encoding
+            print >> self.stdout, '<be-xml>'
+        if len(bugs) > 0:
+            for bug in bugs:
+                if xml == True:
+                    print >> self.stdout, bug.xml(show_comments=True)
+                else:
+                    print >> self.stdout, bug.string(shortlist=True)
+        if xml == True:
+            print >> self.stdout, '</be-xml>'
+
+    def _long_help(self):
+        return """
+This command lists bugs.  Normally it prints a short string like
+  bea/576:om: Allow attachments
+Where
+  bea/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)
+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.  As with the --status and
+--severity options for `be depend`, starting the list with a minus
+sign makes your selections a blacklist instead of the default
+whitelist.
+
+status
+  %s
+severity
+  %s
+assigned
+  free form, with the string '-' being a shortcut for yourself.
+
+In addition, there are some shortcut options that set boolean flags.
+The boolean options are ignored if the matching string option is used.
+""" % (','.join(libbe.bug.status_values),
+       ','.join(libbe.bug.severity_values))
diff --git a/libbe/command/merge.py b/libbe/command/merge.py
new file mode 100644 (file)
index 0000000..2dff59c
--- /dev/null
@@ -0,0 +1,189 @@
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import copy
+import os
+
+import libbe
+import libbe.command
+import libbe.command.util
+
+
+class Merge (libbe.command.Command):
+    """Merge duplicate bugs
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> import libbe.comment
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_bugdir(bd)
+    >>> cmd = Merge(ui=ui)
+
+    >>> a = bd.bug_from_uuid('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_uuid('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
+
+    >>> ret = ui.run(cmd, args=['/a', '/b'])
+    Merged bugs #abc/a# and #abc/b#
+    >>> bd.flush_reload()
+    >>> a = bd.bug_from_uuid('a')
+    >>> a.load_comments()
+    >>> a_comments = sorted([c for c in a.comments()],
+    ...                     cmp=libbe.comment.cmp_time)
+    >>> mergeA = a_comments[0]
+    >>> mergeA.time = 3
+    >>> print a.string(show_comments=True) # doctest: +ELLIPSIS
+              ID : a
+      Short name : abc/a
+        Severity : minor
+          Status : open
+        Assigned : 
+        Reporter : 
+         Creator : John Doe <jdoe@example.com>
+         Created : ...
+    Bug A
+    --------- Comment ---------
+    Name: abc/a/...
+    From: ...
+    Date: ...
+    <BLANKLINE>
+    Testing
+      --------- Comment ---------
+      Name: abc/a/...
+      From: ...
+      Date: ...
+    <BLANKLINE>
+      Testing...
+    --------- Comment ---------
+    Name: abc/a/...
+    From: ...
+    Date: ...
+    <BLANKLINE>
+    Merged from bug #abc/b#
+      --------- Comment ---------
+      Name: abc/a/...
+      From: ...
+      Date: ...
+    <BLANKLINE>
+      1 2
+        --------- Comment ---------
+        Name: abc/a/...
+        From: ...
+        Date: ...
+    <BLANKLINE>
+        1 2 3 4
+    >>> b = bd.bug_from_uuid('b')
+    >>> b.load_comments()
+    >>> b_comments = sorted([c for c in b.comments()],
+    ...                     libbe.comment.cmp_time)
+    >>> mergeB = b_comments[0]
+    >>> mergeB.time = 3
+    >>> print b.string(show_comments=True) # doctest: +ELLIPSIS
+              ID : b
+      Short name : abc/b
+        Severity : minor
+          Status : closed
+        Assigned : 
+        Reporter : 
+         Creator : Jane Doe <jdoe@example.com>
+         Created : ...
+    Bug B
+    --------- Comment ---------
+    Name: abc/b/...
+    From: ...
+    Date: ...
+    <BLANKLINE>
+    1 2
+      --------- Comment ---------
+      Name: abc/b/...
+      From: ...
+      Date: ...
+    <BLANKLINE>
+      1 2 3 4
+    --------- Comment ---------
+    Name: abc/b/...
+    From: ...
+    Date: ...
+    <BLANKLINE>
+    Merged into bug #abc/a#
+    >>> print b.status
+    closed
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'merge'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.args.extend([
+                libbe.command.Argument(
+                    name='bug-id', metavar='BUG-ID', default=None,
+                    completion_callback=libbe.command.util.complete_bug_id),
+                libbe.command.Argument(
+                    name='bug-id-to-merge', metavar='BUG-ID', default=None,
+                    completion_callback=libbe.command.util.complete_bug_id),
+                ])
+
+    def _run(self, **params):
+        bugdir = self._get_bugdir()
+        bugA,dummy_comment = \
+            libbe.command.util.bug_comment_from_user_id(
+                bugdir, params['bug-id'])
+        bugA.load_comments()
+        bugB,dummy_comment = \
+            libbe.command.util.bug_comment_from_user_id(
+                bugdir, params['bug-id-to-merge'])
+        bugB.load_comments()
+        mergeA = bugA.new_comment('Merged from bug #%s#' % bugB.id.long_user())
+        newCommTree = copy.deepcopy(bugB.comment_root)
+        for comment in newCommTree.traverse(): # all descendant comments
+            comment.bug = bugA
+            # uuids must be unique in storage
+            if comment.alt_id == None:
+                comment.storage = None
+                comment.alt_id = comment.uuid
+                comment.storage = bugdir.storage
+            comment.uuid = libbe.util.id.uuid_gen()
+            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.id.long_user())
+        bugB.status = 'closed'
+        print >> self.stdout, 'Merged bugs #%s# and #%s#' \
+            % (bugA.id.user(), bugB.id.user())
+        return 0
+
+    def _long_help(self):
+        return """
+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.
+"""
diff --git a/libbe/command/new.py b/libbe/command/new.py
new file mode 100644 (file)
index 0000000..be18306
--- /dev/null
@@ -0,0 +1,103 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import libbe
+import libbe.command
+import libbe.command.util
+
+
+class New (libbe.command.Command):
+    """Create a new bug
+
+    >>> import os
+    >>> import sys
+    >>> import time
+    >>> import libbe.bugdir
+    >>> import libbe.util.id
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = New()
+
+    >>> uuid_gen = libbe.util.id.uuid_gen
+    >>> libbe.util.id.uuid_gen = lambda: 'X'
+    >>> ui._user_id = u'Fran\\xe7ois'
+    >>> ret = ui.run(cmd, args=['this is a test',])
+    Created bug with ID abc/X
+    >>> libbe.util.id.uuid_gen = uuid_gen
+    >>> bd.flush_reload()
+    >>> bug = bd.bug_from_uuid('X')
+    >>> print bug.summary
+    this is a test
+    >>> bug.creator
+    u'Fran\\xe7ois'
+    >>> bug.reporter
+    u'Fran\\xe7ois'
+    >>> bug.time <= int(time.time())
+    True
+    >>> print bug.severity
+    minor
+    >>> print bug.status
+    open
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'new'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='reporter', short_name='r',
+                    help='The user who reported the bug',
+                    arg=libbe.command.Argument(
+                        name='reporter', metavar='NAME')),
+                libbe.command.Option(name='assigned', short_name='a',
+                    help='The developer in charge of the bug',
+                    arg=libbe.command.Argument(
+                        name='assigned', metavar='NAME',
+                        completion_callback=libbe.command.util.complete_assigned)),
+                ])
+        self.args.extend([
+                libbe.command.Argument(name='summary', metavar='SUMMARY')
+                ])
+
+    def _run(self, **params):
+        if params['summary'] == '-': # read summary from stdin
+            summary = self.stdin.readline()
+        else:
+            summary = params['summary']
+        bugdir = self._get_bugdir()
+        bug = bugdir.new_bug(summary=summary.strip())
+        bug.creator = self._get_user_id()
+        if params['reporter'] != None:
+            bug.reporter = params['reporter']
+        else:
+            bug.reporter = bug.creator
+        if params['assigned'] != None:
+            bug.assigned = params['assigned']
+        print >> self.stdout, 'Created bug with ID %s' % bug.id.user()
+        return 0
+
+    def _long_help(self):
+        return """
+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.
+"""
similarity index 89%
rename from becommands/open.py
rename to libbe/command/open.py
index 0c6bf05b52a18674ce4e5ce127c2edb171abc416..a6fe48d2a207e870d575bce8975c7cb95e8e4d3e 100644 (file)
@@ -1,4 +1,5 @@
 # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
 #                         Marien Zwart <marienz@gentoo.org>
 #                         Thomas Gerigk <tgerigk@gmx.de>
 #                         W. Trevor King <wking@drexel.edu>
@@ -20,7 +21,8 @@
 from libbe import cmdutil, bugdir
 __desc__ = __doc__
 
-def execute(args, manipulate_encodings=True):
+def execute(args, manipulate_encodings=True, restrict_file_access=False,
+            dir="."):
     """
     >>> import os
     >>> bd = bugdir.SimpleBugDir()
@@ -42,8 +44,9 @@ def execute(args, manipulate_encodings=True):
     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])
+                       manipulate_encodings=manipulate_encodings,
+                       root=dir)
+    bug = cmdutil.bug_from_id(bd, args[0])
     bug.status = "open"
 
 def get_parser():
diff --git a/libbe/command/remove.py b/libbe/command/remove.py
new file mode 100644 (file)
index 0000000..8d8e641
--- /dev/null
@@ -0,0 +1,79 @@
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import libbe
+import libbe.command
+import libbe.command.util
+
+
+class Remove (libbe.command.Command):
+    """Remove (delete) a bug and its comments
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = Remove(ui=ui)
+
+    >>> print bd.bug_from_uuid('b').status
+    closed
+    >>> ret = ui.run(cmd, args=['/b'])
+    Removed bug abc/b
+    >>> bd.flush_reload()
+    >>> try:
+    ...     bd.bug_from_uuid('b')
+    ... except libbe.bugdir.NoBugMatches:
+    ...     print 'Bug not found'
+    Bug not found
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'remove'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.args.extend([
+                libbe.command.Argument(
+                    name='bug-id', metavar='BUG-ID', default=None,
+                    repeatable=True,
+                    completion_callback=libbe.command.util.complete_bug_id),
+                ])
+
+    def _run(self, **params):
+        bugdir = self._get_bugdir()
+        user_ids = []
+        for bug_id in params['bug-id']:
+            bug,dummy_comment = libbe.command.util.bug_comment_from_user_id(
+                bugdir, bug_id)
+            user_ids.append(bug.id.user())
+            bugdir.remove_bug(bug)
+        if len(user_ids) == 1:
+            print >> self.stdout, 'Removed bug %s' % user_ids[0]
+        else:
+            print >> self.stdout, 'Removed bugs %s' % ', '.join(user_ids)
+        return 0
+
+    def _long_help(self):
+        return """
+Remove (delete) existing bugs.  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.
+"""
diff --git a/libbe/command/serve.py b/libbe/command/serve.py
new file mode 100644 (file)
index 0000000..7237343
--- /dev/null
@@ -0,0 +1,1172 @@
+# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+#
+# 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 :class:`Serve` serving BE Storage over HTTP.
+
+See Also
+--------
+:mod:`libbe.storage.http` : the associated client
+"""
+
+import hashlib
+import logging
+import os.path
+import posixpath
+import re
+import sys
+import time
+import traceback
+import types
+import urllib
+import wsgiref.simple_server
+try:
+    # Python >= 2.6
+    from urlparse import parse_qs
+except ImportError:
+    # Python <= 2.5
+    from cgi import parse_qs
+try:
+    import cherrypy
+    import cherrypy.wsgiserver
+except ImportError:
+    cherrypy = None
+if cherrypy != None:
+    try: # CherryPy >= 3.2
+        import cherrypy.wsgiserver.ssl_builtin
+    except ImportError: # CherryPy <= 3.1.X
+        cherrypy.wsgiserver.ssl_builtin = None
+try:
+    import OpenSSL
+except ImportError:
+    OpenSSL = None
+
+import libbe
+import libbe.command
+import libbe.command.util
+import libbe.util.encoding
+import libbe.version
+
+if libbe.TESTING == True:
+    import copy
+    import doctest
+    import StringIO
+    import unittest
+    import wsgiref.validate
+    try:
+        import cherrypy.test.webtest
+        cherrypy_test_webtest = True
+    except ImportError:
+        cherrypy_test_webtest = None
+
+    import libbe.bugdir
+    
+class _HandlerError (Exception):
+    def __init__(self, code, msg, headers=[]):
+        Exception.__init__(self, '%d %s' % (code, msg))
+        self.code = code
+        self.msg = msg
+        self.headers = headers
+
+class _Unauthenticated (_HandlerError):
+    def __init__(self, realm, msg='User Not Authenticated', headers=[]):
+        _HandlerError.__init__(self, 401, msg, headers+[
+                ('WWW-Authenticate','Basic realm="%s"' % realm)])
+
+class _Unauthorized (_HandlerError):
+    def __init__(self, msg='User Not Authorized', headers=[]):
+        _HandlerError.__init__(self, 403, msg, headers)
+
+class User (object):
+    def __init__(self, uname=None, name=None, passhash=None, password=None):
+        self.uname = uname
+        self.name = name
+        self.passhash = passhash
+        if passhash == None:
+            if password != None:
+                self.passhash = self.hash(password)
+        else:
+            assert password == None, \
+                'Redundant password %s with passhash %s' % (password, passhash)
+        self.users = None
+    def from_string(self, string):
+        string = string.strip()
+        fields = string.split(':')
+        if len(fields) != 3:
+            raise ValueError, '%d!=3 fields in "%s"' % (len(fields), string)
+        self.uname,self.name,self.passhash = fields
+    def __str__(self):
+        return ':'.join([self.uname, self.name, self.passhash])
+    def __cmp__(self, other):
+        return cmp(self.uname, other.uname)
+    def hash(self, password):
+        return hashlib.sha1(password).hexdigest()
+    def valid_login(self, password):
+        if self.hash(password) == self.passhash:
+            return True
+        return False
+    def set_name(self, name):
+        self._set_property('name', name)
+    def set_password(self, password):
+        self._set_property('passhash', self.hash(password))
+    def _set_property(self, property, value):
+        if self.uname == 'guest':
+            raise _Unauthorized('guest user not allowed to change %s' % property)
+        if getattr(self, property) != value \
+                and self.users != None:
+            self.users.changed = True
+        setattr(self, property, value)
+
+class Users (dict):
+    def __init__(self, filename=None):
+        dict.__init__(self)
+        self.filename = filename
+        self.changed = False
+    def load(self):
+        if self.filename == None:
+            return
+        user_file = libbe.util.encoding.get_file_contents(
+            self.filename, decode=True)
+        self.clear()
+        for line in user_file.splitlines():
+            user = User()
+            user.from_string(line)
+            self.add_user(user)
+    def save(self):
+        if self.filename != None and self.changed == True:
+            lines = []
+            for user in sorted(self.users):
+                lines.append(str(user))
+            libbe.util.encoding.set_file_contents(self.filename)
+            self.changed = False
+    def add_user(self, user):
+        assert user.users == None, user.users
+        user.users = self
+        self[user.uname] = user
+    def valid_login(self, uname, password):
+        if uname in self and \
+                self[uname].valid_login(password) == True:
+            return True
+        return False
+
+class WSGI_Object (object):
+    """Utility class for WGSI clients and middleware.
+
+    For details on WGSI, see `PEP 333`_
+
+    .. _PEP 333: http://www.python.org/dev/peps/pep-0333/
+    """
+    def __init__(self, logger=None, log_level=logging.INFO, log_format=None):
+        self.logger = logger
+        self.log_level = log_level
+        if log_format == None:
+            self.log_format = (
+                '%(REMOTE_ADDR)s - %(REMOTE_USER)s [%(time)s] '
+                '"%(REQUEST_METHOD)s %(REQUEST_URI)s %(HTTP_VERSION)s" '
+                '%(status)s %(bytes)s "%(HTTP_REFERER)s" "%(HTTP_USER_AGENT)s"')
+        else:
+            self.log_format = log_format
+
+    def __call__(self, environ, start_response):
+        """The main WSGI entry point."""
+        raise NotImplementedError
+        # start_response() is a callback for setting response headers
+        #   start_response(status, response_headers, exc_info=None)
+        # status is an HTTP status string (e.g., "200 OK").
+        # response_headers is a list of 2-tuples, the HTTP headers in
+        # key-value format.
+        # exc_info is used in exception handling.
+        #
+        # The application function then returns an iterable of body chunks.
+
+    def error(self, environ, start_response, error, message, headers=[]):
+        """Make it easy to call start_response for errors."""
+        response = '%d %s' % (error, message)
+        self.log_request(environ, status=response, bytes=len(message))
+        start_response(response,
+                       [('Content-Type', 'text/plain')]+headers)
+        return [message]
+
+    def log_request(self, environ, status='-1 OK', bytes=-1):
+        if self.logger == None:
+            return
+        req_uri = urllib.quote(environ.get('SCRIPT_NAME', '')
+                               + environ.get('PATH_INFO', ''))
+        if environ.get('QUERY_STRING'):
+            req_uri += '?'+environ['QUERY_STRING']
+        start = time.localtime()
+        if time.daylight:
+            offset = time.altzone / 60 / 60 * -100
+        else:
+            offset = time.timezone / 60 / 60 * -100
+        if offset >= 0:
+            offset = "+%0.4d" % (offset)
+        elif offset < 0:
+            offset = "%0.4d" % (offset)
+        d = {
+            'REMOTE_ADDR': environ.get('REMOTE_ADDR') or '-',
+            'REMOTE_USER': environ.get('REMOTE_USER') or '-',
+            'REQUEST_METHOD': environ['REQUEST_METHOD'],
+            'REQUEST_URI': req_uri,
+            'HTTP_VERSION': environ.get('SERVER_PROTOCOL'),
+            'time': time.strftime('%d/%b/%Y:%H:%M:%S ', start) + offset,
+            'status': status.split(None, 1)[0],
+            'bytes': bytes,
+            'HTTP_REFERER': environ.get('HTTP_REFERER', '-'),
+            'HTTP_USER_AGENT': environ.get('HTTP_USER_AGENT', '-'),
+            }
+        self.logger.log(self.log_level, self.log_format % d)
+
+class ExceptionApp (WSGI_Object):
+    """Some servers (e.g. cherrypy) eat app-raised exceptions.
+
+    Work around that by logging tracebacks by hand.
+    """
+    def __init__(self, app, *args, **kwargs):
+        WSGI_Object.__init__(self, *args, **kwargs)
+        self.app = app
+
+    def __call__(self, environ, start_response):
+        if self.logger != None:
+            self.logger.log(logging.DEBUG, 'ExceptionApp')
+        try:
+            return self.app(environ, start_response)
+        except Exception, e:
+            etype,value,tb = sys.exc_info()
+            trace = ''.join(
+                traceback.format_exception(etype, value, tb, None))
+            self.logger.log(self.log_level, trace)
+            raise
+
+class UppercaseHeaderApp (WSGI_Object):
+    """WSGI middleware that uppercases incoming HTTP headers.
+
+    From PEP 333, `The start_response() Callable`_ :
+
+        A reminder for server/gateway authors: HTTP
+        header names are case-insensitive, so be sure
+        to take that into consideration when examining
+        application-supplied headers!
+
+    .. _The start_response() Callable:
+      http://www.python.org/dev/peps/pep-0333/#id20
+    """
+    def __init__(self, app, *args, **kwargs):
+        WSGI_Object.__init__(self, *args, **kwargs)
+        self.app = app
+
+    def __call__(self, environ, start_response):
+        if self.logger != None:
+            self.logger.log(logging.DEBUG, 'UppercaseHeaderApp')
+        for key,value in environ.items():
+            if key.startswith('HTTP_'):
+                uppercase = key.upper()
+                if uppercase != key:
+                    environ[uppercase] = environ.pop(key)
+        return self.app(environ, start_response)
+
+class AuthenticationApp (WSGI_Object):
+    """WSGI middleware for handling user authentication.
+    """
+    def __init__(self, app, realm, setting='be-auth', users=None, *args, **kwargs):
+        WSGI_Object.__init__(self, *args, **kwargs)
+        self.app = app
+        self.realm = realm
+        self.setting = setting
+        self.users = users
+
+    def __call__(self, environ, start_response):
+        if self.logger != None:
+            self.logger.log(logging.DEBUG, 'AuthenticationApp')
+        environ['%s.realm' % self.setting] = self.realm
+        try:
+            username = self.authenticate(environ)
+            environ['%s.user' % self.setting] = username
+            environ['%s.user.name' % self.setting] = \
+                self.users[username].name
+            return self.app(environ, start_response)
+        except _Unauthorized, e:
+            return self.error(environ, start_response,
+                              e.code, e.msg, e.headers)
+
+    def authenticate(self, environ):
+        """Handle user-authentication sent in the "Authorization" header.
+        
+        This function implements ``Basic`` authentication as described in
+        HTTP/1.0 specification [1]_ .  Do not use this module unless you
+        are using SSL, as it transmits unencrypted passwords.
+
+        .. [1] http://www.w3.org/Protocols/HTTP/1.0/draft-ietf-http-spec.html#BasicAA
+
+        Examples
+        --------
+
+        >>> users = Users()
+        >>> users.add_user(User('Aladdin', 'Big Al', password='open sesame'))
+        >>> app = AuthenticationApp(app=None, realm='Dummy Realm', users=users)
+        >>> app.authenticate({'HTTP_AUTHORIZATION':'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='})
+        'Aladdin'
+        >>> app.authenticate({'HTTP_AUTHORIZATION':'Basic AAAAAAAAAAAAAAAAAAAAAAAAAA=='})
+
+        Notes
+        -----
+
+        Code based on authkit/authenticate/basic.py
+        (c) 2005 Clark C. Evans.
+        Released under the MIT License:
+        http://www.opensource.org/licenses/mit-license.php
+        """
+        authorization = environ.get('HTTP_AUTHORIZATION', None)
+        if authorization == None:
+            raise _Unauthorized('Authorization required')
+        try:
+            authmeth,auth = authorization.split(' ',1)
+        except ValueError:
+            return None
+        if 'basic' != authmeth.lower():
+            return None # non-basic HTTP authorization not implemented
+        auth = auth.strip().decode('base64')
+        try:
+            username,password = auth.split(':',1)
+        except ValueError:
+            return None
+        if self.authfunc(environ, username, password) == True:
+            return username
+
+    def authfunc(self, environ, username, password):
+        if not username in self.users:
+            return False
+        if self.users[username].valid_login(password) == True:
+            if self.logger != None:
+                self.logger.log(self.log_level,
+                    'Authenticated %s' % self.users[username].name)
+            return True
+        return False
+
+class WSGI_AppObject (WSGI_Object):
+    """Useful WSGI utilities for handling data (POST, QUERY) and
+    returning responses.
+    """
+    def __init__(self, *args, **kwargs):
+        WSGI_Object.__init__(self, *args, **kwargs)
+
+        # Maximum input we will accept when REQUEST_METHOD is POST
+        # 0 ==> unlimited input
+        self.maxlen = 0
+
+    def ok_response(self, environ, start_response, content,
+                    content_type='application/octet-stream',
+                    headers=[]):
+        if content == None:
+            start_response('200 OK', [])
+            return []
+        if type(content) == types.UnicodeType:
+            content = content.encode('utf-8')
+        for i,header in enumerate(headers):
+            header_name,header_value = header
+            if type(header_value) == types.UnicodeType:
+                headers[i] = (header_name, header_value.encode('ISO-8859-1'))
+        response = '200 OK'
+        content_length = len(content)
+        self.log_request(environ, status=response, bytes=content_length)
+        start_response('200 OK', [
+                ('Content-Type', content_type),
+                ('Content-Length', str(content_length)),
+                ]+headers)
+        if self.is_head(environ) == True:
+            return []
+        return [content]
+
+    def query_data(self, environ):
+        if not environ['REQUEST_METHOD'] in ['GET', 'HEAD']:
+            raise _HandlerError(404, 'Not Found')
+        return self._parse_query(environ.get('QUERY_STRING', ''))
+
+    def _parse_query(self, query):
+        if len(query) == 0:
+            return {}
+        data = parse_qs(
+            query, keep_blank_values=True, strict_parsing=True)
+        for k,v in data.items():
+            if len(v) == 1:
+                data[k] = v[0]
+        return data
+
+    def post_data(self, environ):
+        if environ['REQUEST_METHOD'] != 'POST':
+            raise _HandlerError(404, 'Not Found')
+        post_data = self._read_post_data(environ)
+        return self._parse_post(post_data)
+
+    def _parse_post(self, post):
+        return self._parse_query(post)
+
+    def _read_post_data(self, environ):
+        try:
+            clen = int(environ.get('CONTENT_LENGTH', '0'))
+        except ValueError:
+            clen = 0
+        if clen != 0:
+            if self.maxlen > 0 and clen > self.maxlen:
+                raise ValueError, 'Maximum content length exceeded'
+            return environ['wsgi.input'].read(clen)
+        return ''
+
+    def data_get_string(self, data, key, default=None, source='query'):
+        if not key in data or data[key] in [None, 'None']:
+            if default == _HandlerError:
+                raise _HandlerError(406, 'Missing %s key %s' % (source, key))
+            return default
+        return data[key]
+
+    def data_get_id(self, data, key='id', default=_HandlerError,
+                    source='query'):
+        return self.data_get_string(data, key, default, source)
+
+    def data_get_boolean(self, data, key, default=False, source='query'):
+        val = self.data_get_string(data, key, default, source)
+        if val == 'True':
+            return True
+        elif val == 'False':
+            return False
+        return val
+
+    def is_head(self, environ):
+        return environ['REQUEST_METHOD'] == 'HEAD'
+
+
+class AdminApp (WSGI_AppObject):
+    """WSGI middleware for managing users (changing passwords,
+    usernames, etc.).
+    """
+    def __init__(self, app, users=None, url=r'^admin/?', *args, **kwargs):
+        WSGI_AppObject.__init__(self, *args, **kwargs)
+        self.app = app
+        self.users = users
+        self.url = url
+
+    def __call__(self, environ, start_response):
+        if self.logger != None:
+            self.logger.log(logging.DEBUG, 'AdminApp')
+        path = environ.get('PATH_INFO', '').lstrip('/')
+        match = re.search(self.url, path)
+        if match is not None:
+            return self.admin(environ, start_response)
+        return self.app(environ, start_response)
+
+    def admin(self, environ, start_response):
+        if not 'be-auth.user' in environ:
+            raise _Unauthenticated(realm=envirion.get('be-auth.realm'))
+        uname = environ.get('be-auth.user')
+        user = self.users[uname]
+        data = self.post_data(environ)
+        source = 'post'
+        name = self.data_get_string(
+            data, 'name', default=None, source=source)
+        if name != None:
+            self.users[uname].set_name(name)
+        password = self.data_get_string(
+            data, 'password', default=None, source=source)
+        if password != None:
+            self.users[uname].set_password(password)
+        self.users.save()
+        return self.ok_response(environ, start_response, None)
+
+class ServerApp (WSGI_AppObject):
+    """WSGI server for a BE Storage instance over HTTP.
+
+    RESTful_ WSGI request handler for serving the
+    libbe.storage.http.HTTP backend with GET, POST, and HEAD commands.
+    For more information on authentication and REST, see John
+    Calcote's `Open Sourcery article`_
+
+    .. _RESTful: http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
+    .. _Open Sourcery article: http://jcalcote.wordpress.com/2009/08/10/restful-authentication/
+
+    This serves files from a connected storage instance, usually
+    a VCS-based repository located on the local machine.
+
+    Notes
+    -----
+
+    The GET and HEAD requests are identical except that the HEAD
+    request omits the actual content of the file.
+    """
+    server_version = "BE-server/" + libbe.version.version()
+
+    def __init__(self, storage, *args, **kwargs):
+        WSGI_AppObject.__init__(self, *args, **kwargs)
+        self.storage = storage
+        self.http_user_error = 418
+
+        self.urls = [
+            (r'^add/?', self.add),
+            (r'^exists/?', self.exists),
+            (r'^remove/?', self.remove),
+            (r'^ancestors/?', self.ancestors),
+            (r'^children/?', self.children),
+            (r'^get/(.+)', self.get),
+            (r'^set/(.+)', self.set),
+            (r'^commit/?', self.commit),
+            (r'^revision-id/?', self.revision_id),
+            (r'^changed/?', self.changed),
+            (r'^version/?', self.version),
+            ]
+
+    def __call__(self, environ, start_response):
+        """The main WSGI application.
+
+        Dispatch the current request to the functions from above and
+        store the regular expression captures in the WSGI environment
+        as `be-server.url_args` so that the functions from above can
+        access the url placeholders.
+
+        URL dispatcher from Armin Ronacher's "Getting Started with WSGI"
+          http://lucumr.pocoo.org/2007/5/21/getting-started-with-wsgi
+        """
+        if self.logger != None:
+            self.logger.log(logging.DEBUG, 'ServerApp')
+        path = environ.get('PATH_INFO', '').lstrip('/')
+        try:
+            for regex, callback in self.urls:
+                match = re.search(regex, path)
+                if match is not None:
+                    environ['be-server.url_args'] = match.groups()
+                    try:
+                        return callback(environ, start_response)
+                    except libbe.storage.NotReadable, e:
+                        raise _HandlerError(403, 'Read permission denied')
+                    except libbe.storage.NotWriteable, e:
+                        raise _HandlerError(403, 'Write permission denied')
+                    except libbe.storage.InvalidID, e:
+                        raise _HandlerError(
+                            self.http_user_error, 'InvalidID %s' % e)
+            raise _HandlerError(404, 'Not Found')
+        except _HandlerError, e:
+            return self.error(environ, start_response,
+                              e.code, e.msg, e.headers)
+
+    # handlers
+    def add(self, environ, start_response):
+        self.check_login(environ)
+        data = self.post_data(environ)
+        source = 'post'
+        id = self.data_get_id(data, source=source)
+        parent = self.data_get_string(
+            data, 'parent', default=None, source=source)
+        directory = self.data_get_boolean(
+            data, 'directory', default=False, source=source)
+        self.storage.add(id, parent=parent, directory=directory)
+        return self.ok_response(environ, start_response, None)
+
+    def exists(self, environ, start_response):
+        self.check_login(environ)
+        data = self.query_data(environ)
+        source = 'query'
+        id = self.data_get_id(data, source=source)
+        revision = self.data_get_string(
+            data, 'revision', default=None, source=source)
+        content = str(self.storage.exists(id, revision))
+        return self.ok_response(environ, start_response, content)
+
+    def remove(self, environ, start_response):
+        self.check_login(environ)
+        data = self.post_data(environ)
+        source = 'post'
+        id = self.data_get_id(data, source=source)
+        recursive = self.data_get_boolean(
+            data, 'recursive', default=False, source=source)
+        if recursive == True:
+            self.storage.recursive_remove(id)
+        else:
+            self.storage.remove(id)
+        return self.ok_response(environ, start_response, None)
+
+    def ancestors(self, environ, start_response):
+        self.check_login(environ)
+        data = self.query_data(environ)
+        source = 'query'
+        id = self.data_get_id(data, source=source)
+        revision = self.data_get_string(
+            data, 'revision', default=None, source=source)
+        content = '\n'.join(self.storage.ancestors(id, revision))+'\n'
+        return self.ok_response(environ, start_response, content)
+
+    def children(self, environ, start_response):
+        self.check_login(environ)
+        data = self.query_data(environ)
+        source = 'query'
+        id = self.data_get_id(data, default=None, source=source)
+        revision = self.data_get_string(
+            data, 'revision', default=None, source=source)
+        content = '\n'.join(self.storage.children(id, revision))
+        return self.ok_response(environ, start_response, content)
+
+    def get(self, environ, start_response):
+        self.check_login(environ)
+        data = self.query_data(environ)
+        source = 'query'
+        try:
+            id = environ['be-server.url_args'][0]
+        except:
+            raise _HandlerError(404, 'Not Found')
+        revision = self.data_get_string(
+            data, 'revision', default=None, source=source)
+        content = self.storage.get(id, revision=revision)
+        be_version = self.storage.storage_version(revision)
+        return self.ok_response(environ, start_response, content,
+                                headers=[('X-BE-Version', be_version)])
+
+    def set(self, environ, start_response):
+        self.check_login(environ)
+        data = self.post_data(environ)
+        try:
+            id = environ['be-server.url_args'][0]
+        except:
+            raise _HandlerError(404, 'Not Found')
+        if not 'value' in data:
+            raise _HandlerError(406, 'Missing query key value')
+        value = data['value']
+        self.storage.set(id, value)
+        return self.ok_response(environ, start_response, None)
+
+    def commit(self, environ, start_response):
+        self.check_login(environ)
+        data = self.post_data(environ)
+        if not 'summary' in data:
+            raise _HandlerError(406, 'Missing query key summary')
+        summary = data['summary']
+        if not 'body' in data or data['body'] == 'None':
+            data['body'] = None
+        body = data['body']
+        if not 'allow_empty' in data \
+                or data['allow_empty'] == 'True':
+            allow_empty = True
+        else:
+            allow_empty = False
+        try:
+            revision = self.storage.commit(summary, body, allow_empty)
+        except libbe.storage.EmptyCommit, e:
+            raise _HandlerError(self.http_user_error, 'EmptyCommit')
+        return self.ok_response(environ, start_response, revision)
+
+    def revision_id(self, environ, start_response):
+        self.check_login(environ)
+        data = self.query_data(environ)
+        source = 'query'
+        index = int(self.data_get_string(
+            data, 'index', default=_HandlerError, source=source))
+        content = self.storage.revision_id(index)
+        return self.ok_response(environ, start_response, content)
+
+    def changed(self, environ, start_response):
+        self.check_login(environ)
+        data = self.query_data(environ)
+        source = 'query'
+        revision = self.data_get_string(
+            data, 'revision', default=None, source=source)
+        add,mod,rem = self.storage.changed(revision)
+        content = '\n\n'.join(['\n'.join(p) for p in (add,mod,rem)])
+        return self.ok_response(environ, start_response, content)
+
+    def version(self, environ, start_response):
+        self.check_login(environ)
+        data = self.query_data(environ)
+        source = 'query'
+        revision = self.data_get_string(
+            data, 'revision', default=None, source=source)
+        content = self.storage.storage_version(revision)
+        return self.ok_response(environ, start_response, content)
+
+    # handler utility functions
+    def check_login(self, environ):
+        user = environ.get('be-auth.user', None)
+        if user != None: # we're running under AuthenticationApp
+            if environ['REQUEST_METHOD'] == 'POST':
+                if user == 'guest' or self.storage.is_writeable() == False:
+                    raise _Unauthorized() # only non-guests allowed to write
+            # allow read-only commands for all users
+
+
+class Serve (libbe.command.Command):
+    """:class:`~libbe.command.base.Command` wrapper around
+    :class:`ServerApp`.
+    """
+
+    name = 'serve'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='port',
+                    help='Bind server to port (%default)',
+                    arg=libbe.command.Argument(
+                        name='port', metavar='INT', type='int', default=8000)),
+                libbe.command.Option(name='host',
+                    help='Set host string (blank for localhost, %default)',
+                    arg=libbe.command.Argument(
+                        name='host', metavar='HOST', default='')),
+                libbe.command.Option(name='read-only', short_name='r',
+                    help='Dissable operations that require writing'),
+                libbe.command.Option(name='ssl', short_name='s',
+                    help='Use CherryPy to serve HTTPS (HTTP over SSL/TLS)'),
+                libbe.command.Option(name='auth', short_name='a',
+                    help='Require authentication.  FILE should be a file containing colon-separated UNAME:USER:sha1(PASSWORD) lines, for example: "jdoe:John Doe <jdoe@example.com>:read:d99f8e5a4b02dc25f49da2ea67c0034f61779e72"',
+                    arg=libbe.command.Argument(
+                        name='auth', metavar='FILE', default=None,
+                        completion_callback=libbe.command.util.complete_path)),
+                ])
+
+    def _run(self, **params):
+        self._setup_logging()
+        storage = self._get_storage()
+        if params['read-only'] == True:
+            writeable = storage.writeable
+            storage.writeable = False
+        if params['host'] == '':
+            params['host'] = 'localhost'
+        if params['auth'] != None:
+            self._check_restricted_access(storage, params['auth'])
+        users = Users(params['auth'])
+        users.load()
+        app = ServerApp(storage=storage, logger=self.logger)
+        if params['auth'] != None:
+            app = AdminApp(app, users=users, logger=self.logger)
+            app = AuthenticationApp(app, realm=storage.repo,
+                                    users=users, logger=self.logger)
+        app = UppercaseHeaderApp(app, logger=self.logger)
+        server,details = self._get_server(params, app)
+        details['repo'] = storage.repo
+        try:
+            self._start_server(params, server, details)
+        except KeyboardInterrupt:
+            pass
+        self._stop_server(params, server)
+        if params['read-only'] == True:
+            storage.writeable = writeable
+
+    def _setup_logging(self, log_level=logging.INFO):
+        self.logger = logging.getLogger('be-serve')
+        self.log_level = logging.INFO
+        console = logging.StreamHandler(self.stdout)
+        console.setFormatter(logging.Formatter('%(message)s'))
+        self.logger.addHandler(console)
+        self.logger.propagate = False
+        if log_level is not None:
+            console.setLevel(log_level)
+            self.logger.setLevel(log_level)
+
+    def _get_server(self, params, app):
+        details = {'port':params['port']}
+        if params['ssl'] == True:
+            details['protocol'] = 'HTTPS'
+            if cherrypy == None:
+                raise libbe.command.UserError, \
+                    '--ssl requires the cherrypy module'
+            app = ExceptionApp(app, logger=self.logger)
+            server = cherrypy.wsgiserver.CherryPyWSGIServer(
+                (params['host'], params['port']), app)
+            #server.throw_errors = True
+            #server.show_tracebacks = True
+            private_key,certificate = get_cert_filenames(
+                'be-server', logger=self.logger)
+            if cherrypy.wsgiserver.ssl_builtin == None:
+                server.ssl_module = 'builtin'
+                server.ssl_private_key = private_key
+                server.ssl_certificate = certificate
+            else:
+                server.ssl_adapter = \
+                    cherrypy.wsgiserver.ssl_builtin.BuiltinSSLAdapter(
+                    certificate=certificate, private_key=private_key)
+            details['socket-name'] = params['host']
+        else:
+            details['protocol'] = 'HTTP'
+            server = wsgiref.simple_server.make_server(
+                params['host'], params['port'], app)
+            details['socket-name'] = server.socket.getsockname()[0]
+        return (server, details)
+
+    def _start_server(self, params, server, details):
+        self.logger.log(self.log_level,
+            'Serving %(protocol)s on %(socket-name)s port %(port)s ...' \
+            % details)
+        self.logger.log(self.log_level,
+                        'BE repository %(repo)s' % details)
+        if params['ssl'] == True:
+            server.start()
+        else:
+            server.serve_forever()
+
+    def _stop_server(self, params, server):
+        self.logger.log(self.log_level, 'Clossing server')
+        if params['ssl'] == True:
+            server.stop()
+        else:
+            server.server_close()
+
+    def _long_help(self):
+        return """
+Example usage::
+
+    $ be serve
+
+And in another terminal (or after backgrounding the server)::
+
+    $ be --repo http://localhost:8000/ list
+
+If you bind your server to a public interface, take a look at the
+``--read-only`` option or the combined ``--ssl --auth FILE``
+options so other people can't mess with your repository.  If you do use
+authentication, you'll need to send in your username and password with,
+for example::
+
+    $ be --repo http://username:password@localhost:8000/ list
+"""
+
+def random_string(length=256):
+    if os.path.exists(os.path.join('dev', 'urandom')):
+        return open("/dev/urandom").read(length)
+    else:
+        import array
+        from random import randint
+        d = array.array('B')
+        for i in xrange(1000000):
+            d.append(randint(0,255))
+        return d.tostring()
+
+if libbe.TESTING == True:
+    class WSGITestCase (unittest.TestCase):
+        def setUp(self):
+            self.logstream = StringIO.StringIO()
+            self.logger = logging.getLogger('be-serve-test')
+            console = logging.StreamHandler(self.logstream)
+            console.setFormatter(logging.Formatter('%(message)s'))
+            self.logger.addHandler(console)
+            self.logger.propagate = False
+            console.setLevel(logging.INFO)
+            self.logger.setLevel(logging.INFO)
+            self.default_environ = { # required by PEP 333
+                'REQUEST_METHOD': 'GET', # 'POST', 'HEAD'
+                'SCRIPT_NAME':'',
+                'PATH_INFO': '',
+                #'QUERY_STRING':'',   # may be empty or absent
+                #'CONTENT_TYPE':'',   # may be empty or absent
+                #'CONTENT_LENGTH':'', # may be empty or absent
+                'SERVER_NAME':'example.com',
+                'SERVER_PORT':'80',
+                'SERVER_PROTOCOL':'HTTP/1.1',
+                'wsgi.version':(1,0),
+                'wsgi.url_scheme':'http',
+                'wsgi.input':StringIO.StringIO(),
+                'wsgi.errors':StringIO.StringIO(),
+                'wsgi.multithread':False,
+                'wsgi.multiprocess':False,
+                'wsgi.run_once':False,
+                }
+        def getURL(self, app, path='/', method='GET', data=None,
+                   scheme='http', environ={}):
+            env = copy.copy(self.default_environ)
+            env['PATH_INFO'] = path
+            env['REQUEST_METHOD'] = method
+            env['scheme'] = scheme
+            if data != None:
+                enc_data = urllib.urlencode(data)
+                if method == 'POST':
+                    env['CONTENT_LENGTH'] = len(enc_data)
+                    env['wsgi.input'] = StringIO.StringIO(enc_data)
+                else:
+                    assert method in ['GET', 'HEAD'], method
+                    env['QUERY_STRING'] = enc_data
+            for key,value in environ.items():
+                env[key] = value
+            return ''.join(app(env, self.start_response))
+        def start_response(self, status, response_headers, exc_info=None):
+            self.status = status
+            self.response_headers = response_headers
+            self.exc_info = exc_info
+
+    class WSGI_ObjectTestCase (WSGITestCase):
+        def setUp(self):
+            WSGITestCase.setUp(self)
+            self.app = WSGI_Object(self.logger)
+        def test_error(self):
+            contents = self.app.error(
+                environ=self.default_environ,
+                start_response=self.start_response,
+                error=123,
+                message='Dummy Error',
+                headers=[('X-Dummy-Header','Dummy Value')])
+            self.failUnless(contents == ['Dummy Error'], contents)
+            self.failUnless(self.status == '123 Dummy Error', self.status)
+            self.failUnless(self.response_headers == [
+                    ('Content-Type','text/plain'),
+                    ('X-Dummy-Header','Dummy Value')],
+                            self.response_headers)
+            self.failUnless(self.exc_info == None, self.exc_info)
+        def test_log_request(self):
+            self.app.log_request(
+                environ=self.default_environ, status='-1 OK', bytes=123)
+            log = self.logstream.getvalue()
+            self.failUnless(log.startswith('- -'), log)
+
+    class ExceptionAppTestCase (WSGITestCase):
+        def setUp(self):
+            WSGITestCase.setUp(self)
+            def child_app(environ, start_response):
+                raise ValueError('Dummy Error')
+            self.app = ExceptionApp(child_app, self.logger)
+        def test_traceback(self):
+            try:
+                self.getURL(self.app)
+            except ValueError, e:
+                pass
+            log = self.logstream.getvalue()
+            self.failUnless(log.startswith('Traceback'), log)
+            self.failUnless('child_app' in log, log)
+            self.failUnless('ValueError: Dummy Error' in log, log)
+
+    class AdminAppTestCase (WSGITestCase):
+        def setUp(self):
+            WSGITestCase.setUp(self)
+            self.users = Users()
+            self.users.add_user(
+                User('Aladdin', 'Big Al', password='open sesame'))
+            self.users.add_user(
+                User('guest', 'Guest', password='guestpass'))
+            def child_app(environ, start_response):
+                pass
+            self.app = AdminApp(
+                child_app, users=self.users, logger=self.logger)
+            self.app = AuthenticationApp(
+                self.app, realm='Dummy Realm', users=self.users,
+                logger=self.logger)
+            self.app = UppercaseHeaderApp(self.app, logger=self.logger)
+        def basic_auth(self, uname, password):
+            """HTTP basic authorization string"""
+            return 'Basic %s' % \
+                ('%s:%s' % (uname, password)).encode('base64')
+        def test_new_name(self):
+            self.getURL(
+                self.app, '/admin/', method='POST',
+                data={'name':'Prince Al'},
+                environ={'HTTP_Authorization':
+                             self.basic_auth('Aladdin', 'open sesame')})
+            self.failUnless(self.status == '200 OK', self.status)
+            self.failUnless(self.response_headers == [],
+                            self.response_headers)
+            self.failUnless(self.exc_info == None, self.exc_info)
+            self.failUnless(self.users['Aladdin'].name == 'Prince Al',
+                            self.users['Aladdin'].name)
+            self.failUnless(self.users.changed == True,
+                            self.users.changed)
+        def test_new_password(self):
+            self.getURL(
+                self.app, '/admin/', method='POST',
+                data={'password':'New Pass'},
+                environ={'HTTP_Authorization':
+                             self.basic_auth('Aladdin', 'open sesame')})
+            self.failUnless(self.status == '200 OK', self.status)
+            self.failUnless(self.response_headers == [],
+                            self.response_headers)
+            self.failUnless(self.exc_info == None, self.exc_info)
+            self.failUnless(self.users['Aladdin'].passhash == \
+                            self.users['Aladdin'].hash('New Pass'),
+                            self.users['Aladdin'].passhash)
+            self.failUnless(self.users.changed == True,
+                            self.users.changed)
+        def test_guest_name(self):
+            self.getURL(
+                self.app, '/admin/', method='POST',
+                data={'name':'SPAM'},
+                environ={'HTTP_Authorization':
+                             self.basic_auth('guest', 'guestpass')})
+            self.failUnless(self.status.startswith('403 '), self.status)
+            self.failUnless(self.response_headers == [
+                    ('Content-Type', 'text/plain')],
+                            self.response_headers)
+            self.failUnless(self.exc_info == None, self.exc_info)
+            self.failUnless(self.users['guest'].name == 'Guest',
+                            self.users['guest'].name)
+            self.failUnless(self.users.changed == False,
+                            self.users.changed)
+        def test_guest_password(self):
+            self.getURL(
+                self.app, '/admin/', method='POST',
+                data={'password':'SPAM'},
+                environ={'HTTP_Authorization':
+                             self.basic_auth('guest', 'guestpass')})
+            self.failUnless(self.status.startswith('403 '), self.status)
+            self.failUnless(self.response_headers == [
+                    ('Content-Type', 'text/plain')],
+                            self.response_headers)
+            self.failUnless(self.exc_info == None, self.exc_info)
+            self.failUnless(self.users['guest'].name == 'Guest',
+                            self.users['guest'].name)
+            self.failUnless(self.users.changed == False,
+                            self.users.changed)
+
+    class ServerAppTestCase (WSGITestCase):
+        def setUp(self):
+            WSGITestCase.setUp(self)
+            self.bd = libbe.bugdir.SimpleBugDir(memory=False)
+            self.app = ServerApp(self.bd.storage, logger=self.logger)
+        def tearDown(self):
+            self.bd.cleanup()
+            WSGITestCase.tearDown(self)
+        def test_add_get(self):
+            self.getURL(self.app, '/add/', method='GET')
+            self.failUnless(self.status.startswith('404 '), self.status)
+            self.failUnless(self.response_headers == [
+                    ('Content-Type', 'text/plain')],
+                            self.response_headers)
+            self.failUnless(self.exc_info == None, self.exc_info)
+        def test_add_post(self):
+            self.getURL(self.app, '/add/', method='POST',
+                        data={'id':'123456', 'parent':'abc123',
+                              'directory':'True'})
+            self.failUnless(self.status == '200 OK', self.status)
+            self.failUnless(self.response_headers == [],
+                            self.response_headers)
+            self.failUnless(self.exc_info == None, self.exc_info)
+        # Note: other methods tested in libbe.storage.http
+
+        # TODO: integration tests on Serve?
+
+    unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
+    suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
+
+
+# The following certificate-creation code is adapted From pyOpenSSL's
+# examples.
+
+def get_cert_filenames(server_name, autogenerate=True, logger=None):
+    """
+    Generate private key and certification filenames.
+    get_cert_filenames(server_name) -> (pkey_filename, cert_filename)
+    """
+    pkey_file = '%s.pkey' % server_name
+    cert_file = '%s.cert' % server_name
+    if autogenerate == True:
+        for file in [pkey_file, cert_file]:
+            if not os.path.exists(file):
+                make_certs(server_name, logger)
+    return (pkey_file, cert_file)
+
+def createKeyPair(type, bits):
+    """Create a public/private key pair.
+
+    Returns the public/private key pair in a PKey object.
+
+    Parameters
+    ----------
+    type : TYPE_RSA or TYPE_DSA
+      Key type.
+    bits : int
+      Number of bits to use in the key.
+    """
+    pkey = OpenSSL.crypto.PKey()
+    pkey.generate_key(type, bits)
+    return pkey
+
+def createCertRequest(pkey, digest="md5", **name):
+    """Create a certificate request.
+
+    Returns the certificate request in an X509Req object.
+
+    Parameters
+    ----------
+    pkey : PKey
+      The key to associate with the request.
+    digest : "md5" or ?
+      Digestion method to use for signing, default is "md5",
+    `**name` :
+      The name of the subject of the request, possible.
+      Arguments are:
+
+      ============ ========================
+      C            Country name
+      ST           State or province name
+      L            Locality name
+      O            Organization name
+      OU           Organizational unit name
+      CN           Common name
+      emailAddress E-mail address
+      ============ ========================
+    """
+    req = OpenSSL.crypto.X509Req()
+    subj = req.get_subject()
+
+    for (key,value) in name.items():
+        setattr(subj, key, value)
+
+    req.set_pubkey(pkey)
+    req.sign(pkey, digest)
+    return req
+
+def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter), digest="md5"):
+    """Generate a certificate given a certificate request.
+
+    Returns the signed certificate in an X509 object.
+
+    Parameters
+    ----------
+    req :
+      Certificate reqeust to use
+    issuerCert :
+      The certificate of the issuer
+    issuerKey :
+      The private key of the issuer
+    serial :
+      Serial number for the certificate
+    notBefore :
+      Timestamp (relative to now) when the certificate
+      starts being valid
+    notAfter :
+      Timestamp (relative to now) when the certificate
+      stops being valid
+    digest :
+      Digest method to use for signing, default is md5
+    """
+    cert = OpenSSL.crypto.X509()
+    cert.set_serial_number(serial)
+    cert.gmtime_adj_notBefore(notBefore)
+    cert.gmtime_adj_notAfter(notAfter)
+    cert.set_issuer(issuerCert.get_subject())
+    cert.set_subject(req.get_subject())
+    cert.set_pubkey(req.get_pubkey())
+    cert.sign(issuerKey, digest)
+    return cert
+
+def make_certs(server_name, logger=None) :
+    """Generate private key and certification files.
+
+    `mk_certs(server_name) -> (pkey_filename, cert_filename)`
+    """
+    if OpenSSL == None:
+        raise libbe.command.UserError, \
+            'SSL certificate generation requires the OpenSSL module'
+    pkey_file,cert_file = get_cert_filenames(
+        server_name, autogenerate=False)
+    if logger != None:
+        logger.log(logger._server_level,
+                   'Generating certificates', pkey_file, cert_file)
+    cakey = createKeyPair(OpenSSL.crypto.TYPE_RSA, 1024)
+    careq = createCertRequest(cakey, CN='Certificate Authority')
+    cacert = createCertificate(
+        careq, (careq, cakey), 0, (0, 60*60*24*365*5)) # five years
+    open(pkey_file, 'w').write(OpenSSL.crypto.dump_privatekey(
+            OpenSSL.crypto.FILETYPE_PEM, cakey))
+    open(cert_file, 'w').write(OpenSSL.crypto.dump_certificate(
+            OpenSSL.crypto.FILETYPE_PEM, cacert))
diff --git a/libbe/command/set.py b/libbe/command/set.py
new file mode 100644 (file)
index 0000000..720dd0f
--- /dev/null
@@ -0,0 +1,144 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Marien Zwart <marienz@gentoo.org>
+#                         Thomas Gerigk <tgerigk@gmx.de>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import textwrap
+
+import libbe
+import libbe.bugdir
+import libbe.command
+import libbe.command.util
+from libbe.storage.util.settings_object import EMPTY
+
+
+class Set (libbe.command.Command):
+    """Change bug directory settings
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = Set(ui=ui)
+
+    >>> ret = ui.run(cmd, args=['target'])
+    None
+    >>> ret = ui.run(cmd, args=['target', 'abcdefg'])
+    >>> ret = ui.run(cmd, args=['target'])
+    abcdefg
+    >>> ret = ui.run(cmd, args=['target', 'none'])
+    >>> ret = ui.run(cmd, args=['target'])
+    None
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'set'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.args.extend([
+                libbe.command.Argument(
+                    name='setting', metavar='SETTING', optional=True,
+                    completion_callback=complete_bugdir_settings),
+                libbe.command.Argument(
+                    name='value', metavar='VALUE', optional=True)
+                ])
+
+    def _run(self, **params):
+        bugdir = self._get_bugdir()
+        if params['setting'] == None:
+            keys = bugdir.settings_properties
+            keys.sort()
+            for key in keys:
+                print >> self.stdout, \
+                    '%16s: %s' % (key, _value_string(bugdir, key))
+            return 0
+        if params['setting'] not in bugdir.settings_properties:
+            msg = 'Invalid setting %s\n' % params['setting']
+            msg += 'Allowed settings:\n  '
+            msg += '\n  '.join(bugdir.settings_properties)
+            raise libbe.command.UserError(msg)
+        if params['value'] == None:
+            print _value_string(bugdir, params['setting'])
+        else:
+            if params['value'] == 'none':
+                params['value'] = EMPTY
+            old_setting = bugdir.settings.get(params['setting'])
+            attr = bugdir._setting_name_to_attr_name(params['setting'])
+            setattr(bugdir, attr, params['value'])
+        return 0
+
+    def _long_help(self):
+        return """
+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 get_bugdir_settings():
+    settings = []
+    for s in libbe.bugdir.BugDir.settings_properties:
+        settings.append(s)
+    settings.sort()
+    documented_settings = []
+    for s in settings:
+        set = getattr(libbe.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
+
+def _value_string(bugdir, setting):
+    val = bugdir.settings.get(setting, EMPTY)
+    if val == EMPTY:
+        default = getattr(bugdir, bugdir._setting_name_to_attr_name(setting))
+        if default not in [None, EMPTY]:
+            val = 'None (%s)' % default
+        else:
+            val = None
+    return str(val)
+
+def complete_bugdir_settings(command, argument, fragment=None):
+    """
+    List possible command completions for fragment.
+
+    Neither the command nor argument arguments are used.
+    """
+    return libbe.bugdir.BugDir.settings_properties
diff --git a/libbe/command/severity.py b/libbe/command/severity.py
new file mode 100644 (file)
index 0000000..27898f7
--- /dev/null
@@ -0,0 +1,98 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Marien Zwart <marienz@gentoo.org>
+#                         Thomas Gerigk <tgerigk@gmx.de>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import libbe
+import libbe.bug
+import libbe.command
+import libbe.command.util
+
+
+class Severity (libbe.command.Command):
+    """Change a bug's severity level
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_bugdir(bd)
+    >>> cmd = Severity(ui=ui)
+
+    >>> bd.bug_from_uuid('a').severity
+    'minor'
+    >>> ret = ui.run(cmd, args=['wishlist', '/a'])
+    >>> bd.flush_reload()
+    >>> bd.bug_from_uuid('a').severity
+    'wishlist'
+    >>> ret = ui.run(cmd, args=['none', '/a'])
+    Traceback (most recent call last):
+    UserError: Invalid severity level: none
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'severity'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.args.extend([
+                libbe.command.Argument(
+                    name='severity', metavar='SEVERITY', default=None,
+                    completion_callback=libbe.command.util.complete_severity),
+                libbe.command.Argument(
+                    name='bug-id', metavar='BUG-ID', default=None,
+                    repeatable=True,
+                    completion_callback=libbe.command.util.complete_bug_id),
+                ])
+
+    def _run(self, **params):
+        bugdir = self._get_bugdir()
+        for bug_id in params['bug-id']:
+            bug,dummy_comment = \
+                libbe.command.util.bug_comment_from_user_id(bugdir, bug_id)
+            if bug.severity != params['severity']:
+                try:
+                    bug.severity = params['severity']
+                except ValueError, e:
+                    if e.name != 'severity':
+                        raise e
+                    raise libbe.command.UserError(
+                        'Invalid severity level: %s' % e.value)
+        return 0
+
+    def _long_help(self):
+        ret = ["""
+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 = self._get_bugdir()
+        except NotImplementedError:
+            pass # No tree, just show the defaults
+        longest_severity_len = max([len(s) for s in libbe.bug.severity_values])
+        for severity in libbe.bug.severity_values :
+            description = libbe.bug.severity_description[severity]
+            ret.append('%*s : %s\n' \
+                % (longest_severity_len, severity, description))
+        return ''.join(ret)
diff --git a/libbe/command/show.py b/libbe/command/show.py
new file mode 100644 (file)
index 0000000..ab3be73
--- /dev/null
@@ -0,0 +1,207 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Thomas Gerigk <tgerigk@gmx.de>
+#                         Thomas Habets <thomas@habets.pp.se>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import sys
+
+import libbe
+import libbe.command
+import libbe.command.util
+import libbe.util.id
+import libbe.version
+import libbe._version
+
+
+class Show (libbe.command.Command):
+    """Show a particular bug, comment, or combination of both.
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> io.stdout.encoding = 'ascii'
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_bugdir(bd)
+    >>> cmd = Show(ui=ui)
+
+    >>> ret = ui.run(cmd, args=['/a',])  # doctest: +ELLIPSIS
+              ID : a
+      Short name : abc/a
+        Severity : minor
+          Status : open
+        Assigned : 
+        Reporter : 
+         Creator : John Doe <jdoe@example.com>
+         Created : ...
+    Bug A
+    <BLANKLINE>
+
+    >>> ret = ui.run(cmd, {'xml':True}, ['/a'])  # doctest: +ELLIPSIS
+    <?xml version="1.0" encoding="..." ?>
+    <be-xml>
+      <version>
+        <tag>...</tag>
+        <branch-nick>...</branch-nick>
+        <revno>...</revno>
+        <revision-id>...</revision-id>
+      </version>
+      <bug>
+        <uuid>a</uuid>
+        <short-name>abc/a</short-name>
+        <severity>minor</severity>
+        <status>open</status>
+        <creator>John Doe &lt;jdoe@example.com&gt;</creator>
+        <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+        <summary>Bug A</summary>
+      </bug>
+    </be-xml>
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'show'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='xml', short_name='x',
+                                     help='Dump as XML'),
+                libbe.command.Option(name='only-raw-body',
+                    help="When printing only a single comment, just print it's"
+                  " body.  This allows extraction of non-text content types."),
+                libbe.command.Option(name='no-comments', short_name='c',
+                    help="Disable comment output.  This is useful if you just "
+                         "want more details on a bug's current status."),
+                ])
+        self.args.extend([
+                libbe.command.Argument(
+                    name='id', metavar='ID', default=None,
+                    optional=True, repeatable=True,
+                    completion_callback=libbe.command.util.complete_bug_comment_id),
+                ])
+
+    def _run(self, **params):
+        bugdir = self._get_bugdir()
+        if params['only-raw-body'] == True:
+            if len(params['id']) != 1:
+                raise libbe.command.UsageError(
+                    'only one ID accepted with --only-raw-body')
+            bug,comment = libbe.command.util.bug_comment_from_user_id(
+                bugdir, params['id'][0])
+            if comment == bug.comment_root:
+                raise libbe.command.UsageError(
+                    "--only-raw-body requires a comment ID, not '%s'"
+                    % params['id'][0])
+            sys.__stdout__.write(comment.body)
+            return 0
+        print >> self.stdout, \
+            output(bugdir, params['id'], encoding=self.stdout.encoding,
+                   as_xml=params['xml'],
+                   with_comments=not params['no-comments'])
+        return 0
+
+    def _long_help(self):
+        return """
+Show all information about the bugs or comments whose IDs are given.
+If no IDs are given, show the entire repository.
+
+Without the --xml flag set, 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.  With the --xml flag set, there will never be any root comments,
+so mix and match away (the bug listings for directly requested
+comments will be restricted to the bug uuid and the requested
+comment(s)).
+
+Directly requested comments will be grouped by their parent bug and
+placed at the end of the output, so the ordering may not match the
+order of the listed IDs.
+"""
+
+def _sort_ids(bugdir, ids, with_comments=True):
+    bugs = []
+    root_comments = {}
+    for id in ids:
+        p = libbe.util.id.parse_user(bugdir, id)
+        if p['type'] == 'bug':
+            bugs.append(p['bug'])
+        elif with_comments == True:
+            if p['bug'] not in root_comments:
+                root_comments[p['bug']] = [p['comment']]
+            else:
+                root_comments[p['bug']].append(p['comment'])
+    for bugname in root_comments.keys():
+        assert bugname not in bugs, \
+            'specifically requested both #/%s/%s# and #/%s#' \
+            % (bugname, root_comments[bugname][0], bugname)
+    return (bugs, root_comments)
+
+def _xml_header(encoding):
+    lines = ['<?xml version="1.0" encoding="%s" ?>' % encoding,
+             '<be-xml>',
+             '  <version>',
+             '    <tag>%s</tag>' % libbe.version.version()]
+    for tag in ['branch-nick', 'revno', 'revision-id']:
+        value = libbe._version.version_info[tag.replace('-', '_')]
+        lines.append('    <%s>%s</%s>' % (tag, value, tag))
+    lines.append('  </version>')
+    return lines
+
+def _xml_footer():
+    return ['</be-xml>']
+
+def output(bd, ids, encoding, as_xml=True, with_comments=True):
+    if ids == None or len(ids) == 0:
+        bd.load_all_bugs()
+        ids = [bug.id.user() for bug in bd]
+    bugs,root_comments = _sort_ids(bd, ids, with_comments)
+    lines = []
+    if as_xml:
+        lines.extend(_xml_header(encoding))
+    else:
+        spaces_left = len(ids) - 1
+    for bugname in bugs:
+        bug = bd.bug_from_uuid(bugname)
+        if as_xml:
+            lines.append(bug.xml(indent=2, show_comments=with_comments))
+        else:
+            lines.append(bug.string(show_comments=with_comments))
+            if spaces_left > 0:
+                spaces_left -= 1
+                lines.append('') # add a blank line between bugs/comments
+    for bugname,comments in root_comments.items():
+        bug = bd.bug_from_uuid(bugname)
+        if as_xml:
+            lines.extend(['  <bug>', '    <uuid>%s</uuid>' % bug.uuid])
+        for commname in comments:
+            try:
+                comment = bug.comment_root.comment_from_uuid(commname)
+            except KeyError, e:
+                raise libbe.command.UserError(e.message)
+            if as_xml:
+                lines.append(comment.xml(indent=4))
+            else:
+                lines.append(comment.string())
+                if spaces_left > 0:
+                    spaces_left -= 1
+                    lines.append('') # add a blank line between bugs/comments
+        if as_xml:
+            lines.append('</bug>')
+    if as_xml:
+        lines.extend(_xml_footer())
+    return '\n'.join(lines)
diff --git a/libbe/command/status.py b/libbe/command/status.py
new file mode 100644 (file)
index 0000000..1659f75
--- /dev/null
@@ -0,0 +1,108 @@
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import libbe
+import libbe.bug
+import libbe.command
+import libbe.command.util
+
+
+class Status (libbe.command.Command):
+    """Change a bug's status level
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_bugdir(bd)
+    >>> cmd = Status(ui=ui)
+    >>> cmd._storage = bd.storage
+
+    >>> bd.bug_from_uuid('a').status
+    'open'
+    >>> ret = ui.run(cmd, args=['closed', '/a'])
+    >>> bd.flush_reload()
+    >>> bd.bug_from_uuid('a').status
+    'closed'
+    >>> ret = ui.run(cmd, args=['none', '/a'])
+    Traceback (most recent call last):
+    UserError: Invalid status level: none
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'status'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.args.extend([
+                libbe.command.Argument(
+                    name='status', metavar='STATUS', default=None,
+                    completion_callback=libbe.command.util.complete_status),
+                libbe.command.Argument(
+                    name='bug-id', metavar='BUG-ID', default=None,
+                    repeatable=True,
+                    completion_callback=libbe.command.util.complete_bug_id),
+                ])
+
+    def _run(self, **params):
+        bugdir = self._get_bugdir()
+        for bug_id in params['bug-id']:
+            bug,dummy_comment = \
+                libbe.command.util.bug_comment_from_user_id(bugdir, bug_id)
+            if bug.status != params['status']:
+                try:
+                    bug.status = params['status']
+                except ValueError, e:
+                    if e.name != 'status':
+                        raise e
+                    raise libbe.command.UserError(
+                        'Invalid status level: %s' % e.value)
+        return 0
+
+    def _long_help(self):
+        longest_status_len = max([len(s) for s in libbe.bug.status_values])
+        active_statuses = []
+        for status in libbe.bug.active_status_values :
+            description = libbe.bug.status_description[status]
+            s = '%*s : %s' % (longest_status_len, status, description)
+            active_statuses.append(s)
+        inactive_statuses = []
+        for status in libbe.bug.inactive_status_values :
+            description = libbe.bug.status_description[status]
+            s = '%*s : %s' % (longest_status_len, status, description)
+            inactive_statuses.append(s)
+        ret = """
+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 ret
diff --git a/libbe/command/subscribe.py b/libbe/command/subscribe.py
new file mode 100644 (file)
index 0000000..d1cf72e
--- /dev/null
@@ -0,0 +1,385 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import copy
+import os
+
+import libbe
+import libbe.bug
+import libbe.command
+import libbe.diff
+import libbe.command.util
+import libbe.util.id
+import libbe.util.tree
+
+
+TAG="SUBSCRIBE:"
+
+
+class Subscribe (libbe.command.Command):
+    """(Un)subscribe to change notification
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_bugdir(bd)
+    >>> cmd = Subscribe(ui=ui)
+
+    >>> a = bd.bug_from_uuid('a')
+    >>> print a.extra_strings
+    []
+    >>> ret = ui.run(cmd, {'subscriber':'John Doe <j@doe.com>'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+    Subscriptions for abc/a:
+    John Doe <j@doe.com>    all    *
+    >>> bd.flush_reload()
+    >>> a = bd.bug_from_uuid('a')
+    >>> print a.extra_strings
+    ['SUBSCRIBE:John Doe <j@doe.com>\\tall\\t*']
+    >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.com,b.net'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+    Subscriptions for abc/a:
+    Jane Doe <J@doe.com>    all    a.com,b.net
+    John Doe <j@doe.com>    all    *
+    >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.edu'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+    Subscriptions for abc/a:
+    Jane Doe <J@doe.com>    all    a.com,a.edu,b.net
+    John Doe <j@doe.com>    all    *
+    >>> ret = ui.run(cmd, {'unsubscribe':True, 'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.com'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+    Subscriptions for abc/a:
+    Jane Doe <J@doe.com>    all    a.edu,b.net
+    John Doe <j@doe.com>    all    *
+    >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>', 'servers':'*'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+    Subscriptions for abc/a:
+    Jane Doe <J@doe.com>    all    *
+    John Doe <j@doe.com>    all    *
+    >>> ret = ui.run(cmd, {'unsubscribe':True, 'subscriber':'Jane Doe <J@doe.com>'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+    Subscriptions for abc/a:
+    John Doe <j@doe.com>    all    *
+    >>> ret = ui.run(cmd, {'unsubscribe':True, 'subscriber':'John Doe <j@doe.com>'}, ['/a'])
+    >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>', 'types':'new'}, ['DIR']) # doctest: +NORMALIZE_WHITESPACE
+    Subscriptions for bug directory:
+    Jane Doe <J@doe.com>    new    *
+    >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>'}, ['DIR']) # doctest: +NORMALIZE_WHITESPACE
+    Subscriptions for bug directory:
+    Jane Doe <J@doe.com>    all    *
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'subscribe'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='unsubscribe', short_name='u',
+                    help='Unsubscribe instead of subscribing'),
+                libbe.command.Option(name='list-all', short_name='a',
+                    help='List all subscribers (no ID argument, read only action)'),
+                libbe.command.Option(name='list', short_name='l',
+                    help='List subscribers (read only action).'),
+                libbe.command.Option(name='subscriber', short_name='s',
+                    help='Email address of the subscriber (defaults to your user id).',
+                    arg=libbe.command.Argument(
+                        name='subscriber', metavar='EMAIL')),
+                libbe.command.Option(name='servers', short_name='S',
+                    help='Servers from which you want notification.',
+                    arg=libbe.command.Argument(
+                        name='servers', metavar='STRING')),
+                libbe.command.Option(name='types', short_name='t',
+                    help='Types of changes you wish to be notified about.',
+                    arg=libbe.command.Argument(
+                        name='types', metavar='STRING')),
+                ])
+        self.args.extend([
+                libbe.command.Argument(
+                    name='id', metavar='ID', default=tuple(),
+                    optional=True, repeatable=True,
+                    completion_callback=libbe.command.util.complete_bug_comment_id),
+                ])
+
+    def _run(self, **params):
+        bugdir = self._get_bugdir()
+        if params['list-all'] == True or params['list'] == True:
+            writeable = bugdir.storage.writeable
+            bugdir.storage.writeable = False
+            if params['list-all'] == True:
+                assert len(params['id']) == 0, params['id']
+        subscriber = params['subscriber']
+        if subscriber == None:
+            subscriber = self._get_user_id()
+        if params['unsubscribe'] == True:
+            if params['servers'] == None:
+                params['servers'] = 'INVALID'
+            if params['types'] == None:
+                params['types'] = 'INVALID'
+        else:
+            if params['servers'] == None:
+                params['servers'] = '*'
+            if params['types'] == None:
+                params['types'] = 'all'
+        servers = params['servers'].split(',')
+        types = params['types'].split(',')
+
+        if len(params['id']) == 0:
+            params['id'] = [libbe.diff.BUGDIR_ID]
+        for _id in params['id']:
+            if _id == libbe.diff.BUGDIR_ID: # directory-wide subscriptions
+                type_root = libbe.diff.BUGDIR_TYPE_ALL
+                entity = bugdir
+                entity_name = 'bug directory'
+            else: # bug-specific subscriptions
+                type_root = libbe.diff.BUG_TYPE_ALL
+                bug,dummy_comment = libbe.command.util.bug_comment_from_user_id(
+                    bugdir, _id)
+                entity = bug
+                entity_name = bug.id.user()
+            if params['list-all'] == True:
+                entity_name = 'anything in the bug directory'
+            types = [libbe.diff.type_from_name(name, type_root, default=libbe.diff.INVALID_TYPE,
+                                         default_ok=params['unsubscribe'])
+                     for name in types]
+            estrs = entity.extra_strings
+            if params['list'] == True or params['list-all'] == True:
+                pass
+            else: # alter subscriptions
+                if params['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 params['list-all'] == True:
+                bugdir.load_all_bugs()
+                subscriptions = get_bugdir_subscribers(bugdir, servers[0])
+            else:
+                subscriptions = []
+                for estr in entity.extra_strings:
+                    if estr.startswith(TAG):
+                        subscriptions.append(estr[len(TAG):])
+
+            if len(subscriptions) > 0:
+                print >> self.stdout, 'Subscriptions for %s:' % entity_name
+                print >> self.stdout, '\n'.join(subscriptions)
+        if params['list-all'] == True or params['list'] == True:
+            bugdir.storage.writeable = writeable
+        return 0
+
+    def _long_help(self):
+        return """
+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 %s:
+%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.
+""" % (libbe.diff.BUG_TYPE_ALL.string_tree(6), libbe.diff.BUGDIR_ID,
+       libbe.diff.BUGDIR_TYPE_ALL.string_tree(6),
+       libbe.diff.BUGDIR_TYPE_ALL)
+
+
+# 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 = [libbe.diff.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 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 <j@doe.com>", [libbe.diff.BUGDIR_TYPE_ALL],
+    ...                ["a.com"], libbe.diff.BUGDIR_TYPE_ALL)
+    >>> es = subscribe(es, "Jane Doe <J@doe.com>", [libbe.diff.BUGDIR_TYPE_NEW],
+    ...                ["*"], libbe.diff.BUGDIR_TYPE_ALL)
+    >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "a.com", libbe.diff.BUGDIR_TYPE_ALL)
+    ['John Doe <j@doe.com>']
+    >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "a.com", libbe.diff.BUGDIR_TYPE_ALL,
+    ...     match_descendant_types=True)
+    ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
+    >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "b.net", libbe.diff.BUGDIR_TYPE_ALL,
+    ...     match_descendant_types=True)
+    ['Jane Doe <J@doe.com>']
+    >>> sgs(es, libbe.diff.BUGDIR_TYPE_NEW, "a.com", libbe.diff.BUGDIR_TYPE_ALL)
+    ['Jane Doe <J@doe.com>']
+    >>> sgs(es, libbe.diff.BUGDIR_TYPE_NEW, "a.com", libbe.diff.BUGDIR_TYPE_ALL,
+    ... match_ancestor_types=True)
+    ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
+    """
+    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 "%(bugdir_id)s" (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 <j@doe.com>",
+    ...                [libbe.diff.BUGDIR_TYPE_ALL], ["a.com"], libbe.diff.BUGDIR_TYPE_ALL)
+    >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe <J@doe.com>",
+    ...                [libbe.diff.BUGDIR_TYPE_NEW], ["*"], libbe.diff.BUGDIR_TYPE_ALL)
+    >>> a.extra_strings = subscribe(a.extra_strings, "John Doe <j@doe.com>",
+    ...                [libbe.diff.BUG_TYPE_ALL], ["a.com"], libbe.diff.BUG_TYPE_ALL)
+    >>> subscribers = get_bugdir_subscribers(bd, "a.com")
+    >>> subscribers["Jane Doe <J@doe.com>"]["%(bugdir_id)s"]
+    [<SubscriptionType: new>]
+    >>> subscribers["John Doe <j@doe.com>"]["%(bugdir_id)s"]
+    [<SubscriptionType: all>]
+    >>> subscribers["John Doe <j@doe.com>"]["a"]
+    [<SubscriptionType: all>]
+    >>> get_bugdir_subscribers(bd, "b.net")
+    {'Jane Doe <J@doe.com>': {'%(bugdir_id)s': [<SubscriptionType: new>]}}
+    >>> bd.cleanup()
+    """ % {'bugdir_id':libbe.diff.BUGDIR_ID}
+    subscribers = {}
+    for sub in get_subscribers(bugdir.extra_strings, libbe.diff.BUGDIR_TYPE_ALL,
+                               server, libbe.diff.BUGDIR_TYPE_ALL,
+                               match_descendant_types=True):
+        i,s,ts,srvs = _get_subscriber(bugdir.extra_strings, sub,
+                                      libbe.diff.BUGDIR_TYPE_ALL)
+        subscribers[sub] = {"DIR":ts}
+    for bug in bugdir:
+        for sub in get_subscribers(bug.extra_strings, libbe.diff.BUG_TYPE_ALL,
+                                   server, libbe.diff.BUG_TYPE_ALL,
+                                   match_descendant_types=True):
+            i,s,ts,srvs = _get_subscriber(bug.extra_strings, sub,
+                                          libbe.diff.BUG_TYPE_ALL)
+            if sub in subscribers:
+                subscribers[sub][bug.uuid] = ts
+            else:
+                subscribers[sub] = {bug.uuid:ts}
+    return subscribers
diff --git a/libbe/command/tag.py b/libbe/command/tag.py
new file mode 100644 (file)
index 0000000..f4dc3ba
--- /dev/null
@@ -0,0 +1,152 @@
+# Copyright (C) 2009-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import libbe
+import libbe.command
+import libbe.command.util
+
+
+TAG_TAG = 'TAG:'
+
+
+class Tag (libbe.command.Command):
+    __doc__ = """Tag a bug, or search bugs for tags
+
+    >>> import sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_bugdir(bd)
+    >>> cmd = Tag(ui=ui)
+
+    >>> a = bd.bug_from_uuid('a')
+    >>> print a.extra_strings
+    []
+    >>> ret = ui.run(cmd, args=['/a', 'GUI'])
+    Tags for abc/a:
+    GUI
+    >>> bd.flush_reload()
+    >>> a = bd.bug_from_uuid('a')
+    >>> print a.extra_strings
+    ['%(tag_tag)sGUI']
+    >>> ret = ui.run(cmd, args=['/a', 'later'])
+    Tags for abc/a:
+    GUI
+    later
+    >>> ret = ui.run(cmd, args=['/a'])
+    Tags for abc/a:
+    GUI
+    later
+    >>> ret = ui.run(cmd, {'list':True})
+    GUI
+    later
+    >>> ret = ui.run(cmd, args=['/a', 'Alphabetically first'])
+    Tags for abc/a:
+    Alphabetically first
+    GUI
+    later
+    >>> bd.flush_reload()
+    >>> a = bd.bug_from_uuid('a')
+    >>> print a.extra_strings
+    ['%(tag_tag)sAlphabetically first', '%(tag_tag)sGUI', '%(tag_tag)slater']
+    >>> a.extra_strings = []
+    >>> print a.extra_strings
+    []
+    >>> ret = ui.run(cmd, args=['/a'])
+    >>> bd.flush_reload()
+    >>> a = bd.bug_from_uuid('a')
+    >>> print a.extra_strings
+    []
+    >>> ret = ui.run(cmd, args=['/a', 'Alphabetically first'])
+    Tags for abc/a:
+    Alphabetically first
+    >>> ret = ui.run(cmd, {'remove':True}, ['/a', 'Alphabetically first'])
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """ % {'tag_tag':TAG_TAG}
+    name = 'tag'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='remove', short_name='r',
+                    help='Remove TAG (instead of adding it)'),
+                libbe.command.Option(name='list', short_name='l',
+                    help='List all available tags and exit'),
+                ])
+        self.args.extend([
+                libbe.command.Argument(
+                    name='id', metavar='BUG-ID', optional=True,
+                    completion_callback=libbe.command.util.complete_bug_id),
+                libbe.command.Argument(
+                    name='tag', metavar='TAG', default=tuple(),
+                    optional=True, repeatable=True),
+                ])
+
+    def _run(self, **params):
+        if params['id'] == None and params['list'] == False:
+            raise libbe.command.UserError('Please specify a bug id.')
+        if params['id'] != None and params['list'] == True:
+            raise libbe.command.UserError(
+                'Do not specify a bug id with the --list option.')
+        bugdir = self._get_bugdir()
+        if params['list'] == True:
+            bugdir.load_all_bugs()
+            tags = []
+            for bug in bugdir:
+                for estr in bug.extra_strings:
+                    if estr.startswith(TAG_TAG):
+                        tag = estr[len(TAG_TAG):]
+                        if tag not in tags:
+                            tags.append(tag)
+            tags.sort()
+            if len(tags) > 0:
+                print >> self.stdout, '\n'.join(tags)
+            return 0
+
+        bug,dummy_comment = libbe.command.util.bug_comment_from_user_id(
+            bugdir, params['id'])
+        if len(params['tag']) > 0:
+            estrs = bug.extra_strings
+            for tag in params['tag']:
+                tag_string = '%s%s' % (TAG_TAG, tag)
+                if params['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_TAG):
+                tags.append(estr[len(TAG_TAG):])
+
+        if len(tags) > 0:
+            print "Tags for %s:" % bug.id.user()
+            print '\n'.join(tags)
+        return 0
+
+    def _long_help(self):
+        return """
+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 %s<your-tag>
+""" % TAG_TAG
diff --git a/libbe/command/target.py b/libbe/command/target.py
new file mode 100644 (file)
index 0000000..f8a956b
--- /dev/null
@@ -0,0 +1,209 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Chris Ball <cjb@laptop.org>
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Marien Zwart <marienz@gentoo.org>
+#                         Thomas Gerigk <tgerigk@gmx.de>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import libbe
+import libbe.command
+import libbe.command.util
+import libbe.command.depend
+
+
+class Target (libbe.command.Command):
+    """Assorted bug target manipulations and queries
+
+    >>> import os, StringIO, sys
+    >>> import libbe.bugdir
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+    >>> io = libbe.command.StringInputOutput()
+    >>> io.stdout = sys.stdout
+    >>> ui = libbe.command.UserInterface(io=io)
+    >>> ui.storage_callbacks.set_storage(bd.storage)
+    >>> cmd = Target(ui=ui)
+
+    >>> ret = ui.run(cmd, args=['/a'])
+    No target assigned.
+    >>> ret = ui.run(cmd, args=['/a', 'tomorrow'])
+    >>> ret = ui.run(cmd, args=['/a'])
+    tomorrow
+
+    >>> ui.io.stdout = StringIO.StringIO()
+    >>> ret = ui.run(cmd, {'resolve':True}, ['tomorrow'])
+    >>> output = ui.io.get_stdout().strip()
+    >>> bd.flush_reload()
+    >>> target = bd.bug_from_uuid(output)
+    >>> print target.summary
+    tomorrow
+    >>> print target.severity
+    target
+
+    >>> ui.io.stdout = sys.stdout
+    >>> ret = ui.run(cmd, args=['/a', 'none'])
+    >>> ret = ui.run(cmd, args=['/a'])
+    No target assigned.
+    >>> ui.cleanup()
+    >>> bd.cleanup()
+    """
+    name = 'target'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='resolve', short_name='r',
+                    help="Print the UUID for the target bug whose summary "
+                    "matches TARGET.  If TARGET is not given, print the UUID "
+                    "of the current bugdir target."),
+                ])
+        self.args.extend([
+                libbe.command.Argument(
+                    name='id', metavar='BUG-ID', optional=True,
+                    completion_callback=libbe.command.util.complete_bug_id),
+                libbe.command.Argument(
+                    name='target', metavar='TARGET', optional=True,
+                    completion_callback=complete_target),
+                ])
+
+    def _run(self, **params):
+        if params['resolve'] == False:
+            if params['id'] == None:
+                raise libbe.command.UserError('Please specify a bug id.')
+        else:
+            if params['target'] != None:
+                raise libbe.command.UserError('Too many arguments')
+            params['target'] = params.pop('id')
+        bugdir = self._get_bugdir()
+        if params['resolve'] == True:
+            bug = bug_from_target_summary(bugdir, params['target'])
+            if bug == None:
+                print >> self.stdout, 'No target assigned.'
+            else:
+                print >> self.stdout, bug.uuid
+            return 0
+        bug,dummy_comment = libbe.command.util.bug_comment_from_user_id(
+            bugdir, params['id'])
+        if params['target'] == None:
+            target = bug_target(bugdir, bug)
+            if target == None:
+                print >> self.stdout, 'No target assigned.'
+            else:
+                print >> self.stdout, target.summary
+        else:
+            if params['target'] == 'none':
+                target = remove_target(bugdir, bug)
+            else:
+                target = add_target(bugdir, bug, params['target'])
+        return 0
+
+    def usage(self):
+        return 'usage: be %(name)s BUG-ID [TARGET]\nor:    be %(name)s --resolve [TARGET]' \
+            % vars(self.__class__)
+
+    def _long_help(self):
+        return """
+Assorted bug target manipulations and queries.
+
+If no target is specified, the bug's current target is printed.  If
+TARGET is specified, it will be assigned to the bug, creating a new
+target bug if necessary.
+
+Targets are free-form; 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 --resolve TARGET` form, print the UUID
+of the target-bug with summary TARGET.  If target is not given, return
+use the bugdir's current target (see `be set`).
+
+If you want to list all bugs blocking the current target, try
+  $ be depend --status -closed,fixed,wontfix --severity -target \
+    $(be target --resolve)
+
+If you want to set the current bugdir target by summary (rather than
+by UUID), try
+  $ be set target $(be target --resolve SUMMARY)
+"""
+
+def bug_from_target_summary(bugdir, summary=None):
+    if summary == None:
+        if bugdir.target == None:
+            return None
+        else:
+            return bugdir.bug_from_uuid(bugdir.target)
+    matched = []
+    for uuid in bugdir.uuids():
+        bug = bugdir.bug_from_uuid(uuid)
+        if bug.severity == 'target' and bug.summary == summary:
+            matched.append(bug)
+    if len(matched) == 0:
+        return None
+    if len(matched) > 1:
+        raise Exception('Several targets with same summary:  %s'
+                        % '\n  '.join([bug.uuid for bug in matched]))
+    return matched[0]
+
+def bug_target(bugdir, bug):
+    if bug.severity == 'target':
+        return bug
+    matched = []
+    for blocked in libbe.command.depend.get_blocks(bugdir, bug):
+        if blocked.severity == 'target':
+            matched.append(blocked)
+    if len(matched) == 0:
+        return None
+    if len(matched) > 1:
+        raise Exception('This bug (%s) blocks several targets:  %s'
+                        % (bug.uuid,
+                           '\n  '.join([b.uuid for b in matched])))
+    return matched[0]
+
+def remove_target(bugdir, bug):
+    target = bug_target(bugdir, bug)
+    libbe.command.depend.remove_block(target, bug)
+    return target
+
+def add_target(bugdir, bug, summary):
+    target = bug_from_target_summary(bugdir, summary)
+    if target == None:
+        target = bugdir.new_bug(summary=summary)
+        target.severity = 'target'
+    libbe.command.depend.add_block(target, bug)
+    return target
+
+def targets(bugdir):
+    """Generate all possible target bug summaries."""
+    bugdir.load_all_bugs()
+    for bug in bugdir:
+        if bug.severity == 'target':
+            yield bug.summary
+
+def target_dict(bugdir):
+    """
+    Return a dict with bug UUID keys and bug summary values for all
+    target bugs.
+    """
+    ret = {}
+    bugdir.load_all_bugs()
+    for bug in bugdir:
+        if bug.severity == 'target':
+            ret[bug.uuid] = bug.summary
+    return ret
+
+def complete_target(command, argument, fragment=None):
+    """List possible command completions for fragment."""
+    return targets(command._get_bugdir())
diff --git a/libbe/command/util.py b/libbe/command/util.py
new file mode 100644 (file)
index 0000000..6e8e36c
--- /dev/null
@@ -0,0 +1,203 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import glob
+import os.path
+
+import libbe
+import libbe.command
+
+class Completer (object):
+    def __init__(self, options):
+        self.options = options
+    def __call__(self, bugdir, fragment=None):
+        return [fragment]
+
+def complete_command(command, argument, fragment=None):
+    """
+    List possible command completions for fragment.
+
+    command argument is not used.
+    """
+    return list(libbe.command.commands(command_names=True))
+
+def comp_path(fragment=None):
+    """List possible path completions for fragment."""
+    if fragment == None:
+        fragment = '.'
+    comps = glob.glob(fragment+'*') + glob.glob(fragment+'/*')
+    if len(comps) == 1 and os.path.isdir(comps[0]):
+        comps.extend(glob.glob(comps[0]+'/*'))
+    return comps
+
+def complete_path(command, argument, fragment=None):
+    """List possible path completions for fragment."""
+    return comp_path(fragment)
+
+def complete_status(command, argument, fragment=None):
+    bd = command._get_bugdir()
+    import libbe.bug
+    return libbe.bug.status_values
+
+def complete_severity(command, argument, fragment=None):
+    bd = command._get_bugdir()
+    import libbe.bug
+    return libbe.bug.severity_values
+
+def assignees(bugdir):
+    bugdir.load_all_bugs()
+    return list(set([bug.assigned for bug in bugdir
+                     if bug.assigned != None]))
+
+def complete_assigned(command, argument, fragment=None):
+    return assignees(command._get_bugdir())
+
+def complete_extra_strings(command, argument, fragment=None):
+    if fragment == None:
+        return []
+    return [fragment]
+
+def complete_bug_id(command, argument, fragment=None):
+    return complete_bug_comment_id(command, argument, fragment,
+                                   comments=False)
+
+def complete_bug_comment_id(command, argument, fragment=None,
+                            active_only=True, comments=True):
+    import libbe.bugdir
+    import libbe.util.id
+    bd = command._get_bugdir()
+    if fragment == None or len(fragment) == 0:
+        fragment = '/'
+    try:
+        p = libbe.util.id.parse_user(bd, fragment)
+        matches = None
+        root,residual = (fragment, None)
+        if not root.endswith('/'):
+            root += '/'
+    except libbe.util.id.InvalidIDStructure, e:
+        return []
+    except libbe.util.id.NoIDMatches:
+        return []
+    except libbe.util.id.MultipleIDMatches, e:
+        if e.common == None:
+            # choose among bugdirs
+            return e.matches
+        common = e.common
+        matches = e.matches
+        root,residual = libbe.util.id.residual(common, fragment)
+        p = libbe.util.id.parse_user(bd, e.common)
+    bug = None
+    if matches == None: # fragment was complete, get a list of children uuids
+        if p['type'] == 'bugdir':
+            matches = bd.uuids()
+            common = bd.id.user()
+        elif p['type'] == 'bug':
+            if comments == False:
+                return [fragment]
+            bug = bd.bug_from_uuid(p['bug'])
+            matches = bug.uuids()
+            common = bug.id.user()
+        else:
+            assert p['type'] == 'comment', p
+            return [fragment]
+    if p['type'] == 'bugdir':
+        child_fn = bd.bug_from_uuid
+    elif p['type'] == 'bug':
+        if comments == False:
+            return[fragment]
+        if bug == None:
+            bug = bd.bug_from_uuid(p['bug'])
+        child_fn = bug.comment_from_uuid
+    elif p['type'] == 'comment':
+        assert matches == None, matches
+        return [fragment]
+    possible = []
+    common += '/'
+    for m in matches:
+        child = child_fn(m)
+        id = child.id.user()
+        possible.append(id.replace(common, root))
+    return possible
+
+def select_values(string, possible_values, name="unkown"):
+    """
+    This function allows the user to select values from a list of
+    possible values.  The default is to select all the values:
+
+    >>> select_values(None, ['abc', 'def', 'hij'])
+    ['abc', 'def', 'hij']
+
+    The user selects values with a comma-separated limit_string.
+    Prepending a minus sign to such a list denotes blacklist mode:
+
+    >>> select_values('-abc,hij', ['abc', 'def', 'hij'])
+    ['def']
+
+    Without the leading -, the selection is in whitelist mode:
+
+    >>> select_values('abc,hij', ['abc', 'def', 'hij'])
+    ['abc', 'hij']
+
+    In either case, appropriate errors are raised if on of the
+    user-values is not in the list of possible values.  The name
+    parameter lets you make the error message more clear:
+
+    >>> select_values('-xyz,hij', ['abc', 'def', 'hij'], name="foobar")
+    Traceback (most recent call last):
+      ...
+    UserError: Invalid foobar xyz
+      ['abc', 'def', 'hij']
+    >>> select_values('xyz,hij', ['abc', 'def', 'hij'], name="foobar")
+    Traceback (most recent call last):
+      ...
+    UserError: Invalid foobar xyz
+      ['abc', 'def', 'hij']
+    """
+    possible_values = list(possible_values) # don't alter the original
+    if string == None:
+        pass
+    elif string.startswith('-'):
+        blacklisted_values = set(string[1:].split(','))
+        for value in blacklisted_values:
+            if value not in possible_values:
+                raise libbe.command.UserError('Invalid %s %s\n  %s'
+                                % (name, value, possible_values))
+            possible_values.remove(value)
+    else:
+        whitelisted_values = string.split(',')
+        for value in whitelisted_values:
+            if value not in possible_values:
+                raise libbe.command.UserError(
+                    'Invalid %s %s\n  %s'
+                    % (name, value, possible_values))
+        possible_values = whitelisted_values
+    return possible_values
+
+def bug_comment_from_user_id(bugdir, id):
+    p = libbe.util.id.parse_user(bugdir, id)
+    if not p['type'] in ['bug', 'comment']:
+        raise libbe.command.UserError(
+            '%s is a %s id, not a bug or comment id' % (id, p['type']))
+    if p['bugdir'] != bugdir.uuid:
+        raise libbe.command.UserError(
+            "%s doesn't belong to this bugdir (%s)"
+            % (id, bugdir.uuid))
+    bug = bugdir.bug_from_uuid(p['bug'])
+    if 'comment' in p:
+        comment = bug.comment_from_uuid(p['comment'])
+    else:
+        comment = bug.comment_root
+    return (bug, comment)
index 41bc7e6c1ea629277e8836186ad570ab676ea22a..d8632a4dc670a10604710f0e083488375bc58577 100644 (file)
@@ -1,5 +1,4 @@
-# Bugs Everywhere, a distributed bugtracker
-# Copyright (C) 2008-2009 Chris Ball <cjb@laptop.org>
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
 #                         Thomas Habets <thomas@habets.pp.se>
 #                         W. Trevor King <wking@drexel.edu>
 #
@@ -17,8 +16,7 @@
 # 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.
+"""Define the :class:`Comment` class for representing bug comments.
 """
 
 import base64
@@ -27,21 +25,31 @@ import os.path
 import sys
 import time
 import types
+try:
+    from email.mime.base import MIMEBase
+    from email.encoders import encode_base64
+except ImportError:
+    # adjust to old python 2.4
+    from email.MIMEBase import MIMEBase
+    from email.Encoders import encode_base64
 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, \
+import libbe
+import libbe.util.id
+from libbe.storage.util.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
+import libbe.storage.util.settings_object as settings_object
+import libbe.storage.util.mapfile as mapfile
+from libbe.util.tree import Tree
+import libbe.util.utility as utility
+
+if libbe.TESTING == True:
+    import doctest
 
 
 class InvalidShortname(KeyError):
@@ -51,14 +59,6 @@ class InvalidShortname(KeyError):
         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)
@@ -73,84 +73,37 @@ class DiskAccessRequired (Exception):
 
 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):
+def load_comments(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)
+    uuids = []
+    for id in libbe.util.id.child_uuids(
+                  bug.storage.children(
+                      bug.id.storage())):
+        uuids.append(id)
     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)
+    for uuid in uuids:
+        comm = Comment(bug, uuid, from_storage=True)
         if load_full == True:
             comm.load_settings()
             dummy = comm.body # force the body to load
         comments.append(comm)
-    return list_to_root(comments, bug)
+    bug.comment_root = Comment(bug, uuid=INVALID_UUID)
+    bug.add_comments(comments, ignore_missing_references=True)
+    return bug.comment_root
 
-def saveComments(bug):
-    if bug.sync_with_disk == False:
-        raise DiskAccessRequired("save comments")
+def save_comments(bug):
     for comment in bug.comment_root.traverse():
         comment.save()
 
 
-class Comment(Tree, settings_object.SavedSettingsObject):
-    """
+class Comment (Tree, settings_object.SavedSettingsObject):
+    """Comments are a notes that attach to :class:`~libbe.bug.Bug`\s in
+    threaded trees.  In mailing-list terms, a comment is analogous to
+    a single part of an email.
+
     >>> c = Comment()
     >>> c.uuid != None
     True
@@ -205,15 +158,19 @@ class Comment(Tree, settings_object.SavedSettingsObject):
                     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)
+        if self.storage != None and self.storage.is_readable() \
+                and self.uuid != INVALID_UUID:
+            return self.storage.get(self.id.storage("body"),
+                decode=self.content_type.startswith("text/"))
     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 self.uuid != INVALID_UUID, self
+        if self.content_type.startswith('text/') \
+                and self.bug != None and self.bug.bugdir != None:
+            new = libbe.util.id.short_to_long_text([self.bug.bugdir], new)
+        if (self.storage != None and self.storage.writeable == 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)
+            self.storage.set(self.id.storage("body"), new)
 
     @Property
     @change_hook_property(hook=_set_comment_body)
@@ -222,16 +179,6 @@ class Comment(Tree, settings_object.SavedSettingsObject):
     @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)
@@ -246,35 +193,42 @@ class Comment(Tree, settings_object.SavedSettingsObject):
                          mutable=True)
     def extra_strings(): return {}
 
-    def __init__(self, bug=None, uuid=None, from_disk=False,
-                 in_reply_to=None, body=None):
+    def __init__(self, bug=None, uuid=None, from_storage=False,
+                 in_reply_to=None, body=None, content_type=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.
+        Set ``from_storage=True`` to load an old comment.
+        Set ``from_storage=False`` to create a new comment.
+
+        The ``uuid`` option is required when ``from_storage==True``.
+
+        The in_reply_to, body, and content_type options are only used
+        if ``from_storage==False`` (the default).  When
+        ``from_storage==True``, they are loaded from the bug database.
+        ``content_type`` decides if the body should be run through
+        :func:`util.id.short_to_long_text` before saving.  See
+        :meth:`_set_comment_body` for details.
+
+        ``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
+        self.storage = None
+        self.uuid = uuid
+        self.id = libbe.util.id.ID(self, 'comment')
+        if from_storage == False:
             if uuid == None:
-                self.uuid = uuid_gen()
+                self.uuid = libbe.util.id.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
+            if content_type != None:
+                self.content_type = content_type
             self.body = body
+        if self.bug != None:
+            self.storage = self.bug.storage
+        if from_storage == False:
+            if self.storage != None and self.storage.is_writeable():
+                self.save()
 
     def __cmp__(self, other):
         return cmp_full(self, other)
@@ -287,7 +241,7 @@ class Comment(Tree, settings_object.SavedSettingsObject):
         >>> comm.author = "Jane Doe <jdoe@example.com>"
         >>> print comm
         --------- Comment ---------
-        Name: com-1
+        Name: //com
         From: Jane Doe <jdoe@example.com>
         Date: Thu, 20 Nov 2008 15:55:11 +0000
         <BLANKLINE>
@@ -308,17 +262,37 @@ class Comment(Tree, settings_object.SavedSettingsObject):
         value = getattr(self, setting)
         if value == None:
             return ""
-        return str(value)
+        if type(value) not in types.StringTypes:
+            return str(value)
+        return value
 
-    def xml(self, indent=0, shortname=None):
+    def safe_in_reply_to(self):
+        """
+        Return self.in_reply_to, except...
+
+          * if no comment matches that id, in which case return None.
+          * if that id matches another comments .alt_id, in which case
+            return the matching comments .uuid.
+        """
+        if self.in_reply_to == None:
+            return None
+        else:
+            try:
+                irt_comment = self.bug.comment_from_uuid(
+                    self.in_reply_to, match_alt_id=True)
+                return irt_comment.uuid
+            except KeyError:
+                return None
+
+    def xml(self, indent=0):
         """
         >>> 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")
+        >>> print comm.xml(indent=2)
           <comment>
             <uuid>0123</uuid>
-            <short-name>com-1</short-name>
+            <short-name>//012</short-name>
             <author></author>
             <date>Thu, 01 Jan 1970 00:00:00 +0000</date>
             <content-type>text/plain</content-type>
@@ -326,101 +300,197 @@ class Comment(Tree, settings_object.SavedSettingsObject):
         insightful
         remarks</body>
           </comment>
+        >>> comm.content_type = 'image/png'
+        >>> print comm.xml()
+        <comment>
+          <uuid>0123</uuid>
+          <short-name>//012</short-name>
+          <author></author>
+          <date>Thu, 01 Jan 1970 00:00:00 +0000</date>
+          <content-type>image/png</content-type>
+          <body>U29tZQppbnNpZ2h0ZnVsCnJlbWFya3MK
+        </body>
+        </comment>
         """
-        if shortname == None:
-            shortname = self.uuid
-        if self.content_type.startswith("text/"):
-            body = (self.body or "").rstrip('\n')
+        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 = ["<comment>"]
+            msg = MIMEBase(maintype, subtype)
+            msg.set_payload(self.body or '')
+            encode_base64(msg)
+            body = base64.encodestring(self.body or '')
+        info = [('uuid', self.uuid),
+                ('alt-id', self.alt_id),
+                ('short-name', self.id.user()),
+                ('in-reply-to', self.safe_in_reply_to()),
+                ('author', self._setting_attr_string('author')),
+                ('date', self.date),
+                ('content-type', self.content_type),
+                ('body', body)]
+        lines = ['<comment>']
         for (k,v) in info:
             if v != None:
                 lines.append('  <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
-        lines.append("</comment>")
+        for estr in self.extra_strings:
+            lines.append('  <extra-string>%s</extra-string>' % estr)
+        lines.append('</comment>')
         istring = ' '*indent
         sep = '\n' + istring
         return istring + sep.join(lines).rstrip('\n')
 
     def from_xml(self, xml_string, verbose=True):
-        """
+        u"""
         Note: If alt-id is not given, translates any <uuid> fields to
         <alt-id> 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")
+        >>> commA.author = u'Fran\xe7ois'
+        >>> commA.extra_strings += ['TAG: very helpful']
+        >>> xml = commA.xml()
         >>> 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
+        >>> commB.from_xml(xml, verbose=True)
+        >>> commB.explicit_attrs
+        ['author', 'date', 'content_type', 'body', 'alt_id']
+        >>> commB.xml() == xml
+        False
+        >>> commB.uuid = commB.alt_id
+        >>> commB.alt_id = None
+        >>> commB.xml() == xml
+        True
         """
         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 <comment>")
-        tags=['uuid','alt-id','in-reply-to','author','date','content-type','body']
+            xml_string = xml_string.strip().encode('unicode_escape')
+        if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
+            comment = xml_string
+        else:
+            comment = ElementTree.XML(xml_string)
+        if comment.tag != 'comment':
+            raise utility.InvalidXML( \
+                'comment', comment, 'root element must be <comment>')
+        tags=['uuid','alt-id','in-reply-to','author','date','content-type',
+              'body','extra-string']
+        self.explicit_attrs = []
         uuid = None
         body = None
+        estrs = []
         for child in comment.getchildren():
-            if child.tag == "short-name":
+            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":
+                    text = text.decode('unicode_escape').strip()
+                if child.tag == 'uuid':
                     uuid = text
-                    continue # don't set the bug's uuid tag.
-                if child.tag == "body":
+                    continue # don't set the comment's uuid tag.
+                elif child.tag == 'body':
                     body = text
-                    continue # don't set the bug's body yet.
-                else:
-                    attr_name = child.tag.replace('-','_')
+                    self.explicit_attrs.append(child.tag)
+                    continue # don't set the comment's body yet.
+                elif child.tag == 'extra-string':
+                    estrs.append(text)
+                    continue # don't set the comment's extra_string yet.
+                attr_name = child.tag.replace('-','_')
+                self.explicit_attrs.append(attr_name)
                 setattr(self, attr_name, text)
             elif verbose == True:
-                print >> sys.stderr, "Ignoring unknown tag %s in %s" \
+                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]:
+        if uuid != self.uuid and self.alt_id == None:
+            self.explicit_attrs.append('alt_id')
             self.alt_id = uuid
         if body != None:
-            if self.content_type.startswith("text/"):
-                self.body = body+"\n" # restore trailing newline
+            if self.content_type.startswith('text/'):
+                self.body = body+'\n' # restore trailing newline
             else:
                 self.body = base64.decodestring(body)
+        self.extra_strings = estrs
 
-    def string(self, indent=0, shortname=None):
+    def merge(self, other, accept_changes=True,
+              accept_extra_strings=True, change_exception=False):
+        """
+        Merge info from other into this comment.  Overrides any
+        attributes in self that are listed in other.explicit_attrs.
+
+        >>> commA = Comment(bug=None, body='Some insightful remarks')
+        >>> commA.uuid = '0123'
+        >>> commA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
+        >>> commA.author = 'Frank'
+        >>> commA.extra_strings += ['TAG: very helpful']
+        >>> commA.extra_strings += ['TAG: favorite']
+        >>> commB = Comment(bug=None, body='More insightful remarks')
+        >>> commB.uuid = '3210'
+        >>> commB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
+        >>> commB.author = 'John'
+        >>> commB.explicit_attrs = ['author', 'body']
+        >>> commB.extra_strings += ['TAG: very helpful']
+        >>> commB.extra_strings += ['TAG: useful']
+        >>> commA.merge(commB, accept_changes=False,
+        ...             accept_extra_strings=False, change_exception=False)
+        >>> commA.merge(commB, accept_changes=False,
+        ...             accept_extra_strings=False, change_exception=True)
+        Traceback (most recent call last):
+          ...
+        ValueError: Merge would change author "Frank"->"John" for comment 0123
+        >>> commA.merge(commB, accept_changes=True,
+        ...             accept_extra_strings=False, change_exception=True)
+        Traceback (most recent call last):
+          ...
+        ValueError: Merge would add extra string "TAG: useful" to comment 0123
+        >>> print commA.author
+        John
+        >>> print commA.extra_strings
+        ['TAG: favorite', 'TAG: very helpful']
+        >>> commA.merge(commB, accept_changes=True,
+        ...             accept_extra_strings=True, change_exception=True)
+        >>> print commA.extra_strings
+        ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
+        >>> print commA.xml()
+        <comment>
+          <uuid>0123</uuid>
+          <short-name>//012</short-name>
+          <author>John</author>
+          <date>Thu, 01 Jan 1970 00:00:00 +0000</date>
+          <content-type>text/plain</content-type>
+          <body>More insightful remarks</body>
+          <extra-string>TAG: favorite</extra-string>
+          <extra-string>TAG: useful</extra-string>
+          <extra-string>TAG: very helpful</extra-string>
+        </comment>
+        """
+        for attr in other.explicit_attrs:
+            old = getattr(self, attr)
+            new = getattr(other, attr)
+            if old != new:
+                if accept_changes == True:
+                    setattr(self, attr, new)
+                elif change_exception == True:
+                    raise ValueError, \
+                        'Merge would change %s "%s"->"%s" for comment %s' \
+                        % (attr, old, new, self.uuid)
+        if self.alt_id == self.uuid:
+            self.alt_id = None
+        for estr in other.extra_strings:
+            if not estr in self.extra_strings:
+                if accept_extra_strings == True:
+                    self.extra_strings.append(estr)
+                elif change_exception == True:
+                    raise ValueError, \
+                        'Merge would add extra string "%s" to comment %s' \
+                        % (estr, self.uuid)
+
+    def string(self, indent=0):
         """
         >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
+        >>> comm.uuid = 'abcdef'
         >>> comm.date = "Thu, 01 Jan 1970 00:00:00 +0000"
-        >>> print comm.string(indent=2, shortname="com-1")
+        >>> print comm.string(indent=2)
           --------- Comment ---------
-          Name: com-1
+          Name: //abc
           From: 
           Date: Thu, 01 Jan 1970 00:00:00 +0000
         <BLANKLINE>
@@ -428,42 +498,34 @@ class Comment(Tree, settings_object.SavedSettingsObject):
           insightful
           remarks
         """
-        if shortname == None:
-            shortname = self.uuid
         lines = []
         lines.append("--------- Comment ---------")
-        lines.append("Name: %s" % shortname)
+        lines.append("Name: %s" % self.id.user())
         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())
+            body = (self.body or "")
+            if self.bug != None and self.bug.bugdir != None:
+                body = libbe.util.id.long_to_short_text([self.bug.bugdir], body)
+            lines.extend(body.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):
+    def string_thread(self, string_method_name="string",
+                      indent=0, flatten=True):
         """
         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")
@@ -479,132 +541,123 @@ class Comment(Tree, settings_object.SavedSettingsObject):
         >>> a.sort(key=lambda comm : comm.time)
         >>> print a.string_thread(flatten=True)
         --------- Comment ---------
-        Name: a
+        Name: //a
         From: 
         Date: Thu, 20 Nov 2008 01:00:00 +0000
         <BLANKLINE>
         Insightful remarks
           --------- Comment ---------
-          Name: b
+          Name: //b
           From: 
           Date: Thu, 20 Nov 2008 02:00:00 +0000
         <BLANKLINE>
           Critique original comment
           --------- Comment ---------
-          Name: c
+          Name: //c
           From: 
           Date: Thu, 20 Nov 2008 03:00:00 +0000
         <BLANKLINE>
           Begin flamewar :p
         --------- Comment ---------
-        Name: d
+        Name: //d
         From: 
         Date: Thu, 20 Nov 2008 04:00:00 +0000
         <BLANKLINE>
         Useful examples
-        >>> print a.string_thread(auto_name_map=True, bug_shortname="bug-1")
+        >>> print a.string_thread()
         --------- Comment ---------
-        Name: bug-1:1
+        Name: //a
         From: 
         Date: Thu, 20 Nov 2008 01:00:00 +0000
         <BLANKLINE>
         Insightful remarks
           --------- Comment ---------
-          Name: bug-1:2
+          Name: //b
           From: 
           Date: Thu, 20 Nov 2008 02:00:00 +0000
         <BLANKLINE>
           Critique original comment
           --------- Comment ---------
-          Name: bug-1:3
+          Name: //c
           From: 
           Date: Thu, 20 Nov 2008 03:00:00 +0000
         <BLANKLINE>
           Begin flamewar :p
         --------- Comment ---------
-        Name: bug-1:4
+        Name: //d
         From: 
         Date: Thu, 20 Nov 2008 04:00:00 +0000
         <BLANKLINE>
         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))
+            stringlist.append(string_fn(indent=ind))
         return '\n'.join(stringlist)
 
-    def xml_thread(self, name_map={}, indent=0,
-                   auto_name_map=False, bug_shortname=None):
-        return self.string_thread(string_method_name="xml", name_map=name_map,
-                                  indent=indent, auto_name_map=auto_name_map,
-                                  bug_shortname=bug_shortname)
+    def xml_thread(self, indent=0):
+        return self.string_thread(string_method_name="xml", indent=indent)
 
     # 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 load_settings(self, settings_mapfile=None):
+        if self.uuid == INVALID_UUID:
+            return
+        if settings_mapfile == None:
+            settings_mapfile = \
+                self.storage.get(self.id.storage("values"), default="\n")
+        try:
+            settings = mapfile.parse(settings_mapfile)
+        except mapfile.InvalidMapfileContents, e:
+            raise Exception('Invalid settings file for comment %s\n'
+                            '(BE version missmatch?)' % self.id.user())
+        self._setup_saved_settings(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())
+        if self.uuid == INVALID_UUID:
+            return
+        mf = mapfile.generate(self._get_saved_settings())
+        self.storage.set(self.id.storage("values"), mf)
 
     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).
+        Save any loaded contents to storage.
+
+        However, if ``self.storage.is_writeable() == True``, then any
+        changes are automatically written to storage as soon as they
+        happen, so calling this method will just waste time (unless
+        something else has been messing with your stored files).
         """
-        sync_with_disk = self.sync_with_disk
-        if sync_with_disk == False:
-            self.set_sync_with_disk(True)
+        if self.uuid == INVALID_UUID:
+            return
+        assert self.storage != None, "Can't save without storage"
         assert self.body != None, "Can't save blank comment"
+        if self.bug != None:
+            parent = self.bug.id.storage()
+        else:
+            parent = None
+        self.storage.add(self.id.storage(), parent=parent, directory=True)
+        self.storage.add(self.id.storage('values'), parent=self.id.storage(),
+                         directory=False)
+        self.storage.add(self.id.storage('body'), parent=self.id.storage(),
+                         directory=False)
         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)
+        for comment in self:
+            comment.remove()
+        if self.uuid != INVALID_UUID:
+            self.storage.recursive_remove(self.id.storage())
 
     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):
+    def new_reply(self, body=None, content_type=None):
         """
         >>> comm = Comment(bug=None, body="Some insightful remarks")
         >>> repA = comm.new_reply("Critique original comment")
@@ -612,64 +665,13 @@ class Comment(Tree, settings_object.SavedSettingsObject):
         >>> 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()
+        reply = Comment(self.bug, body=body, content_type=content_type)
         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, match_alt_id=True):
+        """Use a uuid to look up a comment.
 
-    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"
@@ -677,20 +679,40 @@ class Comment(Tree, settings_object.SavedSettingsObject):
         >>> c.uuid = "c"
         >>> d = a.new_reply()
         >>> d.uuid = "d"
+        >>> d.alt_id = "d-alt"
         >>> comm = a.comment_from_uuid("d")
         >>> id(comm) == id(d)
         True
+        >>> comm = a.comment_from_uuid("d-alt")
+        >>> id(comm) == id(d)
+        True
+        >>> comm = a.comment_from_uuid(None, match_alt_id=False)
+        Traceback (most recent call last):
+          ...
+        KeyError: None
         """
         for comment in self.traverse():
             if comment.uuid == uuid:
                 return comment
+            if match_alt_id == True and uuid != None \
+                    and comment.alt_id == uuid:
+                return comment
         raise KeyError(uuid)
 
+    # methods for id generation
+
+    def sibling_uuids(self):
+        if self.bug != None:
+            return self.bug.uuids()
+        return []
+
+
 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()
@@ -710,7 +732,7 @@ def cmp_attr(comment_1, comment_2, attr, invert=False):
     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 :
@@ -722,12 +744,14 @@ cmp_author = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "autho
 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")
+cmp_extra_strings = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "extra_strings")
 # 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)
+     cmp_uuid, cmp_extra_strings)
 
 class CommentCompoundComparator (object):
     def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
@@ -738,7 +762,8 @@ class CommentCompoundComparator (object):
             if val != 0 :
                 return val
         return 0
-        
+
 cmp_full = CommentCompoundComparator()
 
-suite = doctest.DocTestSuite()
+if libbe.TESTING == True:
+    suite = doctest.DocTestSuite()
diff --git a/libbe/darcs.py b/libbe/darcs.py
deleted file mode 100644 (file)
index 9115886..0000000
+++ /dev/null
@@ -1,186 +0,0 @@
-# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
-#
-# 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_version(self):
-        status,output,error = self._u_invoke_client("--version")
-        num_part = output.split(" ")[0]
-        self.parsed_version = [int(i) for i in num_part.split(".")]
-        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:
-            if self.parsed_version[0] >= 2:
-                status,output,error = self._u_invoke_client( \
-                    "show", "contents", "--patch", revision, path)
-                return output
-            else:
-                # 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
-\f    
-vcs.make_vcs_testcase_subclasses(Darcs, sys.modules[__name__])
-
-unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
-suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
index 9253a23a99e819fd3030ac6f30bf09f21bba9f81..dc13b6115fd92aa47b381df2dd4df532192cb18d 100644 (file)
@@ -1,4 +1,5 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
 #                         W. Trevor King <wking@drexel.edu>
 #
 # This program is free software; you can redistribute it and/or modify
 # 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."""
+"""Tools for comparing two :class:`libbe.bug.BugDir`\s.
+"""
 
 import difflib
-import doctest
+import types
 
-from libbe import bugdir, bug, settings_object, tree
-from libbe.utility import time_to_str
+import libbe
+import libbe.bugdir
+import libbe.bug
+import libbe.util.tree
+from libbe.storage.util.settings_object import setting_name_to_attr_name
+from libbe.util.utility import time_to_str
 
 
-class DiffTree (tree.Tree):
+class SubscriptionType (libbe.util.tree.Tree):
+    """Trees of subscription types to allow users to select exactly what
+    notifications they want to subscribe to.
     """
-    A tree holding difference data for easy report generation.
-    >>> bugdir = DiffTree("bugdir")
-    >>> bdsettings = DiffTree("settings", data="target: None -> 1.0")
+    def __init__(self, type_name, *args, **kwargs):
+        libbe.util.tree.Tree.__init__(self, *args, **kwargs)
+        self.type = type_name
+    def __str__(self):
+        return self.type
+    def __cmp__(self, other):
+        return cmp(self.type, other.type)
+    def __repr__(self):
+        return '<SubscriptionType: %s>' % 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_ID = 'DIR'
+BUGDIR_TYPE_NEW = SubscriptionType('new')
+BUGDIR_TYPE_MOD = SubscriptionType('mod')
+BUGDIR_TYPE_REM = SubscriptionType('rem')
+BUGDIR_TYPE_ALL = SubscriptionType('all',
+                      [BUGDIR_TYPE_NEW, BUGDIR_TYPE_MOD, BUGDIR_TYPE_REM])
+
+# 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 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)
+
+class Subscription (object):
+    """A user subscription.
+
+    Examples
+    --------
+
+    >>> subscriptions = [Subscription('XYZ', 'all'),
+    ...                  Subscription('DIR', 'new'),
+    ...                  Subscription('ABC', BUG_TYPE_ALL),]
+    >>> print sorted(subscriptions)
+    [<Subscription: DIR (new)>, <Subscription: ABC (all)>, <Subscription: XYZ (all)>]
+    """
+    def __init__(self, id, subscription_type, **kwargs):
+        if 'type_root' not in kwargs:
+            if id == BUGDIR_ID:
+                kwargs['type_root'] = BUGDIR_TYPE_ALL
+            else:
+                kwargs['type_root'] = BUG_TYPE_ALL
+        if type(subscription_type) in types.StringTypes:
+            subscription_type = type_from_name(subscription_type, **kwargs)
+        self.id = id
+        self.type = subscription_type
+    def __cmp__(self, other):
+        for attr in 'id', 'type':
+            value = cmp(getattr(self, attr), getattr(other, attr))
+            if value != 0:
+                if self.id == BUGDIR_ID:
+                    return -1
+                elif other.id == BUGDIR_ID:
+                    return 1
+                return value
+    def __str__(self):
+        return str(self.type)
+    def __repr__(self):
+        return '<Subscription: %s (%s)>' % (self.id, self.type)
+
+def subscriptions_from_string(string=None, subscription_sep=',', id_sep=':'):
+    """Provide a simple way for non-Python interfaces to read in subscriptions.
+
+    Examples
+    --------
+
+    >>> subscriptions_from_string(None)
+    [<Subscription: DIR (all)>]
+    >>> subscriptions_from_string('DIR:new,DIR:rem,ABC:all,XYZ:all')
+    [<Subscription: DIR (new)>, <Subscription: DIR (rem)>, <Subscription: ABC (all)>, <Subscription: XYZ (all)>]
+    >>> subscriptions_from_string('DIR::new')
+    Traceback (most recent call last):
+      ...
+    ValueError: Invalid subscription "DIR::new", should be ID:TYPE
+    """
+    if string == None:
+        return [Subscription(BUGDIR_ID, BUGDIR_TYPE_ALL)]
+    subscriptions = []
+    for subscription in string.split(','):
+        fields = subscription.split(':')
+        if len(fields) != 2:
+            raise ValueError('Invalid subscription "%s", should be ID:TYPE'
+                             % subscription)
+        id,type = fields
+        subscriptions.append(Subscription(id, type))
+    return subscriptions
+
+class DiffTree (libbe.util.tree.Tree):
+    """A tree holding difference data for easy report generation.
+
+    Examples
+    --------
+
+    >>> bugdir = DiffTree('bugdir')
+    >>> bdsettings = DiffTree('settings', data='target: None -> 1.0')
     >>> bugdir.append(bdsettings)
-    >>> bugs = DiffTree("bugs", "bug-count: 5 -> 6")
+    >>> bugs = DiffTree('bugs', 'bug-count: 5 -> 6')
     >>> bugdir.append(bugs)
-    >>> new = DiffTree("new", "new bugs: ABC, DEF")
+    >>> new = DiffTree('new', 'new bugs: ABC, DEF')
     >>> bugs.append(new)
-    >>> rem = DiffTree("rem", "removed bugs: RST, UVW")
+    >>> 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())
+    >>> print '\\n'.join(bugdir.paths())
     bugdir
     bugdir/settings
     bugdir/bugs
     bugdir/bugs/new
     bugdir/bugs/rem
-    >>> bugdir.child_by_path("/") == bugdir
+    >>> bugdir.child_by_path('/') == bugdir
     True
-    >>> bugdir.child_by_path("/bugs") == bugs
+    >>> bugdir.child_by_path('/bugs') == bugs
     True
-    >>> bugdir.child_by_path("/bugs/rem") == rem
+    >>> bugdir.child_by_path('/bugs/rem') == rem
     True
-    >>> bugdir.child_by_path("bugdir") == bugdir
+    >>> bugdir.child_by_path('bugdir') == bugdir
     True
-    >>> bugdir.child_by_path("bugdir/") == bugdir
+    >>> bugdir.child_by_path('bugdir/') == bugdir
     True
-    >>> bugdir.child_by_path("bugdir/bugs") == bugs
+    >>> bugdir.child_by_path('bugdir/bugs') == bugs
     True
-    >>> bugdir.child_by_path("/bugs").masked = 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)
+        libbe.util.tree.Tree.__init__(self)
         self.name = name
         self.data = data
         self.data_part_fn = data_part_fn
@@ -76,17 +197,17 @@ class DiffTree (tree.Tree):
         if parent_path == None:
             path = self.name
         else:
-            path = "%s/%s" % (parent_path, self.name)
+            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] == "":
+        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] == "":
+            if len(names) > 1 and names[-1] == '':
                 names = names[:-1] # strip empty tail
         else: # it was already an array
             names = path
@@ -99,23 +220,27 @@ class DiffTree (tree.Tree):
                     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])
+        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())
+        report = self.report()
+        if report == None:
+            return ''
+        return '\n'.join(report)
     def report(self, root=None, parent=None, depth=0):
         if root == None:
             root = self.make_root()
         if self.masked == True:
-            return None
+            return root
         data_part = self.data_part(depth)
-        if self.requires_children == True and len(self) == 0:
+        if self.requires_children == True \
+                and len([c for c in self if c.masked == False]) == 0:
             pass
         else:
             self.join(root, parent, data_part)
             if data_part != None:
                 depth += 1
-        for child in self:
-            child.report(root, self, depth)
+            for child in self:
+                root = child.report(root, self, depth)
         return root
     def make_root(self):
         return []
@@ -125,36 +250,39 @@ class DiffTree (tree.Tree):
     def data_part(self, depth, indent=True):
         if self.data == None:
             return None
-        if hasattr(self, "_cached_data_part"):
+        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
+            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.
+    """Difference tree generator for BugDirs.
+
+    Examples
+    --------
+
     >>> import copy
-    >>> bd = bugdir.SimpleBugDir(sync_with_disk=False)
-    >>> bd.user_id = "John Doe <j@doe.com>"
+    >>> bd = libbe.bugdir.SimpleBugDir(memory=True)
     >>> bd_new = copy.deepcopy(bd)
-    >>> bd_new.target = "1.0"
-    >>> a = bd_new.bug_from_uuid("a")
+    >>> 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")
+    >>> rep.uuid = 'acom'
+    >>> rep.author = 'John Doe <j@doe.com>'
+    >>> 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")
+    >>> c = bd_new.new_bug('Bug C', _uuid='c')
     >>> d = Diff(bd, bd_new)
     >>> r = d.report_tree()
-    >>> print "\\n".join(r.paths())
+    >>> print '\\n'.join(r.paths())
     bugdir
     bugdir/settings
     bugdir/bugs
@@ -174,16 +302,43 @@ class Diff (object):
     Changed bug directory settings:
       target: None -> 1.0
     New bugs:
-      c:om: Bug C
+      abc/c:om: Bug C
     Removed bugs:
-      b:cm: Bug B
+      abc/b:cm: Bug B
     Modified bugs:
-      a:cm: Bug A
+      abc/a:cm: Bug A
         Changed bug settings:
           status: open -> closed
         New comments:
           from John Doe <j@doe.com> on Thu, 01 Jan 1970 00:00:00 +0000
             I'm closing this bug...
+
+    You can also limit the report generation by providing a list of
+    subscriptions.
+
+    >>> subscriptions = [Subscription('DIR', BUGDIR_TYPE_NEW),
+    ...                  Subscription('b', BUG_TYPE_ALL)]
+    >>> r = d.report_tree(subscriptions)
+    >>> print r.report_string()
+    New bugs:
+      abc/c:om: Bug C
+    Removed bugs:
+      abc/b:cm: Bug B
+
+    While sending subscriptions to report_tree() makes the report
+    generation more efficient (because you may not need to compare
+    _all_ the bugs, etc.), sometimes you will have several sets of
+    subscriptions.  In that case, it's better to run full_report()
+    first, and then use report_tree() to avoid redundant comparisons.
+
+    >>> d.full_report()
+    >>> print d.report_tree([subscriptions[0]]).report_string()
+    New bugs:
+      abc/c:om: Bug C
+    >>> print d.report_tree([subscriptions[1]]).report_string()
+    Removed bugs:
+      abc/b:cm: Bug B
+
     >>> bd.cleanup()
     """
     def __init__(self, old_bugdir, new_bugdir):
@@ -192,7 +347,7 @@ class Diff (object):
 
     # data assembly methods
 
-    def _changed_bugs(self):
+    def _changed_bugs(self, subscriptions):
         """
         Search for differences in all bugs between .old_bugdir and
         .new_bugdir.  Returns
@@ -201,33 +356,97 @@ class Diff (object):
         removed bugs respectively.  modified_bugs is a list of
         (old_bug,new_bug) pairs.
         """
-        if hasattr(self, "__changed_bugs"):
-            return self.__changed_bugs
+        bugdir_types = [s.type for s in subscriptions if s.id == BUGDIR_ID]
+        new_uuids = []
+        old_uuids = []
+        for bd_type in [BUGDIR_TYPE_ALL, BUGDIR_TYPE_NEW, BUGDIR_TYPE_MOD]:
+            if bd_type in bugdir_types:
+                new_uuids = list(self.new_bugdir.uuids())
+                break
+        for bd_type in [BUGDIR_TYPE_ALL, BUGDIR_TYPE_REM]:
+            if bd_type in bugdir_types:
+                old_uuids = list(self.old_bugdir.uuids())
+                break
+        subscribed_bugs = []
+        for s in subscriptions:
+            if s.id != BUGDIR_ID:
+                try:
+                    bug = self.new_bugdir.bug_from_uuid(s.id)
+                except libbe.bugdir.NoBugMatches:
+                    bug = self.old_bugdir.bug_from_uuid(s.id)
+                subscribed_bugs.append(bug.uuid)
+        new_uuids.extend([s for s in subscribed_bugs
+                          if self.new_bugdir.has_bug(s)])
+        new_uuids = sorted(set(new_uuids))
+        old_uuids.extend([s for s in subscribed_bugs
+                          if self.old_bugdir.has_bug(s)])
+        old_uuids = sorted(set(old_uuids))
+
         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)
+        if hasattr(self.old_bugdir, 'changed'):
+            # take advantage of a RevisionedBugDir-style changed() method
+            new_ids,mod_ids,rem_ids = self.old_bugdir.changed()
+            for id in new_ids:
+                for a_id in self.new_bugdir.storage.ancestors(id):
+                    if a_id.count('/') == 0:
+                        if a_id in [b.id.storage() for b in added]:
+                            break
+                        try:
+                            bug = self.new_bugdir.bug_from_uuid(a_id)
+                            added.append(bug)
+                        except libbe.bugdir.NoBugMatches:
+                            pass
+            for id in rem_ids:
+                for a_id in self.old_bugdir.storage.ancestors(id):
+                    if a_id.count('/') == 0:
+                        if a_id in [b.id.storage() for b in removed]:
+                            break
+                        try:
+                            bug = self.old_bugdir.bug_from_uuid(a_id)
+                            removed.append(bug)
+                        except libbe.bugdir.NoBugMatches:
+                            pass
+            for id in mod_ids:
+                for a_id in self.new_bugdir.storage.ancestors(id):
+                    if a_id.count('/') == 0:
+                        if a_id in [b[0].id.storage() for b in modified]:
+                            break
+                        try:
+                            new_bug = self.new_bugdir.bug_from_uuid(a_id)
+                            old_bug = self.old_bugdir.bug_from_uuid(a_id)
+                            modified.append((old_bug, new_bug))
+                        except libbe.bugdir.NoBugMatches:
+                            pass
+        else:
+            for uuid in new_uuids:
+                new_bug = self.new_bugdir.bug_from_uuid(uuid)
+                try:
+                    old_bug = self.old_bugdir.bug_from_uuid(uuid)
+                except KeyError:
+                    if BUGDIR_TYPE_ALL in bugdir_types \
+                            or BUGDIR_TYPE_NEW in bugdir_types \
+                            or uuid in subscribed_bugs:
+                        added.append(new_bug)
+                    continue
+                if BUGDIR_TYPE_ALL in bugdir_types \
+                        or BUGDIR_TYPE_MOD in bugdir_types \
+                        or uuid in subscribed_bugs:
+                    if old_bug.storage != None and old_bug.storage.is_readable():
+                        old_bug.load_comments()
+                    if new_bug.storage != None and new_bug.storage.is_readable():
+                        new_bug.load_comments()
+                    if old_bug != new_bug:
+                        modified.append((old_bug, new_bug))
+            for uuid in old_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
+        return (added, modified, removed)
     def _bug_modified_cmp(self, left, right):
         return cmp(left[1], right[1])
     def _changed_comments(self, old, new):
@@ -237,7 +456,7 @@ class Diff (object):
           (added_comments, modified_comments, removed_comments)
         analogous to ._changed_bugs.
         """
-        if hasattr(self, "__changed_comments"):
+        if hasattr(self, '__changed_comments'):
             if new.uuid in self.__changed_comments:
                 return self.__changed_comments[new.uuid]
         else:
@@ -260,8 +479,8 @@ class Diff (object):
                     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)
+                old_comment = old.comment_from_uuid(uuid)
+                removed.append(old_comment)
         self.__changed_comments[new.uuid] = (added, modified, removed)
         return self.__changed_comments[new.uuid]
     def _attribute_changes(self, old, new, attributes):
@@ -285,13 +504,12 @@ class Diff (object):
         properties = sorted(new.settings_properties)
         for p in hidden_properties:
             properties.remove(p)
-        attributes = [settings_object.setting_name_to_attr_name(None, p)
+        attributes = [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
+            self.old_bugdir, self.new_bugdir)
     def _bug_attribute_changes(self, old, new):
         return self._settings_properties_attribute_changes(old, new)
     def _comment_attribute_changes(self, old, new):
@@ -299,94 +517,148 @@ class Diff (object):
 
     # report generation methods
 
-    def report_tree(self, diff_tree=DiffTree):
+    def full_report(self, diff_tree=DiffTree):
+        """
+        Generate a full report for efficiency if you'll be using
+        .report_tree() with several sets of subscriptions.
+        """
+        self._cached_full_report = self.report_tree(diff_tree=diff_tree,
+                                                    allow_cached=False)
+        self._cached_full_report_diff_tree = diff_tree
+    def _sub_report(self, subscriptions):
+        """
+        Return ._cached_full_report masked for subscriptions.
+        """
+        root = self._cached_full_report
+        bugdir_types = [s.type for s in subscriptions if s.id == BUGDIR_ID]
+        subscribed_bugs = [s.id for s in subscriptions
+                           if BUG_TYPE_ALL.has_descendant( \
+                                     s.type, match_self=True)]
+        selected_by_bug = [node.name
+                           for node in root.child_by_path('bugdir/bugs')]
+        if BUGDIR_TYPE_ALL in bugdir_types:
+            for node in root.traverse():
+                node.masked = False
+            selected_by_bug = []
+        else:
+            try:
+                node = root.child_by_path('bugdir/settings')
+                node.masked = True
+            except KeyError:
+                pass
+        for name,type in (('new', BUGDIR_TYPE_NEW),
+                          ('mod', BUGDIR_TYPE_MOD),
+                          ('rem', BUGDIR_TYPE_REM)):
+            if type in bugdir_types:
+                bugs = root.child_by_path('bugdir/bugs/%s' % name)
+                for bug_node in bugs:
+                    for node in bug_node.traverse():
+                        node.masked = False
+                selected_by_bug.remove(name)
+        for name in selected_by_bug:
+            bugs = root.child_by_path('bugdir/bugs/%s' % name)
+            for bug_node in bugs:
+                if bug_node.name in subscribed_bugs:
+                    for node in bug_node.traverse():
+                        node.masked = False
+                else:
+                    for node in bug_node.traverse():
+                        node.masked = True
+        return root
+    def report_tree(self, subscriptions=None, diff_tree=DiffTree,
+                    allow_cached=True):
         """
         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
+        if allow_cached == True \
+                and hasattr(self, '_cached_full_report') \
+                and diff_tree == self._cached_full_report_diff_tree:
+            return self._sub_report(subscriptions)
+        if subscriptions == None:
+            subscriptions = [Subscription(BUGDIR_ID, BUGDIR_TYPE_ALL)]
         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 = diff_tree('bugdir')
+        bugdir_subscriptions = [s.type for s in subscriptions
+                                if s.id == BUGDIR_ID]
+        if BUGDIR_TYPE_ALL in bugdir_subscriptions:
+            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)
+        add,mod,rem = self._changed_bugs(subscriptions)
+        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)
+        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)
+        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,
+                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)
+                bsum = diff_tree('summary', data, self.bug_summary_change_string)
                 b.append(bsum)
-            cr = diff_tree("comments")
+            cr = diff_tree('comments')
             b.append(cr)
             a,m,d = self._changed_comments(old, new)
-            cnew = diff_tree("new", "New comments:", requires_children=True)
+            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)
+            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)
+            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,
+                    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,
+                    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
+        return root
 
     # 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]
+        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)
+        return u'\n'.join(change_strings)
     def bugdir_attribute_change_string(self, attribute_changes):
-        return "Changed bug directory settings:\n%s" % \
+        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" % \
+        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" % \
+        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)
@@ -397,23 +669,23 @@ class Diff (object):
         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)
+        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))
+        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)
+        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)
+        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()
+        return ''.join(difflib.unified_diff(
+                old_body.splitlines(True),
+                new_body.splitlines(True),
+                'before', 'after'))
diff --git a/libbe/error.py b/libbe/error.py
new file mode 100644 (file)
index 0000000..798136e
--- /dev/null
@@ -0,0 +1,26 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+"""
+General error classes for Bugs-Everywhere.
+"""
+
+class NotSupported (NotImplementedError):
+    def __init__(self, action, message):
+        msg = '%s not supported: %s' % (action, message)
+        NotImplementedError.__init__(self, msg)
+        self.action = action
+        self.message = message
diff --git a/libbe/git.py b/libbe/git.py
deleted file mode 100644 (file)
index 628f9b9..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-# Copyright (C) 2008-2009 Ben Finney <ben+python@benfinney.id.au>
-#                         Chris Ball <cjb@laptop.org>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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_version(self):
-        status,output,error = self._u_invoke_client("--version")
-        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
-
-\f    
-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
deleted file mode 100644 (file)
index 7cd4c2f..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-# Copyright (C) 2007-2009 Aaron Bentley and Panometrics, Inc.
-#                         Ben Finney <ben+python@benfinney.id.au>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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_version(self):
-        status,output,error = self._u_invoke_client("--version")
-        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
-
-\f    
-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/settings_object.py b/libbe/settings_object.py
deleted file mode 100644 (file)
index ceea9d5..0000000
+++ /dev/null
@@ -1,412 +0,0 @@
-# Bugs Everywhere - a distributed bugtracker
-# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
-#
-# 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 == [
-                "'<class 'libbe.settings_object.EMPTY'>' -> '[]'"
-                ], SAVES)
-        t.list_type.append(5)
-        self.failUnless(SAVES == [
-                "'<class 'libbe.settings_object.EMPTY'>' -> '[]'",
-                ], SAVES)
-        self.failUnless(t.settings["List-type"] == [5],t.settings["List-type"])
-        self.failUnless(SAVES == [ # the append(5) has not yet been saved
-                "'<class 'libbe.settings_object.EMPTY'>' -> '[]'",
-                ], SAVES)
-        self.failUnless(t.list_type == [5], t.list_type) # <-get triggers saved
-
-        self.failUnless(SAVES == [ # now the append(5) has been saved.
-                "'<class 'libbe.settings_object.EMPTY'>' -> '[]'",
-                "'[]' -> '[5]'"
-                ], SAVES)
-
-unitsuite=unittest.TestLoader().loadTestsFromTestCase(SavedSettingsObjectTests)
-suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
diff --git a/libbe/storage/__init__.py b/libbe/storage/__init__.py
new file mode 100644 (file)
index 0000000..6bceac9
--- /dev/null
@@ -0,0 +1,74 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# 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 :class:`~libbe.storage.base.Storage` and
+:class:`~libbe.storage.base.VersionedStorage` classes for storing BE
+data.
+
+Also define assorted implementations for the Storage classes:
+
+* :mod:`libbe.storage.vcs`
+* :mod:`libbe.storage.http`
+
+Also define an assortment of storage-related tools and utilities:
+
+* :mod:`libbe.storage.util`
+"""
+
+import base
+
+ConnectionError = base.ConnectionError
+InvalidStorageVersion = base.InvalidStorageVersion
+InvalidID = base.InvalidID
+InvalidRevision = base.InvalidRevision
+InvalidDirectory = base.InvalidDirectory
+NotWriteable = base.NotWriteable
+NotReadable = base.NotReadable
+EmptyCommit = base.EmptyCommit
+
+# a list of all past versions
+STORAGE_VERSIONS = ['Bugs Everywhere Tree 1 0',
+                    'Bugs Everywhere Directory v1.1',
+                    'Bugs Everywhere Directory v1.2',
+                    'Bugs Everywhere Directory v1.3',
+                    'Bugs Everywhere Directory v1.4',
+                    ]
+
+# the current version
+STORAGE_VERSION = STORAGE_VERSIONS[-1]
+
+def get_http_storage(location):
+    import http
+    return http.HTTP(location)
+
+def get_vcs_storage(location):
+    import vcs
+    s = vcs.detect_vcs(location)
+    s.repo = location
+    return s
+
+def get_storage(location):
+    """
+    Return a Storage instance from a repo location string.
+    """
+    if location.startswith('http://') or location.startswith('https://'):
+        return get_http_storage(location)
+    return get_vcs_storage(location)
+
+__all__ = [ConnectionError, InvalidStorageVersion, InvalidID,
+           InvalidRevision, InvalidDirectory, NotWriteable, NotReadable,
+           EmptyCommit, STORAGE_VERSIONS, STORAGE_VERSION,
+           get_storage]
diff --git a/libbe/storage/base.py b/libbe/storage/base.py
new file mode 100644 (file)
index 0000000..0ae9c53
--- /dev/null
@@ -0,0 +1,1070 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+"""
+Abstract bug repository data storage to easily support multiple backends.
+"""
+
+import copy
+import os
+import pickle
+import types
+
+from libbe.error import NotSupported
+import libbe.storage
+from libbe.util.tree import Tree
+from libbe.util import InvalidObject
+import libbe.version
+from libbe import TESTING
+
+if TESTING == True:
+    import doctest
+    import os.path
+    import sys
+    import unittest
+
+    from libbe.util.utility import Dir
+
+class ConnectionError (Exception):
+    pass
+
+class InvalidStorageVersion(ConnectionError):
+    def __init__(self, active_version, expected_version=None):
+        if expected_version == None:
+            expected_version = libbe.storage.STORAGE_VERSION
+        msg = 'Storage in "%s" not the expected "%s"' \
+            % (active_version, expected_version)
+        Exception.__init__(self, msg)
+        self.active_version = active_version
+        self.expected_version = expected_version
+
+class InvalidID (KeyError):
+    def __init__(self, id=None, revision=None, msg=None):
+        KeyError.__init__(self, id)
+        self.msg = msg
+        self.id = id
+        self.revision = revision
+    def __str__(self):
+        if self.msg == None:
+            return '%s in revision %s' % (self.id, self.revision)
+        return self.msg
+
+
+class InvalidRevision (KeyError):
+    pass
+
+class InvalidDirectory (Exception):
+    pass
+
+class DirectoryNotEmpty (InvalidDirectory):
+    pass
+
+class NotWriteable (NotSupported):
+    def __init__(self, msg):
+        NotSupported.__init__(self, 'write', msg)
+
+class NotReadable (NotSupported):
+    def __init__(self, msg):
+        NotSupported.__init__(self, 'read', msg)
+
+class EmptyCommit(Exception):
+    def __init__(self):
+        Exception.__init__(self, 'No changes to commit')
+
+class _EMPTY (object):
+    """Entry has been added but has no user-set value."""
+    pass
+
+class Entry (Tree):
+    def __init__(self, id, value=_EMPTY, parent=None, directory=False,
+                 children=None):
+        if children == None:
+            Tree.__init__(self)
+        else:
+            Tree.__init__(self, children)
+        self.id = id
+        self.value = value
+        self.parent = parent
+        if self.parent != None:
+            if self.parent.directory == False:
+                raise InvalidDirectory(
+                    'Non-directory %s cannot have children' % self.parent)
+            parent.append(self)
+        self.directory = directory
+
+    def __str__(self):
+        return '<Entry %s: %s>' % (self.id, self.value)
+
+    def __repr__(self):
+        return str(self)
+
+    def __cmp__(self, other, local=False):
+        if other == None:
+            return cmp(1, None)
+        if cmp(self.id, other.id) != 0:
+            return cmp(self.id, other.id)
+        if cmp(self.value, other.value) != 0:
+            return cmp(self.value, other.value)
+        if local == False:
+            if self.parent == None:
+                if cmp(self.parent, other.parent) != 0:
+                    return cmp(self.parent, other.parent)
+            elif self.parent.__cmp__(other.parent, local=True) != 0:
+                return self.parent.__cmp__(other.parent, local=True)
+            for sc,oc in zip(self, other):
+                if sc.__cmp__(oc, local=True) != 0:
+                    return sc.__cmp__(oc, local=True)
+        return 0
+
+    def _objects_to_ids(self):
+        if self.parent != None:
+            self.parent = self.parent.id
+        for i,c in enumerate(self):
+            self[i] = c.id
+        return self
+
+    def _ids_to_objects(self, dict):
+        if self.parent != None:
+            self.parent = dict[self.parent]
+        for i,c in enumerate(self):
+            self[i] = dict[c]
+        return self
+
+class Storage (object):
+    """
+    This class declares all the methods required by a Storage
+    interface.  This implementation just keeps the data in a
+    dictionary and uses pickle for persistent storage.
+    """
+    name = 'Storage'
+
+    def __init__(self, repo='/', encoding='utf-8', options=None):
+        self.repo = repo
+        self.encoding = encoding
+        self.options = options
+        self.readable = True  # soft limit (user choice)
+        self._readable = True # hard limit (backend choice)
+        self.writeable = True  # soft limit (user choice)
+        self._writeable = True # hard limit (backend choice)
+        self.versioned = False
+        self.can_init = True
+        self.connected = False
+
+    def __str__(self):
+        return '<%s %s %s>' % (self.__class__.__name__, id(self), self.repo)
+
+    def __repr__(self):
+        return str(self)
+
+    def version(self):
+        """Return a version string for this backend."""
+        return libbe.version.version()
+
+    def storage_version(self, revision=None):
+        """Return the storage format for this backend."""
+        return libbe.storage.STORAGE_VERSION
+
+    def is_readable(self):
+        return self.readable and self._readable
+
+    def is_writeable(self):
+        return self.writeable and self._writeable
+
+    def init(self):
+        """Create a new storage repository."""
+        if self.can_init == False:
+            raise NotSupported('init',
+                               'Cannot initialize this repository format.')
+        if self.is_writeable() == False:
+            raise NotWriteable('Cannot initialize unwriteable storage.')
+        return self._init()
+
+    def _init(self):
+        f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
+        root = Entry(id='__ROOT__', directory=True)
+        d = {root.id:root}
+        pickle.dump(dict((k,v._objects_to_ids()) for k,v in d.items()), f, -1)
+        f.close()
+
+    def destroy(self):
+        """Remove the storage repository."""
+        if self.is_writeable() == False:
+            raise NotWriteable('Cannot destroy unwriteable storage.')
+        return self._destroy()
+
+    def _destroy(self):
+        os.remove(os.path.join(self.repo, 'repo.pkl'))
+
+    def connect(self):
+        """Open a connection to the repository."""
+        if self.is_readable() == False:
+            raise NotReadable('Cannot connect to unreadable storage.')
+        self._connect()
+        self.connected = True
+
+    def _connect(self):
+        try:
+            f = open(os.path.join(self.repo, 'repo.pkl'), 'rb')
+        except IOError:
+            raise ConnectionError(self)
+        d = pickle.load(f)
+        self._data = dict((k,v._ids_to_objects(d)) for k,v in d.items())
+        f.close()
+
+    def disconnect(self):
+        """Close the connection to the repository."""
+        if self.is_writeable() == False:
+            return
+        if self.connected == False:
+            return
+        self._disconnect()
+        self.connected = False
+
+    def _disconnect(self):
+        f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
+        pickle.dump(dict((k,v._objects_to_ids())
+                         for k,v in self._data.items()), f, -1)
+        f.close()
+        self._data = None
+
+    def add(self, id, *args, **kwargs):
+        """Add an entry"""
+        if self.is_writeable() == False:
+            raise NotWriteable('Cannot add entry to unwriteable storage.')
+        if not self.exists(id):
+            self._add(id, *args, **kwargs)
+
+    def _add(self, id, parent=None, directory=False):
+        if parent == None:
+            parent = '__ROOT__'
+        p = self._data[parent]
+        self._data[id] = Entry(id, parent=p, directory=directory)
+
+    def exists(self, *args, **kwargs):
+        """Check an entry's existence"""
+        if self.is_readable() == False:
+            raise NotReadable('Cannot check entry existence in unreadable storage.')
+        return self._exists(*args, **kwargs)
+
+    def _exists(self, id, revision=None):
+        return id in self._data
+
+    def remove(self, *args, **kwargs):
+        """Remove an entry."""
+        if self.is_writeable() == False:
+            raise NotSupported('write',
+                               'Cannot remove entry from unwriteable storage.')
+        self._remove(*args, **kwargs)
+
+    def _remove(self, id):
+        if self._data[id].directory == True \
+                and len(self.children(id)) > 0:
+            raise DirectoryNotEmpty(id)
+        e = self._data.pop(id)
+        e.parent.remove(e)
+
+    def recursive_remove(self, *args, **kwargs):
+        """Remove an entry and all its decendents."""
+        if self.is_writeable() == False:
+            raise NotSupported('write',
+                               'Cannot remove entries from unwriteable storage.')
+        self._recursive_remove(*args, **kwargs)
+
+    def _recursive_remove(self, id):
+        for entry in reversed(list(self._data[id].traverse())):
+            self._remove(entry.id)
+
+    def ancestors(self, *args, **kwargs):
+        """Return a list of the specified entry's ancestors' ids."""
+        if self.is_readable() == False:
+            raise NotReadable('Cannot list parents with unreadable storage.')
+        return self._ancestors(*args, **kwargs)
+
+    def _ancestors(self, id=None, revision=None):
+        if id == None:
+            return []
+        ancestors = []
+        stack = [id]
+        while len(stack) > 0:
+            id = stack.pop(0)
+            parent = self._data[id].parent
+            if parent != None and not parent.id.startswith('__'):
+                ancestor = parent.id
+                ancestors.append(ancestor)
+                stack.append(ancestor)
+        return ancestors
+
+    def children(self, *args, **kwargs):
+        """Return a list of specified entry's children's ids."""
+        if self.is_readable() == False:
+            raise NotReadable('Cannot list children with unreadable storage.')
+        return self._children(*args, **kwargs)
+
+    def _children(self, id=None, revision=None):
+        if id == None:
+            id = '__ROOT__'
+        return [c.id for c in self._data[id] if not c.id.startswith('__')]
+
+    def get(self, *args, **kwargs):
+        """
+        Get contents of and entry as they were in a given revision.
+        revision==None specifies the current revision.
+
+        If there is no id, return default, unless default is not
+        given, in which case raise InvalidID.
+        """
+        if self.is_readable() == False:
+            raise NotReadable('Cannot get entry with unreadable storage.')
+        if 'decode' in kwargs:
+            decode = kwargs.pop('decode')
+        else:
+            decode = False
+        value = self._get(*args, **kwargs)
+        if value != None:
+            if decode == True and type(value) != types.UnicodeType:
+                return unicode(value, self.encoding)
+            elif decode == False and type(value) != types.StringType:
+                return value.encode(self.encoding)
+        return value
+
+    def _get(self, id, default=InvalidObject, revision=None):
+        if id in self._data and self._data[id].value != _EMPTY:
+            return self._data[id].value
+        elif default == InvalidObject:
+            raise InvalidID(id)
+        return default
+
+    def set(self, id, value, *args, **kwargs):
+        """
+        Set the entry contents.
+        """
+        if self.is_writeable() == False:
+            raise NotWriteable('Cannot set entry in unwriteable storage.')
+        if type(value) == types.UnicodeType:
+            value = value.encode(self.encoding)
+        self._set(id, value, *args, **kwargs)
+
+    def _set(self, id, value):
+        if id not in self._data:
+            raise InvalidID(id)
+        if self._data[id].directory == True:
+            raise InvalidDirectory(
+                'Directory %s cannot have data' % self.parent)
+        self._data[id].value = value
+
+class VersionedStorage (Storage):
+    """
+    This class declares all the methods required by a Storage
+    interface that supports versioning.  This implementation just
+    keeps the data in a list and uses pickle for persistent
+    storage.
+    """
+    name = 'VersionedStorage'
+
+    def __init__(self, *args, **kwargs):
+        Storage.__init__(self, *args, **kwargs)
+        self.versioned = True
+
+    def _init(self):
+        f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
+        root = Entry(id='__ROOT__', directory=True)
+        summary = Entry(id='__COMMIT__SUMMARY__', value='Initial commit')
+        body = Entry(id='__COMMIT__BODY__')
+        initial_commit = {root.id:root, summary.id:summary, body.id:body}
+        d = dict((k,v._objects_to_ids()) for k,v in initial_commit.items())
+        pickle.dump([d, copy.deepcopy(d)], f, -1) # [inital tree, working tree]
+        f.close()
+
+    def _connect(self):
+        try:
+            f = open(os.path.join(self.repo, 'repo.pkl'), 'rb')
+        except IOError:
+            raise ConnectionError(self)
+        d = pickle.load(f)
+        self._data = [dict((k,v._ids_to_objects(t)) for k,v in t.items())
+                      for t in d]
+        f.close()
+
+    def _disconnect(self):
+        f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
+        pickle.dump([dict((k,v._objects_to_ids())
+                          for k,v in t.items()) for t in self._data], f, -1)
+        f.close()
+        self._data = None
+
+    def _add(self, id, parent=None, directory=False):
+        if parent == None:
+            parent = '__ROOT__'
+        p = self._data[-1][parent]
+        self._data[-1][id] = Entry(id, parent=p, directory=directory)
+
+    def _exists(self, id, revision=None):
+        if revision == None:
+            revision = -1
+        else:
+            revision = int(revision)
+        return id in self._data[revision]
+
+    def _remove(self, id):
+        if self._data[-1][id].directory == True \
+                and len(self.children(id)) > 0:
+            raise DirectoryNotEmpty(id)
+        e = self._data[-1].pop(id)
+        e.parent.remove(e)
+
+    def _recursive_remove(self, id):
+        for entry in reversed(list(self._data[-1][id].traverse())):
+            self._remove(entry.id)
+
+    def _ancestors(self, id=None, revision=None):
+        if id == None:
+            return []
+        if revision == None:
+            revision = -1
+        else:
+            revision = int(revision)
+        ancestors = []
+        stack = [id]
+        while len(stack) > 0:
+            id = stack.pop(0)
+            parent = self._data[revision][id].parent
+            if parent != None and not parent.id.startswith('__'):
+                ancestor = parent.id
+                ancestors.append(ancestor)
+                stack.append(ancestor)
+        return ancestors
+
+    def _children(self, id=None, revision=None):
+        if id == None:
+            id = '__ROOT__'
+        if revision == None:
+            revision = -1
+        else:
+            revision = int(revision)
+        return [c.id for c in self._data[revision][id]
+                if not c.id.startswith('__')]
+
+    def _get(self, id, default=InvalidObject, revision=None):
+        if revision == None:
+            revision = -1
+        else:
+            revision = int(revision)
+        if id in self._data[revision] \
+                and self._data[revision][id].value != _EMPTY:
+            return self._data[revision][id].value
+        elif default == InvalidObject:
+            raise InvalidID(id)
+        return default
+
+    def _set(self, id, value):
+        if id not in self._data[-1]:
+            raise InvalidID(id)
+        self._data[-1][id].value = value
+
+    def commit(self, *args, **kwargs):
+        """
+        Commit the current repository, with a commit message string
+        summary and body.  Return the name of the new revision.
+
+        If allow_empty == False (the default), raise EmptyCommit if
+        there are no changes to commit.
+        """
+        if self.is_writeable() == False:
+            raise NotWriteable('Cannot commit to unwriteable storage.')
+        return self._commit(*args, **kwargs)
+
+    def _commit(self, summary, body=None, allow_empty=False):
+        if self._data[-1] == self._data[-2] and allow_empty == False:
+            raise EmptyCommit
+        self._data[-1]["__COMMIT__SUMMARY__"].value = summary
+        self._data[-1]["__COMMIT__BODY__"].value = body
+        rev = str(len(self._data)-1)
+        self._data.append(copy.deepcopy(self._data[-1]))
+        return rev
+
+    def revision_id(self, index=None):
+        """
+        Return the name of the <index>th revision.  The choice of
+        which branch to follow when crossing branches/merges is not
+        defined.  Revision indices start at 1; ID 0 is the blank
+        repository.
+
+        Return None if index==None.
+
+        If the specified revision does not exist, raise InvalidRevision.
+        """
+        if index == None:
+            return None
+        try:
+            if int(index) != index:
+                raise InvalidRevision(index)
+        except ValueError:
+            raise InvalidRevision(index)
+        L = len(self._data) - 1  # -1 b/c of initial commit
+        if index >= -L and index <= L:
+            return str(index % L)
+        raise InvalidRevision(i)
+
+    def changed(self, revision):
+        """Return a tuple of lists of ids `(new, modified, removed)` from the
+        specified revision to the current situation.
+        """
+        new = []
+        modified = []
+        removed = []
+        for id,value in self._data[int(revision)].items():
+            if id.startswith('__'):
+                continue
+            if not id in self._data[-1]:
+                removed.append(id)
+            elif value.value != self._data[-1][id].value:
+                modified.append(id)
+        for id in self._data[-1]:
+            if not id in self._data[int(revision)]:
+                new.append(id)
+        return (new, modified, removed)
+
+
+if TESTING == True:
+    class StorageTestCase (unittest.TestCase):
+        """Test cases for Storage class."""
+
+        Class = Storage
+
+        def __init__(self, *args, **kwargs):
+            super(StorageTestCase, self).__init__(*args, **kwargs)
+            self.dirname = None
+
+        # this class will be the basis of tests for several classes,
+        # so make sure we print the name of the class we're dealing with.
+        def _classname(self):
+            version = '?'
+            try:
+                if hasattr(self, 's'):
+                    version = self.s.version()
+            except:
+                pass
+            return '%s:%s' % (self.Class.__name__, version)
+
+        def fail(self, msg=None):
+            """Fail immediately, with the given message."""
+            raise self.failureException, \
+                '(%s) %s' % (self._classname(), msg)
+
+        def failIf(self, expr, msg=None):
+            "Fail the test if the expression is true."
+            if expr: raise self.failureException, \
+                '(%s) %s' % (self._classname(), msg)
+
+        def failUnless(self, expr, msg=None):
+            """Fail the test unless the expression is true."""
+            if not expr: raise self.failureException, \
+                '(%s) %s' % (self._classname(), msg)
+
+        def setUp(self):
+            """Set up test fixtures for Storage test case."""
+            super(StorageTestCase, self).setUp()
+            self.dir = Dir()
+            self.dirname = self.dir.path
+            self.s = self.Class(repo=self.dirname)
+            self.assert_failed_connect()
+            self.s.init()
+            self.s.connect()
+
+        def tearDown(self):
+            super(StorageTestCase, self).tearDown()
+            self.s.disconnect()
+            self.s.destroy()
+            self.assert_failed_connect()
+            self.dir.cleanup()
+
+        def assert_failed_connect(self):
+            try:
+                self.s.connect()
+                self.fail(
+                    "Connected to %(name)s repository before initialising"
+                    % vars(self.Class))
+            except ConnectionError:
+                pass
+
+    class Storage_init_TestCase (StorageTestCase):
+        """Test cases for Storage.init method."""
+
+        def test_connect_should_succeed_after_init(self):
+            """Should connect after initialization."""
+            self.s.connect()
+
+    class Storage_connect_disconnect_TestCase (StorageTestCase):
+        """Test cases for Storage.connect and .disconnect methods."""
+
+        def test_multiple_disconnects(self):
+            """Should be able to call .disconnect multiple times."""
+            self.s.disconnect()
+            self.s.disconnect()
+
+    class Storage_add_remove_TestCase (StorageTestCase):
+        """Test cases for Storage.add, .remove, and .recursive_remove methods."""
+
+        def test_initially_empty(self):
+            """New repository should be empty."""
+            self.failUnless(len(self.s.children()) == 0, self.s.children())
+
+        def test_add_identical_rooted(self):
+            """Adding entries with the same ID should not increase the number of children.
+            """
+            for i in range(10):
+                self.s.add('some id', directory=False)
+                s = sorted(self.s.children())
+                self.failUnless(s == ['some id'], s)
+
+        def test_add_rooted(self):
+            """Adding entries should increase the number of children (rooted).
+            """
+            ids = []
+            for i in range(10):
+                ids.append(str(i))
+                self.s.add(ids[-1], directory=(i % 2 == 0))
+                s = sorted(self.s.children())
+                self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
+
+        def test_add_nonrooted(self):
+            """Adding entries should increase the number of children (nonrooted).
+            """
+            self.s.add('parent', directory=True)
+            ids = []
+            for i in range(10):
+                ids.append(str(i))
+                self.s.add(ids[-1], 'parent', directory=(i % 2 == 0))
+                s = sorted(self.s.children('parent'))
+                self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
+                s = self.s.children()
+                self.failUnless(s == ['parent'], s)
+
+        def test_ancestors(self):
+            """Check ancestors lists.
+            """
+            self.s.add('parent', directory=True)
+            for i in range(10):
+                i_id = str(i)
+                self.s.add(i_id, 'parent', directory=True)
+                for j in range(10): # add some grandkids
+                    j_id = str(20*(i+1)+j)
+                    self.s.add(j_id, i_id, directory=(i%2 == 0))
+                    ancestors = sorted(self.s.ancestors(j_id))
+                    self.failUnless(ancestors == [i_id, 'parent'],
+                        'Unexpected ancestors for %s/%s, "%s"'
+                        % (i_id, j_id, ancestors))
+
+        def test_children(self):
+            """Non-UUID ids should be returned as such.
+            """
+            self.s.add('parent', directory=True)
+            ids = []
+            for i in range(10):
+                ids.append('parent/%s' % str(i))
+                self.s.add(ids[-1], 'parent', directory=(i % 2 == 0))
+                s = sorted(self.s.children('parent'))
+                self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
+
+        def test_add_invalid_directory(self):
+            """Should not be able to add children to non-directories.
+            """
+            self.s.add('parent', directory=False)
+            try:
+                self.s.add('child', 'parent', directory=False)
+                self.fail(
+                    '%s.add() succeeded instead of raising InvalidDirectory'
+                    % (vars(self.Class)['name']))
+            except InvalidDirectory:
+                pass
+            try:
+                self.s.add('child', 'parent', directory=True)
+                self.fail(
+                    '%s.add() succeeded instead of raising InvalidDirectory'
+                    % (vars(self.Class)['name']))
+            except InvalidDirectory:
+                pass
+            self.failUnless(len(self.s.children('parent')) == 0,
+                            self.s.children('parent'))
+
+        def test_remove_rooted(self):
+            """Removing entries should decrease the number of children (rooted).
+            """
+            ids = []
+            for i in range(10):
+                ids.append(str(i))
+                self.s.add(ids[-1], directory=(i % 2 == 0))
+            for i in range(10):
+                self.s.remove(ids.pop())
+                s = sorted(self.s.children())
+                self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
+
+        def test_remove_nonrooted(self):
+            """Removing entries should decrease the number of children (nonrooted).
+            """
+            self.s.add('parent', directory=True)
+            ids = []
+            for i in range(10):
+                ids.append(str(i))
+                self.s.add(ids[-1], 'parent', directory=False)#(i % 2 == 0))
+            for i in range(10):
+                self.s.remove(ids.pop())
+                s = sorted(self.s.children('parent'))
+                self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
+                if len(s) > 0:
+                    s = self.s.children()
+                    self.failUnless(s == ['parent'], s)
+
+        def test_remove_directory_not_empty(self):
+            """Removing a non-empty directory entry should raise exception.
+            """
+            self.s.add('parent', directory=True)
+            ids = []
+            for i in range(10):
+                ids.append(str(i))
+                self.s.add(ids[-1], 'parent', directory=(i % 2 == 0))
+            self.s.remove(ids.pop()) # empty directory removal succeeds
+            try:
+                self.s.remove('parent') # empty directory removal succeeds
+                self.fail(
+                    "%s.remove() didn't raise DirectoryNotEmpty"
+                    % (vars(self.Class)['name']))
+            except DirectoryNotEmpty:
+                pass
+
+        def test_recursive_remove(self):
+            """Recursive remove should empty the tree."""
+            self.s.add('parent', directory=True)
+            ids = []
+            for i in range(10):
+                ids.append(str(i))
+                self.s.add(ids[-1], 'parent', directory=True)
+                for j in range(10): # add some grandkids
+                    self.s.add(str(20*(i+1)+j), ids[-1], directory=(i%2 == 0))
+            self.s.recursive_remove('parent')
+            s = sorted(self.s.children())
+            self.failUnless(s == [], s)
+
+    class Storage_get_set_TestCase (StorageTestCase):
+        """Test cases for Storage.get and .set methods."""
+
+        id = 'unlikely id'
+        val = 'unlikely value'
+
+        def test_get_default(self):
+            """Get should return specified default if id not in Storage.
+            """
+            ret = self.s.get(self.id, default=self.val)
+            self.failUnless(ret == self.val,
+                    "%s.get() returned %s not %s"
+                    % (vars(self.Class)['name'], ret, self.val))
+
+        def test_get_default_exception(self):
+            """Get should raise exception if id not in Storage and no default.
+            """
+            try:
+                ret = self.s.get(self.id)
+                self.fail(
+                    "%s.get() returned %s instead of raising InvalidID"
+                    % (vars(self.Class)['name'], ret))
+            except InvalidID:
+                pass
+
+        def test_get_initial_value(self):
+            """Data value should be default before any value has been set.
+            """
+            self.s.add(self.id, directory=False)
+            val = 'UNLIKELY DEFAULT'
+            ret = self.s.get(self.id, default=val)
+            self.failUnless(ret == val,
+                    "%s.get() returned %s not %s"
+                    % (vars(self.Class)['name'], ret, val))
+
+        def test_set_exception(self):
+            """Set should raise exception if id not in Storage.
+            """
+            try:
+                self.s.set(self.id, self.val)
+                self.fail(
+                    "%(name)s.set() did not raise InvalidID"
+                    % vars(self.Class))
+            except InvalidID:
+                pass
+
+        def test_set(self):
+            """Set should define the value returned by get.
+            """
+            self.s.add(self.id, directory=False)
+            self.s.set(self.id, self.val)
+            ret = self.s.get(self.id)
+            self.failUnless(ret == self.val,
+                    "%s.get() returned %s not %s"
+                    % (vars(self.Class)['name'], ret, self.val))
+
+        def test_unicode_set(self):
+            """Set should define the value returned by get.
+            """
+            val = u'Fran\xe7ois'
+            self.s.add(self.id, directory=False)
+            self.s.set(self.id, val)
+            ret = self.s.get(self.id, decode=True)
+            self.failUnless(type(ret) == types.UnicodeType,
+                    "%s.get() returned %s not UnicodeType"
+                    % (vars(self.Class)['name'], type(ret)))
+            self.failUnless(ret == val,
+                    "%s.get() returned %s not %s"
+                    % (vars(self.Class)['name'], ret, self.val))
+            ret = self.s.get(self.id)
+            self.failUnless(type(ret) == types.StringType,
+                    "%s.get() returned %s not StringType"
+                    % (vars(self.Class)['name'], type(ret)))
+            s = unicode(ret, self.s.encoding)
+            self.failUnless(s == val,
+                    "%s.get() returned %s not %s"
+                    % (vars(self.Class)['name'], s, self.val))
+
+
+    class Storage_persistence_TestCase (StorageTestCase):
+        """Test cases for Storage.disconnect and .connect methods."""
+
+        id = 'unlikely id'
+        val = 'unlikely value'
+
+        def test_get_set_persistence(self):
+            """Set should define the value returned by get after reconnect.
+            """
+            self.s.add(self.id, directory=False)
+            self.s.set(self.id, self.val)
+            self.s.disconnect()
+            self.s.connect()
+            ret = self.s.get(self.id)
+            self.failUnless(ret == self.val,
+                    "%s.get() returned %s not %s"
+                    % (vars(self.Class)['name'], ret, self.val))
+
+        def test_empty_get_set_persistence(self):
+            """After empty set, get may return either an empty string or default.
+            """
+            self.s.add(self.id, directory=False)
+            self.s.set(self.id, '')
+            self.s.disconnect()
+            self.s.connect()
+            default = 'UNLIKELY DEFAULT'
+            ret = self.s.get(self.id, default=default)
+            self.failUnless(ret in ['', default],
+                    "%s.get() returned %s not in %s"
+                    % (vars(self.Class)['name'], ret, ['', default]))
+
+        def test_add_nonrooted_persistence(self):
+            """Adding entries should increase the number of children after reconnect.
+            """
+            self.s.add('parent', directory=True)
+            ids = []
+            for i in range(10):
+                ids.append(str(i))
+                self.s.add(ids[-1], 'parent', directory=(i % 2 == 0))
+            self.s.disconnect()
+            self.s.connect()
+            s = sorted(self.s.children('parent'))
+            self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
+            s = self.s.children()
+            self.failUnless(s == ['parent'], s)
+
+    class VersionedStorageTestCase (StorageTestCase):
+        """Test cases for VersionedStorage methods."""
+
+        Class = VersionedStorage
+
+    class VersionedStorage_commit_TestCase (VersionedStorageTestCase):
+        """Test cases for VersionedStorage.commit and revision_ids methods."""
+
+        id = 'unlikely id'
+        val = 'Some value'
+        commit_msg = 'Committing something interesting'
+        commit_body = 'Some\nlonger\ndescription\n'
+
+        def _setup_for_empty_commit(self):
+            """
+            Initialization might add some files to version control, so
+            commit those first, before testing the empty commit
+            functionality.
+            """
+            try:
+                self.s.commit('Added initialization files')
+            except EmptyCommit:
+                pass
+                
+        def test_revision_id_exception(self):
+            """Invalid revision id should raise InvalidRevision.
+            """
+            try:
+                rev = self.s.revision_id('highly unlikely revision id')
+                self.fail(
+                    "%s.revision_id() didn't raise InvalidRevision, returned %s."
+                    % (vars(self.Class)['name'], rev))
+            except InvalidRevision:
+                pass
+
+        def test_empty_commit_raises_exception(self):
+            """Empty commit should raise exception.
+            """
+            self._setup_for_empty_commit()
+            try:
+                self.s.commit(self.commit_msg, self.commit_body)
+                self.fail(
+                    "Empty %(name)s.commit() didn't raise EmptyCommit."
+                    % vars(self.Class))
+            except EmptyCommit:
+                pass
+
+        def test_empty_commit_allowed(self):
+            """Empty commit should _not_ raise exception if allow_empty=True.
+            """
+            self._setup_for_empty_commit()
+            self.s.commit(self.commit_msg, self.commit_body,
+                          allow_empty=True)
+
+        def test_commit_revision_ids(self):
+            """Commit / revision_id should agree on revision ids.
+            """
+            def val(i):
+                return '%s:%d' % (self.val, i+1)
+            self.s.add(self.id, directory=False)
+            revs = []
+            for i in range(10):
+                self.s.set(self.id, val(i))
+                revs.append(self.s.commit('%s: %d' % (self.commit_msg, i),
+                                          self.commit_body))
+            for i in range(10):
+                rev = self.s.revision_id(i+1)
+                self.failUnless(rev == revs[i],
+                                "%s.revision_id(%d) returned %s not %s"
+                                % (vars(self.Class)['name'], i+1, rev, revs[i]))
+            for i in range(-1, -9, -1):
+                rev = self.s.revision_id(i)
+                self.failUnless(rev == revs[i],
+                                "%s.revision_id(%d) returned %s not %s"
+                                % (vars(self.Class)['name'], i, rev, revs[i]))
+
+        def test_get_previous_version(self):
+            """Get should be able to return the previous version.
+            """
+            def val(i):
+                return '%s:%d' % (self.val, i+1)
+            self.s.add(self.id, directory=False)
+            revs = []
+            for i in range(10):
+                self.s.set(self.id, val(i))
+                revs.append(self.s.commit('%s: %d' % (self.commit_msg, i),
+                                          self.commit_body))
+            for i in range(10):
+                ret = self.s.get(self.id, revision=revs[i])
+                self.failUnless(ret == val(i),
+                                "%s.get() returned %s not %s for revision %s"
+                                % (vars(self.Class)['name'], ret, val(i), revs[i]))
+
+        def test_get_previous_children(self):
+            """Children list should be revision dependent.
+            """
+            self.s.add('parent', directory=True)
+            revs = []
+            cur_children = []
+            children = []
+            for i in range(10):
+                new_child = str(i)
+                self.s.add(new_child, 'parent')
+                self.s.set(new_child, self.val)
+                revs.append(self.s.commit('%s: %d' % (self.commit_msg, i),
+                                          self.commit_body))
+                cur_children.append(new_child)
+                children.append(list(cur_children))
+            for i in range(10):
+                ret = sorted(self.s.children('parent', revision=revs[i]))
+                self.failUnless(ret == children[i],
+                                "%s.get() returned %s not %s for revision %s"
+                                % (vars(self.Class)['name'], ret,
+                                   children[i], revs[i]))
+
+    class VersionedStorage_changed_TestCase (VersionedStorageTestCase):
+        """Test cases for VersionedStorage.changed() method."""
+
+        def test_changed(self):
+            """Changed lists should reflect past activity"""
+            self.s.add('dir', directory=True)
+            self.s.add('modified', parent='dir')
+            self.s.set('modified', 'some value to be modified')
+            self.s.add('moved', parent='dir')
+            self.s.set('moved', 'this entry will be moved')
+            self.s.add('removed', parent='dir')
+            self.s.set('removed', 'this entry will be deleted')
+            revA = self.s.commit('Initial state')
+            self.s.add('new', parent='dir')
+            self.s.set('new', 'this entry is new')
+            self.s.set('modified', 'a new value')
+            self.s.remove('moved')
+            self.s.add('moved2', parent='dir')
+            self.s.set('moved2', 'this entry will be moved')
+            self.s.remove('removed')
+            revB = self.s.commit('Final state')
+            new,mod,rem = self.s.changed(revA)
+            self.failUnless(sorted(new) == ['moved2', 'new'],
+                            'Unexpected new: %s' % new)
+            self.failUnless(mod == ['modified'],
+                            'Unexpected modified: %s' % mod)
+            self.failUnless(sorted(rem) == ['moved', 'removed'],
+                            'Unexpected removed: %s' % rem)
+
+    def make_storage_testcase_subclasses(storage_class, namespace):
+        """Make StorageTestCase subclasses for storage_class in namespace."""
+        storage_testcase_classes = [
+            c for c in (
+                ob for ob in globals().values() if isinstance(ob, type))
+            if issubclass(c, StorageTestCase) \
+                and c.Class == Storage]
+
+        for base_class in storage_testcase_classes:
+            testcase_class_name = storage_class.__name__ + base_class.__name__
+            testcase_class_bases = (base_class,)
+            testcase_class_dict = dict(base_class.__dict__)
+            testcase_class_dict['Class'] = storage_class
+            testcase_class = type(
+                testcase_class_name, testcase_class_bases, testcase_class_dict)
+            setattr(namespace, testcase_class_name, testcase_class)
+
+    def make_versioned_storage_testcase_subclasses(storage_class, namespace):
+        """Make VersionedStorageTestCase subclasses for storage_class in namespace."""
+        storage_testcase_classes = [
+            c for c in (
+                ob for ob in globals().values() if isinstance(ob, type))
+            if ((issubclass(c, StorageTestCase) \
+                     and c.Class == Storage)
+                or
+                (issubclass(c, VersionedStorageTestCase) \
+                     and c.Class == VersionedStorage))]
+
+        for base_class in storage_testcase_classes:
+            testcase_class_name = storage_class.__name__ + base_class.__name__
+            testcase_class_bases = (base_class,)
+            testcase_class_dict = dict(base_class.__dict__)
+            testcase_class_dict['Class'] = storage_class
+            testcase_class = type(
+                testcase_class_name, testcase_class_bases, testcase_class_dict)
+            setattr(namespace, testcase_class_name, testcase_class)
+
+    make_storage_testcase_subclasses(VersionedStorage, sys.modules[__name__])
+
+    unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
+    suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
diff --git a/libbe/storage/http.py b/libbe/storage/http.py
new file mode 100644 (file)
index 0000000..7ec9f54
--- /dev/null
@@ -0,0 +1,446 @@
+# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+# For urllib2 information, see
+#   urllib2, from urllib2 - The Missing Manual
+#   http://www.voidspace.org.uk/python/articles/urllib2.shtml
+# 
+# A dictionary of response codes is available in
+#   httplib.responses
+
+"""Define an HTTP-based :class:`~libbe.storage.base.VersionedStorage`
+implementation.
+
+See Also
+--------
+:mod:`libbe.command.serve` : the associated server
+
+"""
+
+import sys
+import urllib
+import urllib2
+import urlparse
+
+import libbe
+import libbe.version
+import base
+from libbe import TESTING
+
+if TESTING == True:
+    import copy
+    import doctest
+    import StringIO
+    import unittest
+
+    import libbe.bugdir
+    import libbe.command.serve
+
+
+USER_AGENT = 'BE-HTTP-Storage'
+HTTP_OK = 200
+HTTP_FOUND = 302
+HTTP_TEMP_REDIRECT = 307
+HTTP_USER_ERROR = 418
+"""Status returned to indicate exceptions on the server side.
+
+A BE-specific extension to the HTTP/1.1 protocol (See `RFC 2616`_).
+
+.. _RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
+"""
+
+HTTP_VALID = [HTTP_OK, HTTP_FOUND, HTTP_TEMP_REDIRECT, HTTP_USER_ERROR]
+
+class InvalidURL (Exception):
+    def __init__(self, error=None, url=None, msg=None):
+        Exception.__init__(self, msg)
+        self.url = url
+        self.error = error
+        self.msg = msg
+    def __str__(self):
+        if self.msg == None:
+            if self.error == None:
+                return "Unknown URL error: %s" % self.url
+            return self.error.__str__()
+        return self.msg
+
+def get_post_url(url, get=True, data_dict=None, headers=[]):
+    """Execute a GET or POST transaction.
+
+    Parameters
+    ----------
+    url : str
+      The base URL (query portion added internally, if necessary).
+    get : bool
+      Use GET if True, otherwise use POST.
+    data_dict : dict
+      Data to send, either by URL query (if GET) or by POST (if POST).
+    headers : list
+      Extra HTTP headers to add to the request.
+    """
+    if data_dict == None:
+        data_dict = {}
+    if get == True:
+        if data_dict != {}:
+            # encode get parameters in the url
+            param_string = urllib.urlencode(data_dict)
+            url = "%s?%s" % (url, param_string)
+        data = None
+    else:
+        data = urllib.urlencode(data_dict)
+    headers = dict(headers)
+    headers['User-Agent'] = USER_AGENT
+    req = urllib2.Request(url, data=data, headers=headers)
+    try:
+        response = urllib2.urlopen(req)
+    except urllib2.HTTPError, e:
+        if hasattr(e, 'reason'):
+            msg = 'We failed to reach a server.\nURL: %s\nReason: %s' \
+                % (url, e.reason)
+        elif hasattr(e, 'code'):
+            msg = "The server couldn't fulfill the request.\nURL: %s\nError code: %s" \
+                % (url, e.code)
+        raise InvalidURL(error=e, url=url, msg=msg)
+    page = response.read()
+    final_url = response.geturl()
+    info = response.info()
+    response.close()
+    return (page, final_url, info)
+
+
+class HTTP (base.VersionedStorage):
+    """:class:`~libbe.storage.base.VersionedStorage` implementation over
+    HTTP.
+
+    Uses GET to retrieve information and POST to set information.
+    """
+    name = 'HTTP'
+
+    def __init__(self, repo, *args, **kwargs):
+        repo,self.uname,self.password = self.parse_repo(repo)
+        base.VersionedStorage.__init__(self, repo, *args, **kwargs)
+
+    def parse_repo(self, repo):
+        """Grab username and password (if any) from the repo URL.
+
+        Examples
+        --------
+
+        >>> s = HTTP('http://host.com/path/to/repo')
+        >>> s.repo
+        'http://host.com/path/to/repo'
+        >>> s.uname == None
+        True
+        >>> s.password == None
+        True
+        >>> s.parse_repo('http://joe:secret@host.com/path/to/repo')
+        ('http://host.com/path/to/repo', 'joe', 'secret')
+        """
+        scheme,netloc,path,params,query,fragment = urlparse.urlparse(repo)
+        parts = netloc.split('@', 1)
+        if len(parts) == 2:
+            uname,password = parts[0].split(':')
+            repo = urlparse.urlunparse(
+                (scheme, parts[1], path, params, query, fragment))
+        else:
+            uname,password = (None, None)
+        return (repo, uname, password)
+
+    def get_post_url(self, url, get=True, data_dict=None, headers=[]):
+        if self.uname != None and self.password != None:
+            headers.append(('Authorization','Basic %s' % \
+                ('%s:%s' % (self.uname, self.password)).encode('base64')))
+        return get_post_url(url, get, data_dict, headers)
+
+    def storage_version(self, revision=None):
+        """Return the storage format for this backend."""
+        return libbe.storage.STORAGE_VERSION
+
+    def _init(self):
+        """Create a new storage repository."""
+        raise base.NotSupported(
+            'init', 'Cannot initialize this repository format.')
+
+    def _destroy(self):
+        """Remove the storage repository."""
+        raise base.NotSupported(
+            'destroy', 'Cannot destroy this repository format.')
+
+    def _connect(self):
+        self.check_storage_version()
+
+    def _disconnect(self):
+        pass
+
+    def _add(self, id, parent=None, directory=False):
+        url = urlparse.urljoin(self.repo, 'add')
+        page,final_url,info = self.get_post_url(
+            url, get=False,
+            data_dict={'id':id, 'parent':parent, 'directory':directory})
+
+    def _exists(self, id, revision=None):
+        url = urlparse.urljoin(self.repo, 'exists')
+        page,final_url,info = self.get_post_url(
+            url, get=True,
+            data_dict={'id':id, 'revision':revision})
+        if page == 'True':
+            return True
+        return False
+
+    def _remove(self, id):
+        url = urlparse.urljoin(self.repo, 'remove')
+        page,final_url,info = self.get_post_url(
+            url, get=False,
+            data_dict={'id':id, 'recursive':False})
+
+    def _recursive_remove(self, id):
+        url = urlparse.urljoin(self.repo, 'remove')
+        page,final_url,info = self.get_post_url(
+            url, get=False,
+            data_dict={'id':id, 'recursive':True})
+
+    def _ancestors(self, id=None, revision=None):
+        url = urlparse.urljoin(self.repo, 'ancestors')
+        page,final_url,info = self.get_post_url(
+            url, get=True,
+            data_dict={'id':id, 'revision':revision})
+        return page.strip('\n').splitlines()
+
+    def _children(self, id=None, revision=None):
+        url = urlparse.urljoin(self.repo, 'children')
+        page,final_url,info = self.get_post_url(
+            url, get=True,
+            data_dict={'id':id, 'revision':revision})
+        return page.strip('\n').splitlines()
+
+    def _get(self, id, default=base.InvalidObject, revision=None):
+        url = urlparse.urljoin(self.repo, '/'.join(['get', id]))
+        try:
+            page,final_url,info = self.get_post_url(
+                url, get=True,
+                data_dict={'revision':revision})
+        except InvalidURL, e:
+            if not (hasattr(e.error, 'code') and e.error.code in HTTP_VALID):
+                raise
+            elif default == base.InvalidObject:
+                raise base.InvalidID(id)
+            return default
+        version = info['X-BE-Version']
+        if version != libbe.storage.STORAGE_VERSION:
+            raise base.InvalidStorageVersion(
+                version, libbe.storage.STORAGE_VERSION)
+        return page
+
+    def _set(self, id, value):
+        url = urlparse.urljoin(self.repo, '/'.join(['set', id]))
+        try:
+            page,final_url,info = self.get_post_url(
+                url, get=False,
+                data_dict={'value':value})
+        except InvalidURL, e:
+            if not (hasattr(e.error, 'code') and e.error.code in HTTP_VALID):
+                raise
+            if e.error.code == HTTP_USER_ERROR \
+                    and not 'InvalidID' in str(e.error):
+                raise base.InvalidDirectory(
+                    'Directory %s cannot have data' % id)
+            raise base.InvalidID(id)
+
+    def _commit(self, summary, body=None, allow_empty=False):
+        url = urlparse.urljoin(self.repo, 'commit')
+        try:
+            page,final_url,info = self.get_post_url(
+                url, get=False,
+                data_dict={'summary':summary, 'body':body,
+                           'allow_empty':allow_empty})
+        except InvalidURL, e:
+            if not (hasattr(e.error, 'code') and e.error.code in HTTP_VALID):
+                raise
+            if e.error.code == HTTP_USER_ERROR:
+                raise base.EmptyCommit
+            raise base.InvalidID(id)
+        return page.rstrip('\n')
+
+    def revision_id(self, index=None):
+        """Return the name of the <index>th revision.
+
+        The choice of which branch to follow when crossing
+        branches/merges is not defined.  Revision indices start at 1;
+        ID 0 is the blank repository.
+
+        Return None if index==None.
+
+        Raises
+        ------
+        InvalidRevision
+          If the specified revision does not exist.
+        """
+        if index == None:
+            return None
+        try:
+            if int(index) != index:
+                raise base.InvalidRevision(index)
+        except ValueError:
+            raise base.InvalidRevision(index)
+        url = urlparse.urljoin(self.repo, 'revision-id')
+        try:
+            page,final_url,info = self.get_post_url(
+                url, get=True,
+                data_dict={'index':index})
+        except InvalidURL, e:
+            if not (hasattr(e.error, 'code') and e.error.code in HTTP_VALID):
+                raise
+            if e.error.code == HTTP_USER_ERROR:
+                raise base.InvalidRevision(index)
+            raise base.InvalidID(id)
+        return page.rstrip('\n')
+
+    def changed(self, revision=None):
+        url = urlparse.urljoin(self.repo, 'changed')
+        page,final_url,info = self.get_post_url(
+            url, get=True,
+            data_dict={'revision':revision})
+        lines = page.strip('\n')
+        new,mod,rem = [p.splitlines() for p in page.split('\n\n')]
+        return (new, mod, rem)
+
+    def check_storage_version(self):
+        version = self.storage_version()
+        if version != libbe.storage.STORAGE_VERSION:
+            raise base.InvalidStorageVersion(
+                version, libbe.storage.STORAGE_VERSION)
+
+    def storage_version(self, revision=None):
+        url = urlparse.urljoin(self.repo, 'version')
+        page,final_url,info = self.get_post_url(
+            url, get=True, data_dict={'revision':revision})
+        return page.rstrip('\n')
+
+if TESTING == True:
+    class GetPostUrlTestCase (unittest.TestCase):
+        """Test cases for get_post_url()"""
+        def test_get(self):
+            url = 'http://bugseverywhere.org/be/show/HomePage'
+            page,final_url,info = get_post_url(url=url)
+            self.failUnless(final_url == url,
+                'Redirect?\n  Expected: "%s"\n  Got:      "%s"'
+                % (url, final_url))
+        def test_get_redirect(self):
+            url = 'http://bugseverywhere.org'
+            expected = 'http://bugseverywhere.org/be/show/HomePage'
+            page,final_url,info = get_post_url(url=url)
+            self.failUnless(final_url == expected,
+                'Redirect?\n  Expected: "%s"\n  Got:      "%s"'
+                % (expected, final_url))
+
+    class TestingHTTP (HTTP):
+        name = 'TestingHTTP'
+        def __init__(self, repo, *args, **kwargs):
+            self._storage_backend = base.VersionedStorage(repo)
+            self.app = libbe.command.serve.ServerApp(
+                storage=self._storage_backend)
+            HTTP.__init__(self, repo='http://localhost:8000/', *args, **kwargs)
+            self.intitialized = False
+            # duplicated from libbe.storage.serve.WSGITestCase
+            self.default_environ = {
+                'REQUEST_METHOD': 'GET', # 'POST', 'HEAD'
+                'SCRIPT_NAME':'',
+                'PATH_INFO': '',
+                #'QUERY_STRING':'',   # may be empty or absent
+                #'CONTENT_TYPE':'',   # may be empty or absent
+                #'CONTENT_LENGTH':'', # may be empty or absent
+                'SERVER_NAME':'example.com',
+                'SERVER_PORT':'80',
+                'SERVER_PROTOCOL':'HTTP/1.1',
+                'wsgi.version':(1,0),
+                'wsgi.url_scheme':'http',
+                'wsgi.input':StringIO.StringIO(),
+                'wsgi.errors':StringIO.StringIO(),
+                'wsgi.multithread':False,
+                'wsgi.multiprocess':False,
+                'wsgi.run_once':False,
+                }
+        def getURL(self, app, path='/', method='GET', data=None,
+                   scheme='http', environ={}):
+            # duplicated from libbe.storage.serve.WSGITestCase
+            env = copy.copy(self.default_environ)
+            env['PATH_INFO'] = path
+            env['REQUEST_METHOD'] = method
+            env['scheme'] = scheme
+            if data != None:
+                enc_data = urllib.urlencode(data)
+                if method == 'POST':
+                    env['CONTENT_LENGTH'] = len(enc_data)
+                    env['wsgi.input'] = StringIO.StringIO(enc_data)
+                else:
+                    assert method in ['GET', 'HEAD'], method
+                    env['QUERY_STRING'] = enc_data
+            for key,value in environ.items():
+                env[key] = value
+            return ''.join(app(env, self.start_response))
+        def start_response(self, status, response_headers, exc_info=None):
+            self.status = status
+            self.response_headers = response_headers
+            self.exc_info = exc_info
+        def get_post_url(self, url, get=True, data_dict=None, headers=[]):
+            if get == True:
+                method = 'GET'
+            else:
+                method = 'POST'
+            scheme,netloc,path,params,query,fragment = urlparse.urlparse(url)
+            environ = {}
+            for header_name,header_value in headers:
+                environ['HTTP_%s' % header_name] = header_value
+            output = self.getURL(
+                self.app, path, method, data_dict, scheme, environ)
+            if self.status != '200 OK':
+                class __estr (object):
+                    def __init__(self, string):
+                        self.string = string
+                        self.code = int(string.split()[0])
+                    def __str__(self):
+                        return self.string
+                error = __estr(self.status)
+                raise InvalidURL(error=error, url=url, msg=output)
+            info = dict(self.response_headers)
+            return (output, url, info)
+        def _init(self):
+            try:
+                HTTP._init(self)
+                raise AssertionError
+            except base.NotSupported:
+                pass
+            self._storage_backend._init()
+        def _destroy(self):
+            try:
+                HTTP._destroy(self)
+                raise AssertionError
+            except base.NotSupported:
+                pass
+            self._storage_backend._destroy()
+        def _connect(self):
+            self._storage_backend._connect()
+            HTTP._connect(self)
+        def _disconnect(self):
+            HTTP._disconnect(self)
+            self._storage_backend._disconnect()
+
+
+    base.make_versioned_storage_testcase_subclasses(
+        TestingHTTP, sys.modules[__name__])
+
+    unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
+    suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
similarity index 54%
rename from libbe/config.py
rename to libbe/storage/util/config.py
index fb5a0288fbd0b8a3faf491197e6e2000fd4ae662..724d2d3a17e4b5573da29ef9521f30ad410b88c3 100644 (file)
@@ -1,4 +1,5 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
 #                         W. Trevor King <wking@drexel.edu>
 #
 # This program is free software; you can redistribute it and/or modify
 # 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().
+"""Create, save, and load the per-user config file at :func:`path`.
 """
 
 import ConfigParser
 import codecs
-import locale
 import os.path
-import sys
-import doctest
+
+import libbe
+import libbe.util.encoding
+if libbe.TESTING == True:
+    import doctest
 
 
-default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding()
+default_encoding = libbe.util.encoding.get_filesystem_encoding()
+"""Default filesystem encoding.
+
+Initialized with :func:`libbe.util.encoding.get_filesystem_encoding`.
+"""
 
 def path():
-    """Return the path to the per-user config file"""
+    """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
+    """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
+    Parameters
+    ----------
+    name : str
+      The name of the value to set.
+    value : str or None
+      The new value to set (or None to delete the value).
+    section : str
+      The section to store the name/value in.
+    encoding : str
+      The config file's encoding, defaults to :data:`default_encoding`.
     """
     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)
+    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)
+    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
+    """Get a value from the per-user config file
+
+    Parameters
+    ----------
+    name : str
+      The name of the value to set.
+    section : str
+      The section to store the name/value in.
+    default :
+      The value to return if `name` is not set.
+    encoding : str
+      The config file's encoding, defaults to :data:`default_encoding`.
+
+    Examples
+    --------
 
-    :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")
@@ -76,7 +100,7 @@ def get_val(name, section="DEFAULT", default=None, encoding=None):
         if encoding == None:
             encoding = default_encoding
         config = ConfigParser.ConfigParser()
-        f = codecs.open(path(), "r", encoding)
+        f = codecs.open(path(), 'r', encoding)
         config.readfp(f, path())
         f.close()
         try:
@@ -86,4 +110,5 @@ def get_val(name, section="DEFAULT", default=None, encoding=None):
     else:
         return default
 
-suite = doctest.DocTestSuite()
+if libbe.TESTING == True:
+    suite = doctest.DocTestSuite()
similarity index 59%
rename from libbe/mapfile.py
rename to libbe/storage/util/mapfile.py
index 4d696013a6c6a0fb591caa440c63233207a51f43..55863d7031a024f2cbc9fe6f3bb6b98de005c65c 100644 (file)
@@ -1,4 +1,5 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
 #                         W. Trevor King <wking@drexel.edu>
 #
 # This program is free software; you can redistribute it and/or modify
 # 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.
+"""Serializing and deserializing dictionaries of parameters.
+
+The serialized "mapfiles" should be clear, flat-text strings, and allow
+easy merging of independent/conflicting changes.
 """
 
 import errno
 import os.path
+import types
 import yaml
-import doctest
+
+import libbe
+if libbe.TESTING == True:
+    import doctest
 
 
 class IllegalKey(Exception):
@@ -35,34 +40,47 @@ class IllegalKey(Exception):
 class IllegalValue(Exception):
     def __init__(self, value):
         Exception.__init__(self, 'Illegal value "%s"' % value)
-        self.value = value 
+        self.value = value
+
+class InvalidMapfileContents(Exception):
+    def __init__(self, contents):
+        Exception.__init__(self, 'Invalid YAML contents')
+        self.contents = contents
 
 def generate(map):
     """Generate a YAML mapfile content string.
-    >>> generate({"q":"p"})
+
+    Examples
+    --------
+
+    >>> generate({'q':'p'})
     'q: p\\n\\n'
-    >>> generate({"q":u"Fran\u00e7ais"})
+    >>> generate({'q':u'Fran\u00e7ais'})
     'q: Fran\\xc3\\xa7ais\\n\\n'
-    >>> generate({"q":u"hello"})
+    >>> generate({'q':u'hello'})
     'q: hello\\n\\n'
-    >>> generate({"q=":"p"})
+    >>> generate({'q=':'p'})
     Traceback (most recent call last):
     IllegalKey: Illegal key "q="
-    >>> generate({"q:":"p"})
+    >>> generate({'q:':'p'})
     Traceback (most recent call last):
     IllegalKey: Illegal key "q:"
-    >>> generate({"q\\n":"p"})
+    >>> generate({'q\\n':'p'})
     Traceback (most recent call last):
     IllegalKey: Illegal key "q\\n"
-    >>> generate({"":"p"})
+    >>> generate({'':'p'})
     Traceback (most recent call last):
     IllegalKey: Illegal key ""
-    >>> generate({">q":"p"})
+    >>> generate({'>q':'p'})
     Traceback (most recent call last):
     IllegalKey: Illegal key ">q"
-    >>> generate({"q":"p\\n"})
+    >>> generate({'q':'p\\n'})
     Traceback (most recent call last):
     IllegalValue: Illegal value "p\\n"
+
+    See Also
+    --------
+    parse : inverse
     """
     keys = map.keys()
     keys.sort()
@@ -75,7 +93,7 @@ def generate(map):
             assert(len(key) > 0)
         except AssertionError:
             raise IllegalKey(unicode(key).encode('unicode_escape'))
-        if "\n" in map[key]:
+        if '\n' in map[key]:
             raise IllegalValue(unicode(map[key]).encode('unicode_escape'))
 
     lines = []
@@ -83,34 +101,46 @@ def generate(map):
         lines.append(yaml.safe_dump({key: map[key]},
                                     default_flow_style=False,
                                     allow_unicode=True))
-        lines.append("")
+        lines.append('')
     return '\n'.join(lines)
 
 def parse(contents):
-    """
-    Parse a YAML mapfile string.
+    """Parse a YAML mapfile string.
+
+    Examples
+    --------
+
     >>> parse('q: p\\n\\n')['q']
     'p'
     >>> parse('q: \\'p\\'\\n\\n')['q']
     'p'
-    >>> contents = generate({"a":"b", "c":"d", "e":"f"})
+    >>> contents = generate({'a':'b', 'c':'d', 'e':'f'})
     >>> dict = parse(contents)
-    >>> dict["a"]
+    >>> dict['a']
     'b'
-    >>> dict["c"]
+    >>> dict['c']
     'd'
-    >>> dict["e"]
+    >>> dict['e']
     'f'
-    """
-    return yaml.load(contents) or {}
+    >>> contents = generate({'q':u'Fran\u00e7ais'})
+    >>> dict = parse(contents)
+    >>> dict['q']
+    u'Fran\\xe7ais'
+    >>> dict = parse('a!')
+    Traceback (most recent call last):
+      ...
+    InvalidMapfileContents: Invalid YAML contents
 
-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)
+    See Also
+    --------
+    generate : inverse
 
-def map_load(vcs, path, allow_no_vcs=False):
-    contents = vcs.get_file_contents(path, allow_no_vcs=allow_no_vcs)
-    return parse(contents)
+    """
+    c = yaml.load(contents)
+    if type(c) == types.StringType:
+        raise InvalidMapfileContents(
+            'Unable to parse YAML (BE format missmatch?):\n\n%s' % contents)
+    return c or {}
 
-suite = doctest.DocTestSuite()
+if libbe.TESTING == True:
+    suite = doctest.DocTestSuite()
similarity index 52%
rename from libbe/properties.py
rename to libbe/storage/util/properties.py
index 09dd20e0417de099434d038f6ba254a2ba457323..b5681b1f199ee1c9905fcee931df8fbeb53194ed 100644 (file)
@@ -1,5 +1,6 @@
 # Bugs Everywhere - a distributed bugtracker
-# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
 #
 # 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
 # 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.
+"""Provides a series of useful decorators for defining various types
+of properties.
+
+For example usage, consider the unittests at the end of the module.
+
+Notes
+-----
+
+See `PEP 318` and Michele Simionato's `decorator documentation` for
+more information on decorators.
+
+.. _PEP 318: http://www.python.org/dev/peps/pep-0318/
+.. _decorator documentation: http://www.phyast.pitt.edu/~micheles/python/documentation.html
+
+See Also
+--------
+:mod:`libbe.storage.util.settings_object` : bundle properties into a convenient package
+
 """
 
 import copy
 import types
-import unittest
+
+import libbe
+if libbe.TESTING == True:
+    import unittest
 
 
 class ValueCheckError (ValueError):
@@ -299,12 +311,14 @@ def cached_property(generator, initVal=None, mutable=False):
         return funcs
     return decorator
 
-def primed_property(primer, initVal=None):
+def primed_property(primer, initVal=None, unprimeableVal=None):
     """
     Just like a cached_property, except that instead of returning a
-    new value and running fset to cache it, the primer performs some
+    new value and running fset to cache it, the primer attempts some
     background manipulation (e.g. loads data into instance.settings)
-    such that a _second_ pass through fget succeeds.
+    such that a _second_ pass through fget succeeds.  If the second
+    pass doesn't succeed (e.g. no readable storage), we give up and
+    return unprimeableVal.
 
     The 'cache' flag becomes a 'prime' flag, with priming taking place
     whenever ._<name>_prime is True, or is False or missing and
@@ -322,18 +336,19 @@ def primed_property(primer, initVal=None):
             if prime == True or (prime == False and value == initVal):
                 primer(self)
                 value = fget(self)
+                if prime == False and value == initVal:
+                    return unprimeableVal
             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).
+    """Call the function `hook` whenever a value different from the
+    current value is set.
+
     This is useful for saving changes to disk, etc.  This function is
-    called _after_ the new value has been stored, allowing you to
+    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
@@ -342,11 +357,19 @@ def change_hook_property(hook, mutable=False, default=None):
     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.
+    making external modifications, mutability won'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.
+
+    See :class:`testChangeHookMutableProperty` for an example of the
+    expected behavior.
+
+    Parameters
+    ----------
+    hook : fn
+      `hook(instance, old_value, new_value)`, where `instance` is a
+      reference to the class instance to which this property belongs.
     """
     def decorator(funcs):
         if hasattr(funcs, "__call__"):
@@ -382,257 +405,262 @@ def change_hook_property(hook, mutable=False, default=None):
         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:
+if libbe.TESTING == True:
+    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"] = self.primeVal
+                @Property
+                @primed_property(primer=prime, initVal=None, unprimeableVal=2)
+                @settings_property(name="PRIMED")
+                def x(): return {}
+                def __init__(self):
+                    self.settings={}
+                    self.primeVal = "initialized"
+            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
-            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:
+            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)
+            # test unprimableVal
             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)
-
+            t.primeVal = None
+            self.failUnless(t.x == 2, 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/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py
new file mode 100644 (file)
index 0000000..6e4da55
--- /dev/null
@@ -0,0 +1,617 @@
+# Bugs Everywhere - a distributed bugtracker
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+"""Provides :class:`SavedSettingsObject` implementing settings-dict
+based property storage.
+
+See Also
+--------
+:mod:`libbe.storage.util.properties` : underlying property definitions
+"""
+
+import libbe
+from properties import Property, doc_property, local_property, \
+    defaulting_property, checked_property, fn_checked_property, \
+    cached_property, primed_property, change_hook_property, \
+    settings_property
+if libbe.TESTING == True:
+    import doctest
+    import unittest
+
+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 (loaded)."
+    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.storage != None and self.storage.is_writeable():
+        self.save_settings()
+
+def prop_load_settings(self):
+    """The default action undertaken when an UNPRIMED property is
+    accessed.
+
+    Attempt to run `.load_settings()`, which calls
+    `._setup_saved_settings()` internally.  If `.storage` is
+    inaccessible, don't do anything.
+    """
+    if self.storage != None and self.storage.is_readable():
+        self.load_settings()
+
+# 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.
+
+    Examples
+    --------
+
+    >>> print setting_name_to_attr_name(None,"User-id")
+    user_id
+
+    See Also
+    --------
+    attr_name_to_setting_name : inverse
+    """
+    return name.lower().replace('-', '_')
+
+def attr_name_to_setting_name(self, name):
+    """Convert SavedSettingsObject attribute names to `.settings` dict
+    keys.
+
+    Examples:
+
+    >>> print attr_name_to_setting_name(None, "user_id")
+    User-id
+
+    See Also
+    --------
+    setting_name_to_attr_name : inverse
+    """
+    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.
+
+    The value stored in `.settings[name]` will be
+
+    * no value (or UNPRIMED) if the property has been neither set,
+      nor loaded as blank.
+    * EMPTY if the value has been loaded as blank.
+    * some value if the property has been either loaded or set.
+    """
+    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,
+                                      unprimeableVal=EMPTY)
+        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):
+    """Setup a framework for lazy saving and loading of `.settings`
+    properties.
+
+    This is useful for BE objects with saved properties
+    (e.g. :class:`~libbe.bugdir.BugDir`, :class:`~libbe.bug.Bug`,
+    :class:`~libbe.comment.Comment`).  For example usage, consider the
+    unittests at the end of the module.
+
+    See Also
+    --------
+    versioned_property, prop_save_settings, prop_load_settings
+    setting_name_to_attr_name, attr_name_to_setting_name
+    """
+    # 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.storage = None
+        self.settings = {}
+
+    def load_settings(self):
+        """Load the settings from disk."""
+        # Override.  Must call ._setup_saved_settings({}) with
+        # from-storage settings.
+        self._setup_saved_settings({})
+
+    def _setup_saved_settings(self, settings=None):
+        """
+        Sets up a settings dict loaded from storage.  Fills in
+        all missing settings entries with EMPTY.
+        """
+        if settings == None:
+            settings = {}
+        for property in self.settings_properties:
+            if property not in self.settings \
+                    or self.settings[property] == UNPRIMED:
+                if property in settings:
+                    self.settings[property] = settings[property]
+                else:
+                    self.settings[property] = EMPTY
+
+    def save_settings(self):
+        """Save the settings to 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):
+        """
+        In order to avoid overwriting unread on-disk data, make sure
+        we've loaded anything sitting on the disk.  In the current
+        implementation, all the settings are stored in a single file,
+        so we need to load _all_ the saved settings.  Another approach
+        would be per-setting saves, in which case you could skip this
+        step, since any setting changes would have forced that setting
+        load already.
+        """
+        settings = {}
+        for k in self.settings_properties: # force full load
+            if not k in self.settings or self.settings[k] == UNPRIMED:
+                value = getattr(
+                    self, self._setting_name_to_attr_name(k))
+        for k in self.settings_properties:
+            if k in self.settings and self.settings[k] != EMPTY:
+                settings[k] = self.settings[k]
+            elif 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)
+
+
+if libbe.TESTING == True:
+    import copy
+
+    class TestStorage (list):
+        def __init__(self):
+            list.__init__(self)
+            self.readable = True
+            self.writeable = True
+        def is_readable(self):
+            return self.readable
+        def is_writeable(self):
+            return self.writeable
+        
+    class TestObject (SavedSettingsObject):
+        def load_settings(self):
+            self.load_count += 1
+            if len(self.storage) == 0:
+                settings = {}
+            else:
+                settings = copy.deepcopy(self.storage[-1])
+            self._setup_saved_settings(settings)
+        def save_settings(self):
+            settings = self._get_saved_settings()
+            self.storage.append(copy.deepcopy(settings))
+        def __init__(self):
+            SavedSettingsObject.__init__(self)
+            self.load_count = 0
+            self.storage = TestStorage()
+
+    class SavedSettingsObjectTests(unittest.TestCase):
+        def testSimplePropertyDoc(self):
+            """Testing a minimal versioned property docstring"""
+            class Test (TestObject):
+                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 {}
+            expected = "A test property\n\nThis property defaults to None."
+            self.failUnless(Test.content_type.__doc__ == expected,
+                            Test.content_type.__doc__)
+        def testSimplePropertyFromMemory(self):
+            """Testing a minimal versioned property from memory"""
+            class Test (TestObject):
+                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 {}
+            t = Test()
+            self.failUnless(len(t.settings) == 0, len(t.settings))
+            # accessing t.content_type triggers the priming, but
+            # t.storage.is_readable() == False, so nothing happens.
+            t.storage.readable = False
+            self.failUnless(t.content_type == None, t.content_type)
+            self.failUnless(t.settings == {}, t.settings)
+            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 again, and
+            # now that t.storage.is_readable() == True, this fills out
+            # t.settings with EMPTY data.  At this point there should
+            # be one load and no saves.
+            t.storage.readable = True
+            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"])
+            self.failUnless(t.content_type == None, t.content_type)
+            self.failUnless(t.load_count == 1, t.load_count)
+            self.failUnless(len(t.storage) == 0, len(t.storage))
+            # an explicit call to load settings forces a reload,
+            # but nothing else changes.
+            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.content_type == None, t.content_type)
+            self.failUnless(t.load_count == 2, t.load_count)
+            self.failUnless(len(t.storage) == 0, len(t.storage))
+            # now we set a value
+            t.content_type = 5
+            self.failUnless(t.settings["Content-type"] == 5,
+                            t.settings["Content-type"])
+            self.failUnless(t.load_count == 2, t.load_count)
+            self.failUnless(len(t.storage) == 1, len(t.storage))
+            self.failUnless(t.storage == [{'Content-type':5}], t.storage)
+            # getting its value changes nothing
+            self.failUnless(t.content_type == 5, t.content_type)
+            self.failUnless(t.settings["Content-type"] == 5,
+                            t.settings["Content-type"])
+            self.failUnless(t.load_count == 2, t.load_count)
+            self.failUnless(len(t.storage) == 1, len(t.storage))
+            self.failUnless(t.storage == [{'Content-type':5}], t.storage)
+            # 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.load_count == 2, t.load_count)
+            self.failUnless(len(t.storage) == 2, len(t.storage))
+            self.failUnless(t.storage == [{'Content-type':5},
+                                          {'Content-type':'text/plain'}],
+                            t.storage)
+            # t._get_saved_settings() returns a dict of required or
+            # non-default values.
+            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["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"])
+            self.failUnless(t._get_saved_settings() == {},
+                            t._get_saved_settings())
+            self.failUnless(t.storage == [{'Content-type':5},
+                                          {'Content-type':'text/plain'},
+                                          {}],
+                            t.storage)
+        def testSimplePropertyFromStorage(self):
+            """Testing a minimal versioned property from storage"""
+            class Test (TestObject):
+                settings_properties = []
+                required_saved_properties = []
+                @versioned_property(
+                    name="prop-a",
+                    doc="A test property",
+                    settings_properties=settings_properties,
+                    required_saved_properties=required_saved_properties)
+                def prop_a(): return {}
+                @versioned_property(
+                    name="prop-b",
+                    doc="Another test property",
+                    settings_properties=settings_properties,
+                    required_saved_properties=required_saved_properties)
+                def prop_b(): return {}
+            t = Test()
+            t.storage.append({'prop-a':'saved'})
+            # setting prop-b forces a load (to check for changes),
+            # which also pulls in prop-a.
+            t.prop_b = 'new-b'
+            settings = {'prop-b':'new-b', 'prop-a':'saved'}
+            self.failUnless(t.settings == settings, t.settings)
+            self.failUnless(t._get_saved_settings() == settings,
+                            t._get_saved_settings())
+            # test that _get_saved_settings() works even when settings
+            # were _not_ loaded beforehand
+            t = Test()
+            t.storage.append({'prop-a':'saved'})
+            settings ={'prop-a':'saved'}
+            self.failUnless(t.settings == {}, t.settings)
+            self.failUnless(t._get_saved_settings() == settings,
+                            t._get_saved_settings())
+        def testSimplePropertySetStorageSave(self):
+            """Set a property, then attach storage and save"""
+            class Test (TestObject):
+                settings_properties = []
+                required_saved_properties = []
+                @versioned_property(
+                    name="prop-a",
+                    doc="A test property",
+                    settings_properties=settings_properties,
+                    required_saved_properties=required_saved_properties)
+                def prop_a(): return {}
+                @versioned_property(
+                    name="prop-b",
+                    doc="Another test property",
+                    settings_properties=settings_properties,
+                    required_saved_properties=required_saved_properties)
+                def prop_b(): return {}
+            t = Test()
+            storage = t.storage
+            t.storage = None
+            t.prop_a = 'text/html'
+            t.storage = storage
+            t.save_settings()
+            self.failUnless(t.prop_a == 'text/html', t.prop_a)
+            self.failUnless(t.settings == {'prop-a':'text/html',
+                                           'prop-b':EMPTY},
+                            t.settings)
+            self.failUnless(t.load_count == 1, t.load_count)
+            self.failUnless(len(t.storage) == 1, len(t.storage))
+            self.failUnless(t.storage == [{'prop-a':'text/html'}],
+                            t.storage)
+        def testDefaultingProperty(self):
+            """Testing a defaulting versioned property"""
+            class Test (TestObject):
+                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 {}
+            t = Test()
+            self.failUnless(t.settings == {}, t.settings)
+            self.failUnless(t.content_type == "text/plain", t.content_type)
+            self.failUnless(t.settings == {"Content-type":EMPTY},
+                            t.settings)
+            self.failUnless(t.load_count == 1, t.load_count)
+            self.failUnless(len(t.storage) == 0, len(t.storage))
+            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)
+            self.failUnless(t.load_count == 1, t.load_count)
+            self.failUnless(len(t.storage) == 1, len(t.storage))
+            self.failUnless(t.storage == [{'Content-type':'text/html'}],
+                            t.storage)
+            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 (TestObject):
+                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 {}
+            t = Test()
+            self.failUnless(t.settings == {}, t.settings)
+            self.failUnless(t.content_type == "text/plain", t.content_type)
+            self.failUnless(t.settings == {"Content-type":EMPTY},
+                            t.settings)
+            self.failUnless(t.load_count == 1, t.load_count)
+            self.failUnless(len(t.storage) == 0, len(t.storage))
+            self.failUnless(t._get_saved_settings() == \
+                                {"Content-type":"text/plain"},
+                            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)
+            self.failUnless(t.load_count == 1, t.load_count)
+            self.failUnless(len(t.storage) == 1, len(t.storage))
+            self.failUnless(t.storage == [{'Content-type':'text/html'}],
+                            t.storage)
+            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 (TestObject):
+                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 {}
+            t = Test()
+            self.failUnless(t._get_saved_settings() == \
+                                {"Content-type":"text/plain"},
+                            t._get_saved_settings())
+            self.failUnless(t.load_count == 1, t.load_count)
+            self.failUnless(len(t.storage) == 0, len(t.storage))
+            t.content_type = "text/html"
+            self.failUnless(t._get_saved_settings() == \
+                                {"Content-type":"text/html"},
+                            t._get_saved_settings())
+            self.failUnless(t.load_count == 1, t.load_count)
+            self.failUnless(len(t.storage) == 1, len(t.storage))
+            self.failUnless(t.storage == [{'Content-type':'text/html'}],
+                            t.storage)
+        def testMutableChangeHookedProperty(self):
+            """Testing a mutable change-hooked property"""
+            class Test (TestObject):
+                settings_properties = []
+                required_saved_properties = []
+                @versioned_property(
+                    name="List-type",
+                    doc="A test property",
+                    mutable=True,
+                    change_hook=prop_save_settings,
+                    settings_properties=settings_properties,
+                    required_saved_properties=required_saved_properties)
+                def list_type(): return {}
+            t = Test()
+            self.failUnless(len(t.storage) == 0, len(t.storage))
+            self.failUnless(t.list_type == None, t.list_type)
+            self.failUnless(len(t.storage) == 0, len(t.storage))
+            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(len(t.storage) == 1, len(t.storage))
+            self.failUnless(t.storage == [{'List-type':[]}],
+                            t.storage)
+            t.list_type.append(5) # external modification not detected yet
+            self.failUnless(len(t.storage) == 1, len(t.storage))
+            self.failUnless(t.storage == [{'List-type':[]}],
+                            t.storage)
+            self.failUnless(t.settings["List-type"] == [5],
+                            t.settings["List-type"])
+            self.failUnless(t.list_type == [5], t.list_type)# get triggers save
+            self.failUnless(len(t.storage) == 2, len(t.storage))
+            self.failUnless(t.storage == [{'List-type':[]},
+                                          {'List-type':[5]}],
+                            t.storage)
+
+    unitsuite = unittest.TestLoader().loadTestsFromTestCase( \
+        SavedSettingsObjectTests)
+    suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
diff --git a/libbe/storage/util/upgrade.py b/libbe/storage/util/upgrade.py
new file mode 100644 (file)
index 0000000..f3c4912
--- /dev/null
@@ -0,0 +1,331 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# 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 BE storage formats.
+"""
+
+import codecs
+import os, os.path
+import sys
+
+import libbe
+import libbe.bug
+import libbe.storage.util.mapfile as mapfile
+from libbe.storage import STORAGE_VERSIONS, STORAGE_VERSION
+#import libbe.storage.vcs # delay import to avoid cyclic dependency
+import libbe.ui.util.editor
+import libbe.util
+import libbe.util.encoding as encoding
+import libbe.util.id
+
+
+class Upgrader (object):
+    "Class for converting between different on-disk BE storage formats."
+    initial_version = None
+    final_version = None
+    def __init__(self, repo):
+        import libbe.storage.vcs
+
+        self.repo = repo
+        vcs_name = self._get_vcs_name()
+        if vcs_name == None:
+            vcs_name = 'None'
+        self.vcs = libbe.storage.vcs.vcs_by_name(vcs_name)
+        self.vcs.repo = self.repo
+        self.vcs.root()
+
+    def get_path(self, *args):
+        """
+        Return the absolute path using args relative to .be.
+        """
+        dir = os.path.join(self.repo, '.be')
+        if len(args) == 0:
+            return dir
+        return os.path.join(dir, *args)
+
+    def _get_vcs_name(self):
+        return None
+
+    def check_initial_version(self):
+        path = self.get_path('version')
+        version = encoding.get_file_contents(path, decode=True).rstrip('\n')
+        assert version == self.initial_version, '%s: %s' % (path, version)
+
+    def set_version(self):
+        path = self.get_path('version')
+        encoding.set_file_contents(path, self.final_version+'\n')
+        self.vcs._vcs_update(path)
+
+    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 _get_vcs_name(self):
+        path = self.get_path('settings')
+        settings = encoding.get_file_contents(path)
+        for line in settings.splitlines(False):
+            fields = line.split('=')
+            if len(fields) == 2 and fields[0] == 'rcs_name':
+                return fields[1]
+        return None
+            
+    def _upgrade_mapfile(self, path):
+        contents = encoding.get_file_contents(path, decode=True)
+        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)
+            contents = mapfile.generate(map)
+            encoding.set_file_contents(path, contents)
+            self.vcs._vcs_update(path)
+
+    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.parse(
+                    encoding.get_file_contents(path))
+                if 'From' in settings:
+                    settings['Author'] = settings.pop('From')
+                    encoding.set_file_contents(
+                        path, mapfile.generate(settings))
+                    self.vcs._vcs_update(path)
+
+
+class Upgrade_1_1_to_1_2 (Upgrader):
+    initial_version = "Bugs Everywhere Directory v1.1"
+    final_version = "Bugs Everywhere Directory v1.2"
+    def _get_vcs_name(self):
+        path = self.get_path('settings')
+        settings = mapfile.parse(encoding.get_file_contents(path))
+        if 'rcs_name' in settings:
+            return settings['rcs_name']
+        return None
+            
+    def _upgrade(self):
+        """
+        BugDir settings field "rcs_name" -> "vcs_name".
+        """
+        path = self.get_path('settings')
+        settings = mapfile.parse(encoding.get_file_contents(path))
+        if 'rcs_name' in settings:
+            settings['vcs_name'] = settings.pop('rcs_name')
+            encoding.set_file_contents(path, mapfile.generate(settings))
+            self.vcs._vcs_update(path)
+
+class Upgrade_1_2_to_1_3 (Upgrader):
+    initial_version = "Bugs Everywhere Directory v1.2"
+    final_version = "Bugs Everywhere Directory v1.3"
+    def __init__(self, *args, **kwargs):
+        Upgrader.__init__(self, *args, **kwargs)
+        self._targets = {} # key: target text,value: new target bug
+
+    def _get_vcs_name(self):
+        path = self.get_path('settings')
+        settings = mapfile.parse(encoding.get_file_contents(path))
+        if 'vcs_name' in settings:
+            return settings['vcs_name']
+        return None
+
+    def _save_bug_settings(self, bug):
+        # The target bugs don't have comments
+        path = self.get_path('bugs', bug.uuid, 'values')
+        if not os.path.exists(path):
+            self.vcs._add_path(path, directory=False)
+        path = self.get_path('bugs', bug.uuid, 'values')
+        mf = mapfile.generate(bug._get_saved_settings())
+        encoding.set_file_contents(path, mf)
+        self.vcs._vcs_update(path)
+
+    def _target_bug(self, target_text):
+        if target_text not in self._targets:
+            bug = libbe.bug.Bug(summary=target_text)
+            bug.severity = 'target'
+            self._targets[target_text] = bug
+        return self._targets[target_text]
+
+    def _upgrade_bugdir_mapfile(self):
+        path = self.get_path('settings')
+        mf = encoding.get_file_contents(path)
+        if mf == libbe.util.InvalidObject:
+            return # settings file does not exist
+        settings = mapfile.parse(mf)
+        if 'target' in settings:
+            settings['target'] = self._target_bug(settings['target']).uuid
+            mf = mapfile.generate(settings)
+            encoding.set_file_contents(path, mf)
+            self.vcs._vcs_update(path)
+
+    def _upgrade_bug_mapfile(self, bug_uuid):
+        import libbe.command.depend as dep
+        path = self.get_path('bugs', bug_uuid, 'values')
+        mf = encoding.get_file_contents(path)
+        if mf == libbe.util.InvalidObject:
+            return # settings file does not exist
+        settings = mapfile.parse(mf)
+        if 'target' in settings:
+            target_bug = self._target_bug(settings['target'])
+
+            blocked_by_string = '%s%s' % (dep.BLOCKED_BY_TAG, bug_uuid)
+            dep._add_remove_extra_string(target_bug, blocked_by_string, add=True)
+            blocks_string = dep._generate_blocks_string(target_bug)
+            estrs = settings.get('extra_strings', [])
+            estrs.append(blocks_string)
+            settings['extra_strings'] = sorted(estrs)
+
+            settings.pop('target')
+            mf = mapfile.generate(settings)
+            encoding.set_file_contents(path, mf)
+            self.vcs._vcs_update(path)
+
+    def _upgrade(self):
+        """
+        Bug value field "target" -> target bugs.
+        Bugdir value field "target" -> pointer to current target bug.
+        """
+        for bug_uuid in os.listdir(self.get_path('bugs')):
+            self._upgrade_bug_mapfile(bug_uuid)
+        self._upgrade_bugdir_mapfile()
+        for bug in self._targets.values():
+            self._save_bug_settings(bug)
+
+class Upgrade_1_3_to_1_4 (Upgrader):
+    initial_version = "Bugs Everywhere Directory v1.3"
+    final_version = "Bugs Everywhere Directory v1.4"
+    def _get_vcs_name(self):
+        path = self.get_path('settings')
+        settings = mapfile.parse(encoding.get_file_contents(path))
+        if 'vcs_name' in settings:
+            return settings['vcs_name']
+        return None
+
+    def _upgrade(self):
+        """
+        add new directory "./be/BUGDIR-UUID"
+        "./be/bugs" -> "./be/BUGDIR-UUID/bugs"
+        "./be/settings" -> "./be/BUGDIR-UUID/settings"
+        """
+        self.repo = os.path.abspath(self.repo)
+        basenames = [p for p in os.listdir(self.get_path())]
+        if not 'bugs' in basenames and not 'settings' in basenames \
+                and len([p for p in basenames if len(p)==36]) == 1:
+            return # the user has upgraded the directory.
+        basenames = [p for p in basenames if p in ['bugs','settings']]
+        uuid = libbe.util.id.uuid_gen()
+        add = [self.get_path(uuid)]
+        move = [(self.get_path(p), self.get_path(uuid, p)) for p in basenames]
+        msg = ['Upgrading BE directory version v1.3 to v1.4',
+               '',
+               "Because BE's VCS drivers don't support 'move',",
+               'please make the following changes with your VCS',
+               'and re-run BE.  Note that you can choose a different',
+               'bugdir UUID to preserve uniformity across branches',
+               'of a distributed repository.'
+               '',
+               'add',
+               '  ' + '\n  '.join(add),
+               'move',
+               '  ' + '\n  '.join(['%s %s' % (a,b) for a,b in move]),
+               ]
+        self.vcs._cached_path_id.destroy()
+        raise Exception('Need user assistance\n%s' % '\n'.join(msg))
+
+
+upgraders = [Upgrade_1_0_to_1_1,
+             Upgrade_1_1_to_1_2,
+             Upgrade_1_2_to_1_3,
+             Upgrade_1_3_to_1_4]
+upgrade_classes = {}
+for upgrader in upgraders:
+    upgrade_classes[(upgrader.initial_version,upgrader.final_version)]=upgrader
+
+def upgrade(path, current_version,
+            target_version=STORAGE_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 STORAGE_VERSIONS:
+        raise NotImplementedError, \
+            "Cannot handle version '%s' yet." % current_version
+    if target_version not in STORAGE_VERSIONS:
+        raise NotImplementedError, \
+            "Cannot handle version '%s' yet." % current_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 = STORAGE_VERSIONS.index(current_version)
+        while True:
+            version_a = STORAGE_VERSIONS[i]
+            version_b = STORAGE_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
diff --git a/libbe/storage/vcs/__init__.py b/libbe/storage/vcs/__init__.py
new file mode 100644 (file)
index 0000000..552d43e
--- /dev/null
@@ -0,0 +1,41 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# 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 Version Controlled System (VCS)-based
+:class:`~libbe.storage.base.Storage` and
+:class:`~libbe.storage.base.VersionedStorage` implementations.
+
+There is a base class (:class:`~libbe.storage.vcs.VCS`) translating 
+Storage language to VCS language, and a number of `VCS` implementations:
+
+* :class:`~libbe.storage.vcs.arch.Arch`
+* :class:`~libbe.storage.vcs.bzr.Bzr`
+* :class:`~libbe.storage.vcs.darcs.Darcs`
+* :class:`~libbe.storage.vcs.git.Git`
+* :class:`~libbe.storage.vcs.hg.Hg`
+
+The base `VCS` class also serves as a filesystem Storage backend (not
+versioning) in the event that a user has no VCS installed.
+"""
+
+import base
+
+set_preferred_vcs = base.set_preferred_vcs
+vcs_by_name = base.vcs_by_name
+detect_vcs = base.detect_vcs
+installed_vcs = base.installed_vcs
+
+__all__ = [set_preferred_vcs, vcs_by_name, detect_vcs, installed_vcs]
diff --git a/libbe/storage/vcs/arch.py b/libbe/storage/vcs/arch.py
new file mode 100644 (file)
index 0000000..3a50414
--- /dev/null
@@ -0,0 +1,441 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Ben Finney <benf@cybersource.com.au>
+#                         Gianluca Montecchi <gian@grys.it>
+#                         James Rowe <jnrowe@ukfsn.org>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+.. _Arch: http://www.gnu.org/software/gnu-arch/
+"""
+
+import codecs
+import os
+import os.path
+import re
+import shutil
+import sys
+import time # work around http://mercurial.selenic.com/bts/issue618
+
+import libbe
+import libbe.ui.util.user
+import libbe.storage.util.config
+from libbe.util.id import uuid_gen
+from libbe.util.subproc import CommandError
+import base
+
+if libbe.TESTING == True:
+    import unittest
+    import doctest
+
+
+class CantAddFile(Exception):
+    def __init__(self, file):
+        self.file = file
+        Exception.__init__(self, "Can't automatically add file %s" % file)
+
+DEFAULT_CLIENT = 'tla'
+
+client = libbe.storage.util.config.get_val(
+    'arch_client', default=DEFAULT_CLIENT)
+
+def new():
+    return Arch()
+
+class Arch(base.VCS):
+    """:class:`base.VCS` implementation for GNU Arch.
+    """
+    name = 'arch'
+    client = client
+    _archive_name = None
+    _archive_dir = None
+    _tmp_archive = False
+    _project_name = None
+    _tmp_project = False
+    _arch_paramdir = os.path.expanduser('~/.arch-params')
+
+    def __init__(self, *args, **kwargs):
+        base.VCS.__init__(self, *args, **kwargs)
+        self.versioned = True
+        self.interspersed_vcs_files = True
+        self.paranoid = False
+        self.__updated = [] # work around http://mercurial.selenic.com/bts/issue618
+
+    def _vcs_version(self):
+        status,output,error = self._u_invoke_client('--version')
+        version = '\n'.join(output.splitlines()[:2])
+        return version
+
+    def _vcs_detect(self, path):
+        """Detect whether a directory is revision-controlled using Arch"""
+        if self._u_search_parent_directories(path, '{arch}') != None :
+            libbe.storage.util.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::
+
+            destroy->_vcs_destroy->_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 = libbe.ui.util.user.parse_user_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, cwd=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
+          destroy->_vcs_destroy->_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,
+                            cwd=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):
+        """Adjust `Arch naming conventions`_ so ``.be`` is considered source
+        code.
+
+        By default, Arch restricts source code filenames to::
+
+            ^[_=a-zA-Z0-9].*$
+
+        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
+
+        .. _Arch naming conventions:
+          http://regexps.srparish.net/tutorial-tla/naming-conventions.html
+        """
+        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,
+                            cwd=path)
+        self._adjust_naming_conventions(path)
+        self._invoke_client('import', '--summary', 'Began versioning',
+                            cwd=path)
+
+    def _vcs_destroy(self):
+        if self._tmp_project == True:
+            self._remove_project()
+        if self._tmp_archive == True:
+            self._remove_archive()
+        vcs_dir = os.path.join(self.repo, '{arch}')
+        if os.path.exists(vcs_dir):
+            shutil.rmtree(vcs_dir)
+        self._archive_name = None
+
+    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', cwd=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_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.repo)
+        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.repo)
+        if os.path.realpath(path) not in self._list_added(self.repo):
+            raise CantAddFile(path)
+
+    def _vcs_remove(self, path):
+        if self._vcs_is_versioned(path):
+            self._u_invoke_client('delete-id', path)
+        arch_ids = os.path.join(self.repo, path, '.arch-ids')
+        if os.path.exists(arch_ids):
+            shutil.rmtree(arch_ids)
+
+    def _vcs_update(self, path):
+        self.__updated.append(path) # work around http://mercurial.selenic.com/bts/issue618
+
+    def _vcs_is_versioned(self, path):
+        if '.arch-ids' in path:
+            return False
+        return True
+
+    def _vcs_get_file_contents(self, path, revision=None):
+        if revision == None:
+            return base.VCS._vcs_get_file_contents(self, path, revision)
+        else:
+            relpath = self._file_find(path, revision, relpath=True)
+            return base.VCS._vcs_get_file_contents(self, relpath)
+
+    def _file_find(self, path, revision, relpath=False):
+        try:
+            status,output,error = \
+                self._invoke_client(
+                'file-find', '--unescaped', path, revision)
+            path = output.rstrip('\n').splitlines()[-1]
+        except CommandError, e:
+            if e.status == 2 \
+                    and 'illegally formed changeset index' in e.stderr:
+                raise NotImplementedError(
+"""Outstanding tla bug, see
+  https://bugs.launchpad.net/ubuntu/+source/tla/+bug/513472
+""")
+            raise
+        if relpath == True:
+            return path
+        return os.path.abspath(os.path.join(self.repo, path))
+
+    def _vcs_path(self, id, revision):
+        return self._u_find_id(id, revision)
+
+    def _vcs_isdir(self, path, revision):
+        abspath = self._file_find(path, revision)
+        return os.path.isdir(abspath)
+
+    def _vcs_listdir(self, path, revision):
+        abspath = self._file_find(path, revision)
+        return [p for p in os.listdir(abspath) if self._vcs_is_versioned(p)]
+
+    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:
+                # work around http://mercurial.selenic.com/bts/issue618
+                time.sleep(1)
+                for path in self.__updated:
+                    os.utime(os.path.join(self.repo, path), None)
+                self.__updated = []
+                status,output,error = self._u_invoke_client('changes',expect=(0,1))
+                if status == 0:
+                # end work around
+                    raise base.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:
+            if index > 0:
+                log = logs[index-1]
+            elif index < 0:
+                log = logs[index]
+            else:
+                return None
+        except IndexError:
+            return None
+        return '%s--%s' % (self._archive_project_name(), log)
+
+    def _diff(self, revision):
+        status,output,error = self._u_invoke_client(
+            'diff', '--summary', '--unescaped', revision, expect=(0,1))
+        return output
+    
+    def _parse_diff(self, diff_text):
+        """
+        Example diff text:
+        
+        * local directory is at ...
+        * build pristine tree for ...
+        * from import revision: ...
+        * patching for revision: ...
+        * comparing to ...
+        D  .be/dir/bugs/.arch-ids/moved.id
+        D  .be/dir/bugs/.arch-ids/removed.id
+        D  .be/dir/bugs/moved
+        D  .be/dir/bugs/removed
+        A  .be/dir/bugs/.arch-ids/moved2.id
+        A  .be/dir/bugs/.arch-ids/new.id
+        A  .be/dir/bugs/moved2
+        A  .be/dir/bugs/new
+        A  {arch}/bugs-everywhere/bugs-everywhere--mainline/...
+        M  .be/dir/bugs/modified
+        """
+        new = []
+        modified = []
+        removed = []
+        lines = diff_text.splitlines()
+        for i,line in enumerate(lines):
+            if line.startswith('* ') or '/.arch-ids/' in line:
+                continue
+            change,file = line.split('  ',1)
+            if  file.startswith('{arch}/'):
+                continue
+            if change == 'A':
+                new.append(file)
+            elif change == 'M':
+                modified.append(file)
+            elif change == 'D':
+                removed.append(file)
+        return (new,modified,removed)
+
+    def _vcs_changed(self, revision):
+        return self._parse_diff(self._diff(revision))
+
+\f
+if libbe.TESTING == True:
+    base.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/storage/vcs/base.py b/libbe/storage/vcs/base.py
new file mode 100644 (file)
index 0000000..d85c94d
--- /dev/null
@@ -0,0 +1,1127 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Alexander Belchenko <bialix@ukr.net>
+#                         Ben Finney <benf@cybersource.com.au>
+#                         Chris Ball <cjb@laptop.org>
+#                         Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# 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 :class:`VCS` (Version Control System) class, which
+should be subclassed by other Version Control System backends.  The
+base class implements a "do not version" VCS.
+"""
+
+import codecs
+import os
+import os.path
+import re
+import shutil
+import sys
+import tempfile
+import types
+
+import libbe
+import libbe.storage
+import libbe.storage.base
+import libbe.util.encoding
+from libbe.storage.base import EmptyCommit, InvalidRevision, InvalidID
+from libbe.util.utility import Dir, search_parent_directories
+from libbe.util.subproc import CommandError, invoke
+from libbe.util.plugin import import_by_name
+import libbe.storage.util.upgrade as upgrade
+
+if libbe.TESTING == True:
+    import unittest
+    import doctest
+
+    import libbe.ui.util.user
+
+VCS_ORDER = ['arch', 'bzr', 'darcs', 'git', 'hg']
+"""List VCS modules in order of preference.
+
+Don't list this module, it is implicitly last.
+"""
+
+def set_preferred_vcs(name):
+    """Manipulate :data:`VCS_ORDER` to place `name` first.
+
+    This is primarily indended for testing purposes.
+    """
+    global VCS_ORDER
+    assert name in VCS_ORDER, \
+        'unrecognized VCS %s not in\n  %s' % (name, VCS_ORDER)
+    VCS_ORDER.remove(name)
+    VCS_ORDER.insert(0, name)
+
+def _get_matching_vcs(matchfn):
+    """Return the first module for which matchfn(VCS_instance) is True.
+
+    Searches in :data:`VCS_ORDER`.
+    """
+    for submodname in VCS_ORDER:
+        module = import_by_name('libbe.storage.vcs.%s' % submodname)
+        vcs = module.new()
+        if matchfn(vcs) == True:
+            return vcs
+    return VCS()
+
+def vcs_by_name(vcs_name):
+    """Return the module for the VCS with the given name.
+
+    Searches in :data:`VCS_ORDER`.
+    """
+    if vcs_name == VCS.name:
+        return new()
+    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.
+
+    Searches in :data:`VCS_ORDER`.
+    """
+    return _get_matching_vcs(lambda vcs: vcs._detect(dir))
+
+def installed_vcs():
+    """Return an instance of an installed VCS.
+
+    Searches in :data:`VCS_ORDER`.
+    """
+    return _get_matching_vcs(lambda vcs: vcs.installed())
+
+
+class VCSNotRooted (libbe.storage.base.ConnectionError):
+    def __init__(self, vcs):
+        msg = 'VCS not rooted'
+        libbe.storage.base.ConnectionError.__init__(self, msg)
+        self.vcs = vcs
+
+class VCSUnableToRoot (libbe.storage.base.ConnectionError):
+    def __init__(self, vcs):
+        msg = 'VCS unable to root'
+        libbe.storage.base.ConnectionError.__init__(self, msg)
+        self.vcs = vcs
+
+class InvalidPath (InvalidID):
+    def __init__(self, path, root, msg=None, **kwargs):
+        if msg == None:
+            msg = 'Path "%s" not in root "%s"' % (path, root)
+        InvalidID.__init__(self, msg=msg, **kwargs)
+        self.path = path
+        self.root = root
+
+class SpacerCollision (InvalidPath):
+    def __init__(self, path, spacer):
+        msg = 'Path "%s" collides with spacer directory "%s"' % (path, spacer)
+        InvalidPath.__init__(self, path, root=None, msg=msg)
+        self.spacer = spacer
+
+class NoSuchFile (InvalidID):
+    def __init__(self, pathname, root='.'):
+        path = os.path.abspath(os.path.join(root, pathname))
+        InvalidID.__init__(self, 'No such file: %s' % path)
+
+
+class CachedPathID (object):
+    """Cache Storage ID <-> path policy.
+    Paths generated following::
+
+       .../.be/BUGDIR/bugs/BUG/comments/COMMENT
+          ^-- root path
+
+    See :mod:`libbe.util.id` for a discussion of ID formats.
+
+    Examples
+    --------
+
+    >>> dir = Dir()
+    >>> os.mkdir(os.path.join(dir.path, '.be'))
+    >>> os.mkdir(os.path.join(dir.path, '.be', 'abc'))
+    >>> os.mkdir(os.path.join(dir.path, '.be', 'abc', 'bugs'))
+    >>> os.mkdir(os.path.join(dir.path, '.be', 'abc', 'bugs', '123'))
+    >>> os.mkdir(os.path.join(dir.path, '.be', 'abc', 'bugs', '123', 'comments'))
+    >>> os.mkdir(os.path.join(dir.path, '.be', 'abc', 'bugs', '123', 'comments', 'def'))
+    >>> os.mkdir(os.path.join(dir.path, '.be', 'abc', 'bugs', '456'))
+    >>> file(os.path.join(dir.path, '.be', 'abc', 'values'),
+    ...      'w').close()
+    >>> file(os.path.join(dir.path, '.be', 'abc', 'bugs', '123', 'values'),
+    ...      'w').close()
+    >>> file(os.path.join(dir.path, '.be', 'abc', 'bugs', '123', 'comments', 'def', 'values'),
+    ...      'w').close()
+    >>> c = CachedPathID()
+    >>> c.root(dir.path)
+    >>> c.id(os.path.join(dir.path, '.be', 'abc', 'bugs', '123', 'comments', 'def', 'values'))
+    'def/values'
+    >>> c.init()
+    >>> sorted(os.listdir(os.path.join(c._root, '.be')))
+    ['abc', 'id-cache']
+    >>> c.connect()
+    >>> c.path('123/values') # doctest: +ELLIPSIS
+    u'.../.be/abc/bugs/123/values'
+    >>> c.disconnect()
+    >>> c.destroy()
+    >>> sorted(os.listdir(os.path.join(c._root, '.be')))
+    ['abc']
+    >>> c.connect() # demonstrate auto init
+    >>> sorted(os.listdir(os.path.join(c._root, '.be')))
+    ['abc', 'id-cache']
+    >>> c.add_id(u'xyz', parent=None) # doctest: +ELLIPSIS
+    u'.../.be/xyz'
+    >>> c.add_id('xyz/def', parent='xyz') # doctest: +ELLIPSIS
+    u'.../.be/xyz/def'
+    >>> c.add_id('qrs', parent='123') # doctest: +ELLIPSIS
+    u'.../.be/abc/bugs/123/comments/qrs'
+    >>> c.disconnect()
+    >>> c.connect()
+    >>> c.path('qrs') # doctest: +ELLIPSIS
+    u'.../.be/abc/bugs/123/comments/qrs'
+    >>> c.remove_id('qrs')
+    >>> c.path('qrs')
+    Traceback (most recent call last):
+      ...
+    InvalidID: qrs in revision None
+    >>> c.disconnect()
+    >>> c.destroy()
+    >>> dir.cleanup()
+    """
+    def __init__(self, encoding=None):
+        self.encoding = libbe.util.encoding.get_filesystem_encoding()
+        self._spacer_dirs = ['.be', 'bugs', 'comments']
+
+    def root(self, path):
+        self._root = os.path.abspath(path).rstrip(os.path.sep)
+        self._cache_path = os.path.join(
+            self._root, self._spacer_dirs[0], 'id-cache')
+
+    def init(self, verbose=True, cache=None):
+        """Create cache file for an existing .be directory.
+
+        The file contains multiple lines of the form::
+
+            UUID\tPATH
+        """
+        if cache == None:
+            self._cache = {}
+        else:
+            self._cache = cache
+        spaced_root = os.path.join(self._root, self._spacer_dirs[0])
+        for dirpath, dirnames, filenames in os.walk(spaced_root):
+            if dirpath == spaced_root:
+                continue
+            try:
+                id = self.id(dirpath)
+                relpath = dirpath[len(self._root)+1:]
+                if id.count('/') == 0:
+                    if verbose == True and id in self._cache:
+                        print >> sys.stderr, 'Multiple paths for %s: \n  %s\n  %s' % (id, self._cache[id], relpath)
+                    self._cache[id] = relpath
+            except InvalidPath:
+                pass
+        if self._cache != cache:
+            self._changed = True
+        if cache == None:
+            self.disconnect()
+
+    def destroy(self):
+        if os.path.exists(self._cache_path):
+            os.remove(self._cache_path)
+
+    def connect(self):
+        if not os.path.exists(self._cache_path):
+            try:
+                self.init()
+            except IOError:
+                raise libbe.storage.base.ConnectionError
+        self._cache = {} # key: uuid, value: path
+        self._changed = False
+        f = codecs.open(self._cache_path, 'r', self.encoding)
+        for line in f:
+            fields = line.rstrip('\n').split('\t')
+            self._cache[fields[0]] = fields[1]
+        f.close()
+
+    def disconnect(self):
+        if self._changed == True:
+            f = codecs.open(self._cache_path, 'w', self.encoding)
+            for uuid,path in self._cache.items():
+                f.write('%s\t%s\n' % (uuid, path))
+            f.close()
+        self._cache = {}
+
+    def path(self, id, relpath=False):
+        fields = id.split('/', 1)
+        uuid = fields[0]
+        if len(fields) == 1:
+            extra = []
+        else:
+            extra = fields[1:]
+        if uuid not in self._cache:
+            self.init(verbose=False, cache=self._cache)
+            if uuid not in self._cache:
+                raise InvalidID(uuid)
+        if relpath == True:
+            return os.path.join(self._cache[uuid], *extra)
+        return os.path.join(self._root, self._cache[uuid], *extra)
+
+    def add_id(self, id, parent=None):
+        if id.count('/') > 0:
+            # not a UUID-level path
+            assert id.startswith(parent), \
+                'Strange ID: "%s" should start with "%s"' % (id, parent)
+            path = self.path(id)
+        elif id in self._cache:
+            # already added
+            path = self.path(id)
+        else:
+            if parent == None:
+                parent_path = ''
+                spacer = self._spacer_dirs[0]
+            else:
+                assert parent.count('/') == 0, \
+                    'Strange parent ID: "%s" should be UUID' % parent
+                parent_path = self.path(parent, relpath=True)
+                parent_spacer = parent_path.split(os.path.sep)[-2]
+                i = self._spacer_dirs.index(parent_spacer)
+                spacer = self._spacer_dirs[i+1]
+            path = os.path.join(parent_path, spacer, id)
+            self._cache[id] = path
+            self._changed = True
+            path = os.path.join(self._root, path)
+        return path
+
+    def remove_id(self, id):
+        if id.count('/') > 0:
+            return # not a UUID-level path
+        self._cache.pop(id)
+        self._changed = True
+
+    def id(self, path):
+        path = os.path.join(self._root, path)
+        if not path.startswith(self._root + os.path.sep):
+            raise InvalidPath(path, self._root)
+        path = path[len(self._root)+1:]
+        orig_path = path
+        if not path.startswith(self._spacer_dirs[0] + os.path.sep):
+            raise InvalidPath(path, self._spacer_dirs[0])
+        for spacer in self._spacer_dirs:
+            if not path.startswith(spacer + os.path.sep):
+                break
+            id = path[len(spacer)+1:]
+            fields = path[len(spacer)+1:].split(os.path.sep,1)
+            if len(fields) == 1:
+                break
+            path = fields[1]
+        for spacer in self._spacer_dirs:
+            if id.endswith(os.path.sep + spacer):
+                raise SpacerCollision(orig_path, spacer)
+        if os.path.sep != '/':
+            id = id.replace(os.path.sep, '/')
+        return id
+
+
+def new():
+    return VCS()
+
+class VCS (libbe.storage.base.VersionedStorage):
+    """Implement 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 = 'false' # command-line tool for _u_invoke_client
+
+    def __init__(self, *args, **kwargs):
+        if 'encoding' not in kwargs:
+            kwargs['encoding'] = libbe.util.encoding.get_filesystem_encoding()
+        libbe.storage.base.VersionedStorage.__init__(self, *args, **kwargs)
+        self.versioned = False
+        self.interspersed_vcs_files = False
+        self.verbose_invoke = False
+        self._cached_path_id = CachedPathID()
+        self._rooted = False
+
+    def _vcs_version(self):
+        """
+        Return the VCS version string.
+        """
+        return '0'
+
+    def _vcs_get_user_id(self):
+        """
+        Get the VCS's suggested user id (e.g. "John Doe <jdoe@example.com>").
+        If the VCS has not been configured with a username, return None.
+        """
+        return None
+
+    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_destroy(self):
+        """
+        Remove any files used in versioning (e.g. whatever _vcs_init()
+        created).
+        """
+        pass
+
+    def _vcs_add(self, path):
+        """
+        Add the already created file at path to version control.
+        """
+        pass
+
+    def _vcs_exists(self, path, revision=None):
+        """
+        Does the path exist in a given revision? (True/False)
+        """
+        raise NotImplementedError('Lazy BE developers')
+
+    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_is_versioned(self, path):
+        """
+        Return true if a path is under version control, False
+        otherwise.  You only need to set this if the VCS goes about
+        dumping VCS-specific files into the .be directory.
+
+        If you do need to implement this method (e.g. Arch), set
+          self.interspersed_vcs_files = True
+        """
+        assert self.interspersed_vcs_files == False
+        raise NotImplementedError
+
+    def _vcs_get_file_contents(self, path, revision=None):
+        """
+        Get the file contents as they were in a given revision.
+        Revision==None specifies the current revision.
+        """
+        if revision != None:
+            raise libbe.storage.base.InvalidRevision(
+                'The %s VCS does not support revision specifiers' % self.name)
+        path = os.path.join(self.repo, path)
+        if not os.path.exists(path):
+            return libbe.util.InvalidObject
+        if os.path.isdir(path):
+            return libbe.storage.base.InvalidDirectory
+        f = open(path, 'rb')
+        contents = f.read()
+        f.close()
+        return contents
+
+    def _vcs_path(self, id, revision):
+        """
+        Return the relative path to object id as of revision.
+        
+        Revision will not be None.
+        """
+        raise NotImplementedError
+
+    def _vcs_isdir(self, path, revision):
+        """
+        Return True if path (as returned by _vcs_path) was a directory
+        as of revision, False otherwise.
+        
+        Revision will not be None.
+        """
+        raise NotImplementedError
+
+    def _vcs_listdir(self, path, revision):
+        """
+        Return a list of the contents of the directory path (as
+        returned by _vcs_path) as of revision.
+        
+        Revision will not be None, and ._vcs_isdir(path, revision)
+        will be True.
+        """
+        raise NotImplementedError
+
+    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 <index>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 _vcs_changed(self, revision):
+        """
+        Return a tuple of lists of ids
+          (new, modified, removed)
+        from the specified revision to the current situation.
+        """
+        return ([], [], [])
+
+    def version(self):
+        # Cache version string for efficiency.
+        if not hasattr(self, '_version'):
+            self._version = self._get_version()
+        return self._version
+
+    def _get_version(self):
+        try:
+            ret = self._vcs_version()
+            return ret
+        except OSError, e:
+            if e.errno == errno.ENOENT:
+                return None
+            else:
+                raise OSError, e
+        except CommandError:
+            return None
+
+    def installed(self):
+        if self.version() != None:
+            return True
+        return False
+
+    def get_user_id(self):
+        """
+        Get the VCS's suggested user id (e.g. "John Doe <jdoe@example.com>").
+        If the VCS has not been configured with a username, return None.
+        You can override the automatic lookup procedure by setting the
+        VCS.user_id attribute to a string of your choice.
+        """
+        if not hasattr(self, 'user_id'):
+            self.user_id = self._vcs_get_user_id()
+        return self.user_id
+
+    def _detect(self, path='.'):
+        """
+        Detect whether a directory is revision controlled with this VCS.
+        """
+        return self._vcs_detect(path)
+
+    def root(self):
+        """Set the root directory to the path's VCS root.
+
+        This is the default working directory for future invocations.
+        Consider the following usage case:
+
+        You have a project rooted in::
+
+          /path/to/source/
+
+        by which I mean the VCS repository is in, for example::
+
+          /path/to/source/.bzr
+
+        However, you're of in some subdirectory like::
+
+          /path/to/source/ui/testing
+
+        and you want to comment on a bug.  `root` will locate your VCS
+        root (``/path/to/source/``) and set the repo there.  This
+        means that it doesn't matter where you are in your project
+        tree when you call "be COMMAND", it always acts as if you called
+        it from the VCS root.
+        """
+        if self._detect(self.repo) == False:
+            raise VCSUnableToRoot(self)
+        root = self._vcs_root(self.repo)
+        self.repo = os.path.abspath(root)
+        if os.path.isdir(self.repo) == False:
+            self.repo = os.path.dirname(self.repo)
+        self.be_dir = os.path.join(
+            self.repo, self._cached_path_id._spacer_dirs[0])
+        self._cached_path_id.root(self.repo)
+        self._rooted = True
+
+    def _init(self):
+        """
+        Begin versioning the tree based at self.repo.
+        Also roots the vcs at path.
+
+        See Also
+        --------
+        root : called if the VCS has already been initialized.
+        """
+        if not os.path.exists(self.repo) or not os.path.isdir(self.repo):
+            raise VCSUnableToRoot(self)
+        if self._vcs_detect(self.repo) == False:
+            self._vcs_init(self.repo)
+        if self._rooted == False:
+            self.root()
+        os.mkdir(self.be_dir)
+        self._vcs_add(self._u_rel_path(self.be_dir))
+        self._setup_storage_version()
+        self._cached_path_id.init()
+
+    def _destroy(self):
+        self._vcs_destroy()
+        self._cached_path_id.destroy()
+        if os.path.exists(self.be_dir):
+            shutil.rmtree(self.be_dir)
+
+    def _connect(self):
+        if self._rooted == False:
+            self.root()
+        if not os.path.isdir(self.be_dir):
+            raise libbe.storage.base.ConnectionError(self)
+        self._cached_path_id.connect()
+        self.check_storage_version()
+
+    def _disconnect(self):
+        self._cached_path_id.disconnect()
+
+    def path(self, id, revision=None, relpath=True):
+        if revision == None:
+            path = self._cached_path_id.path(id)
+            if relpath == True:
+                return self._u_rel_path(path)
+            return path
+        path = self._vcs_path(id, revision)
+        if relpath == True:
+            return path
+        return os.path.join(self.repo, path)
+
+    def _add_path(self, path, directory=False):
+        relpath = self._u_rel_path(path)
+        reldirs = relpath.split(os.path.sep)
+        if directory == False:
+            reldirs = reldirs[:-1]
+        dir = self.repo
+        for reldir in reldirs:
+            dir = os.path.join(dir, reldir)
+            if not os.path.exists(dir):
+                os.mkdir(dir)
+                self._vcs_add(self._u_rel_path(dir))
+            elif not os.path.isdir(dir):
+                raise libbe.storage.base.InvalidDirectory
+        if directory == False:
+            if not os.path.exists(path):
+                open(path, 'w').close()
+            self._vcs_add(self._u_rel_path(path))
+
+    def _add(self, id, parent=None, **kwargs):
+        path = self._cached_path_id.add_id(id, parent)
+        self._add_path(path, **kwargs)
+
+    def _exists(self, id, revision=None):
+        if revision == None:
+            try:
+                path = self.path(id, revision, relpath=False)
+            except InvalidID, e:
+                return False
+            return os.path.exists(path)
+        path = self.path(id, revision, relpath=True)
+        return self._vcs_exists(relpath, revision)
+
+    def _remove(self, id):
+        path = self._cached_path_id.path(id)
+        if os.path.exists(path):
+            if os.path.isdir(path) and len(self.children(id)) > 0:
+                raise libbe.storage.base.DirectoryNotEmpty(id)
+            self._vcs_remove(self._u_rel_path(path))
+            if os.path.exists(path):
+                if os.path.isdir(path):
+                    os.rmdir(path)
+                else:
+                    os.remove(path)
+        self._cached_path_id.remove_id(id)
+
+    def _recursive_remove(self, id):
+        path = self._cached_path_id.path(id)
+        for dirpath,dirnames,filenames in os.walk(path, topdown=False):
+            filenames.extend(dirnames)
+            for f in filenames:
+                fullpath = os.path.join(dirpath, f)
+                if os.path.exists(fullpath) == False:
+                    continue
+                self._vcs_remove(self._u_rel_path(fullpath))
+        if os.path.exists(path):
+            shutil.rmtree(path)
+        path = self._cached_path_id.path(id, relpath=True)
+        for id,p in self._cached_path_id._cache.items():
+            if p.startswith(path):
+                self._cached_path_id.remove_id(id)
+
+    def _ancestors(self, id=None, revision=None):
+        if id==None:
+            path = self.be_dir
+        else:
+            path = self.path(id, revision, relpath=False)
+        ancestors = []
+        while True:
+            if not path.startswith(self.repo + os.path.sep):
+                break
+            path = os.path.dirname(path)
+            try:
+                id = self._u_path_to_id(path)
+                ancestors.append(id)
+            except (SpacerCollision, InvalidPath):
+                pass    
+        return ancestors
+
+    def _children(self, id=None, revision=None):
+        if revision == None:
+            isdir = os.path.isdir
+            listdir = os.listdir
+        else:
+            isdir = lambda path : self._vcs_isdir(
+                self._u_rel_path(path), revision)
+            listdir = lambda path : self._vcs_listdir(
+                self._u_rel_path(path), revision)
+        if id==None:
+            path = self.be_dir
+        else:
+            path = self.path(id, revision, relpath=False)
+        if isdir(path) == False: 
+            return []
+        children = listdir(path)
+        for i,c in enumerate(children):
+            if c in self._cached_path_id._spacer_dirs:
+                children[i] = None
+                children.extend([os.path.join(c, c2) for c2 in
+                                 listdir(os.path.join(path, c))])
+            elif c in ['id-cache', 'version']:
+                children[i] = None
+            elif self.interspersed_vcs_files \
+                    and self._vcs_is_versioned(c) == False:
+                children[i] = None
+        for i,c in enumerate(children):
+            if c == None: continue
+            cpath = os.path.join(path, c)
+            if self.interspersed_vcs_files == True \
+                    and revision != None \
+                    and self._vcs_is_versioned(cpath) == False:
+                children[i] = None
+            else:
+                children[i] = self._u_path_to_id(cpath)
+                children[i]
+        return [c for c in children if c != None]
+
+    def _get(self, id, default=libbe.util.InvalidObject, revision=None):
+        try:
+            relpath = self.path(id, revision, relpath=True)
+            contents = self._vcs_get_file_contents(relpath, revision)
+        except InvalidID, e:
+            if default == libbe.util.InvalidObject:
+                raise e
+            return default
+        if contents in [libbe.storage.base.InvalidDirectory,
+                        libbe.util.InvalidObject] \
+                or len(contents) == 0:
+            if default == libbe.util.InvalidObject:
+                raise InvalidID(id, revision)
+            return default
+        return contents
+
+    def _set(self, id, value):
+        try:
+            path = self._cached_path_id.path(id)
+        except InvalidID, e:
+            raise
+        if not os.path.exists(path):
+            raise InvalidID(id)
+        if os.path.isdir(path):
+            raise libbe.storage.base.InvalidDirectory(id)
+        f = open(path, "wb")
+        f.write(value)
+        f.close()
+        self._vcs_update(self._u_rel_path(path))
+
+    def _commit(self, summary, body=None, allow_empty=False):
+        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()
+            revision = self._vcs_commit(filename, allow_empty=allow_empty)
+            temp_file.close()
+        finally:
+            os.remove(filename)
+        return revision
+
+    def revision_id(self, index=None):
+        if index == None:
+            return None
+        try:
+            if int(index) != index:
+                raise InvalidRevision(index)
+        except ValueError:
+            raise InvalidRevision(index)
+        revid = self._vcs_revision_id(index)
+        if revid == None:
+            raise libbe.storage.base.InvalidRevision(index)
+        return revid
+
+    def changed(self, revision):
+        new,mod,rem = self._vcs_changed(revision)
+        def paths_to_ids(paths):
+            for p in paths:
+                try:
+                    id = self._u_path_to_id(p)
+                    yield id
+                except (SpacerCollision, InvalidPath):
+                    pass
+        new_id = list(paths_to_ids(new))
+        mod_id = list(paths_to_ids(mod))
+        rem_id = list(paths_to_ids(rem))
+        return (new_id, mod_id, rem_id)
+
+    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, **kwargs):
+        if 'cwd' not in kwargs:
+            kwargs['cwd'] = self.repo
+        if 'verbose' not in kwargs:
+            kwargs['verbose'] = self.verbose_invoke
+        if 'encoding' not in kwargs:
+            kwargs['encoding'] = self.encoding
+        return invoke(*args, **kwargs)
+
+    def _u_invoke_client(self, *args, **kwargs):
+        cl_args = [self.client]
+        cl_args.extend(args)
+        return self._u_invoke(cl_args, **kwargs)
+
+    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.
+        """
+        try:
+            ret = search_parent_directories(path, filename)
+        except AssertionError, e:
+            return None
+        return ret
+
+    def _u_find_id_from_manifest(self, id, manifest, revision=None):
+        """Search for the relative path to id using manifest, a list of all
+        files.
+        
+        Returns None if the id is not found.
+        """
+        be_dir = self._cached_path_id._spacer_dirs[0]
+        be_dir_sep = self._cached_path_id._spacer_dirs[0] + os.path.sep
+        files = [f for f in manifest if f.startswith(be_dir_sep)]
+        for file in files:
+            if not file.startswith(be_dir+os.path.sep):
+                continue
+            parts = file.split(os.path.sep)
+            dir = parts.pop(0) # don't add the first spacer dir
+            for part in parts[:-1]:
+                dir = os.path.join(dir, part)
+                if not dir in files:
+                    files.append(dir)
+        for file in files:
+            try:
+                p_id = self._u_path_to_id(file)
+                if p_id == id:
+                    return file
+            except (SpacerCollision, InvalidPath):
+                pass
+        raise InvalidID(id, revision=revision)
+
+    def _u_find_id(self, id, revision):
+        """Search for the relative path to id as of revision.
+
+        Returns None if the id is not found.
+        """
+        assert self._rooted == True
+        be_dir = self._cached_path_id._spacer_dirs[0]
+        stack = [(be_dir, be_dir)]
+        while len(stack) > 0:
+            path,long_id = stack.pop()
+            if long_id.endswith('/'+id):
+                return path
+            if self._vcs_isdir(path, revision) == False:
+                continue
+            for child in self._vcs_listdir(path, revision):
+                stack.append((os.path.join(path, child),
+                              '/'.join([long_id, child])))
+        raise InvalidID(id, revision=revision)
+
+    def _u_path_to_id(self, path):
+        return self._cached_path_id.id(path)
+
+    def _u_rel_path(self, path, root=None):
+        """Return the relative path to path from root.
+
+        Examples:
+
+        >>> vcs = new()
+        >>> vcs._u_rel_path("/a.b/c/.be", "/a.b/c")
+        '.be'
+        >>> vcs._u_rel_path("/a.b/c/", "/a.b/c")
+        '.'
+        >>> vcs._u_rel_path("/a.b/c/", "/a.b/c/")
+        '.'
+        >>> vcs._u_rel_path("./a", ".")
+        'a'
+        """
+        if root == None:
+            if self.repo == None:
+                raise VCSNotRooted(self)
+            root = self.repo
+        path = os.path.abspath(path)
+        absRoot = os.path.abspath(root)
+        absRootSlashedDir = os.path.join(absRoot,"")
+        if path in [absRoot, absRootSlashedDir]:
+            return '.'
+        if not path.startswith(absRootSlashedDir):
+            raise InvalidPath(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.
+
+        Examples
+        --------
+
+        >>> vcs = new()
+        >>> vcs._u_abspath(".be", "/a.b/c")
+        '/a.b/c/.be'
+        """
+        if root == None:
+            assert self.repo != None, "VCS not rooted"
+            root = self.repo
+        return os.path.abspath(os.path.join(root, path))
+
+    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 check_storage_version(self):
+        version = self.storage_version()
+        if version != libbe.storage.STORAGE_VERSION:
+            upgrade.upgrade(self.repo, version)
+
+    def storage_version(self, revision=None, path=None):
+        """Return the storage version of the on-disk files.
+
+        See Also
+        --------
+        :mod:`libbe.storage.util.upgrade`
+        """
+        if path == None:
+            path = os.path.join(self.repo, '.be', 'version')
+        if not os.path.exists(path):
+            raise libbe.storage.InvalidStorageVersion(None)
+        if revision == None: # don't require connection
+            return libbe.util.encoding.get_file_contents(
+                path, decode=True).rstrip('\n')
+        relpath = self._u_rel_path(path)
+        contents = self._vcs_get_file_contents(relpath, revision=revision)
+        if type(contents) != types.UnicodeType:
+            contents = unicode(contents, self.encoding)
+        return contents.strip()
+
+    def _setup_storage_version(self):
+        """
+        Requires disk access.
+        """
+        assert self._rooted == True
+        path = os.path.join(self.be_dir, 'version')
+        if not os.path.exists(path):
+            libbe.util.encoding.set_file_contents(path,
+                libbe.storage.STORAGE_VERSION+'\n')
+            self._vcs_add(self._u_rel_path(path))
+
+\f
+if libbe.TESTING == True:
+    class VCSTestCase (unittest.TestCase):
+        """
+        Test cases for base VCS class (in addition to the Storage test
+        cases).
+        """
+
+        Class = VCS
+
+        def __init__(self, *args, **kwargs):
+            super(VCSTestCase, self).__init__(*args, **kwargs)
+            self.dirname = None
+
+        def setUp(self):
+            """Set up test fixtures for Storage test case."""
+            super(VCSTestCase, self).setUp()
+            self.dir = Dir()
+            self.dirname = self.dir.path
+            self.s = self.Class(repo=self.dirname)
+            if self.s.installed() == True:
+                self.s.init()
+                self.s.connect()
+
+        def tearDown(self):
+            super(VCSTestCase, self).tearDown()
+            if self.s.installed() == True:
+                self.s.disconnect()
+                self.s.destroy()
+            self.dir.cleanup()
+
+    class VCS_installed_TestCase (VCSTestCase):
+        def test_installed(self):
+            """See if the VCS is installed.
+            """
+            self.failUnless(self.s.installed() == True,
+                            '%(name)s VCS not found' % vars(self.Class))
+
+
+    class VCS_detection_TestCase (VCSTestCase):
+        def test_detection(self):
+            """See if the VCS detects its installed repository
+            """
+            if self.s.installed():
+                self.s.disconnect()
+                self.failUnless(self.s._detect(self.dirname) == True,
+                    'Did not detected %(name)s VCS after initialising'
+                    % vars(self.Class))
+                self.s.connect()
+
+        def test_no_detection(self):
+            """See if the VCS detects its installed repository
+            """
+            if self.s.installed() and self.Class.name != 'None':
+                self.s.disconnect()
+                self.s.destroy()
+                self.failUnless(self.s._detect(self.dirname) == False,
+                    'Detected %(name)s VCS before initialising'
+                    % vars(self.Class))
+                self.s.init()
+                self.s.connect()
+
+        def test_vcs_repo_in_specified_root_path(self):
+            """VCS root directory should be in specified root path."""
+            rp = os.path.realpath(self.s.repo)
+            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 self.s.installed():
+                user_id = self.s.get_user_id()
+                if user_id == None:
+                    return
+                name,email = libbe.ui.util.user.parse_user_id(user_id)
+                if email != None:
+                    self.failUnless('@' in email, email)
+
+    def make_vcs_testcase_subclasses(vcs_class, namespace):
+        c = vcs_class()
+        if c.installed():
+            if c.versioned == True:
+                libbe.storage.base.make_versioned_storage_testcase_subclasses(
+                    vcs_class, namespace)
+            else:
+                libbe.storage.base.make_storage_testcase_subclasses(
+                    vcs_class, namespace)
+
+        if namespace != sys.modules[__name__]:
+            # 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) \
+                    and c.Class == VCS]
+
+            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)
+
+    make_vcs_testcase_subclasses(VCS, sys.modules[__name__])
+
+    unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
+    suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
diff --git a/libbe/storage/vcs/bzr.py b/libbe/storage/vcs/bzr.py
new file mode 100644 (file)
index 0000000..5a62968
--- /dev/null
@@ -0,0 +1,361 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Ben Finney <benf@cybersource.com.au>
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Marien Zwart <marienz@gentoo.org>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+.. _Bazaar: http://bazaar.canonical.com/
+"""
+
+try:
+    import bzrlib
+    import bzrlib.branch
+    import bzrlib.builtins
+    import bzrlib.config
+    import bzrlib.errors
+    import bzrlib.option
+except ImportError:
+    bzrlib = None
+import os
+import os.path
+import re
+import shutil
+import StringIO
+import sys
+import types
+
+import libbe
+import base
+
+if libbe.TESTING == True:
+    import doctest
+    import unittest
+
+
+def new():
+    return Bzr()
+
+class Bzr(base.VCS):
+    """:class:`base.VCS` implementation for Bazaar.
+    """
+    name = 'bzr'
+    client = None # bzrlib module
+
+    def __init__(self, *args, **kwargs):
+        base.VCS.__init__(self, *args, **kwargs)
+        self.versioned = True
+
+    def _vcs_version(self):
+        if bzrlib == None:
+            return None
+        return bzrlib.__version__
+
+    def version_cmp(self, *args):
+        """Compare the installed Bazaar version `V_i` with another version
+        `V_o` (given in `*args`).  Returns
+
+           === ===============
+            1  if `V_i > V_o`
+            0  if `V_i == V_o`
+           -1  if `V_i < V_o`
+           === ===============
+
+        Examples
+        --------
+
+        >>> b = Bzr(repo='.')
+        >>> b._vcs_version = lambda : "2.3.1 (release)"
+        >>> b.version_cmp(2,3,1)
+        0
+        >>> b.version_cmp(2,3,2)
+        -1
+        >>> b.version_cmp(2,3,0)
+        1
+        >>> b.version_cmp(3)
+        -1
+        >>> b._vcs_version = lambda : "2.0.0pre2"
+        >>> b._parsed_version = None
+        >>> b.version_cmp(3)
+        -1
+        >>> b.version_cmp(2,0,1)
+        Traceback (most recent call last):
+          ...
+        NotImplementedError: Cannot parse non-integer portion "0pre2" of Bzr version "2.0.0pre2"
+        """
+        if not hasattr(self, '_parsed_version') \
+                or self._parsed_version == None:
+            num_part = self._vcs_version().split(' ')[0]
+            self._parsed_version = []
+            for num in num_part.split('.'):
+                try:
+                    self._parsed_version.append(int(num))
+                except ValueError, e:
+                    self._parsed_version.append(num)
+        for current,other in zip(self._parsed_version, args):
+            if type(current) != types.IntType:
+                raise NotImplementedError(
+                    'Cannot parse non-integer portion "%s" of Bzr version "%s"'
+                    % (current, self._vcs_version()))
+            c = cmp(current,other)
+            if c != 0:
+                return c
+        return 0
+
+    def _vcs_get_user_id(self):
+        # excerpted from bzrlib.builtins.cmd_whoami.run()
+        try:
+            c = bzrlib.branch.Branch.open_containing(self.repo)[0].get_config()
+        except errors.NotBranchError:
+            c = bzrlib.config.GlobalConfig()
+        return c.username()
+
+    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."""
+        cmd = bzrlib.builtins.cmd_root()
+        cmd.outf = StringIO.StringIO()
+        cmd.run(filename=path)
+        return cmd.outf.getvalue().rstrip('\n')
+
+    def _vcs_init(self, path):
+        cmd = bzrlib.builtins.cmd_init()
+        cmd.outf = StringIO.StringIO()
+        cmd.run(location=path)
+
+    def _vcs_destroy(self):
+        vcs_dir = os.path.join(self.repo, '.bzr')
+        if os.path.exists(vcs_dir):
+            shutil.rmtree(vcs_dir)
+
+    def _vcs_add(self, path):
+        path = os.path.join(self.repo, path)
+        cmd = bzrlib.builtins.cmd_add()
+        cmd.outf = StringIO.StringIO()
+        cmd.run(file_list=[path], file_ids_from=self.repo)
+
+    def _vcs_exists(self, path, revision=None):
+        manifest = self._vcs_listdir(
+            self.repo, revision=revision, recursive=True)
+        if path in manifest:
+            return True
+        return False
+
+    def _vcs_remove(self, path):
+        # --force to also remove unversioned files.
+        path = os.path.join(self.repo, path)
+        cmd = bzrlib.builtins.cmd_remove()
+        cmd.outf = StringIO.StringIO()
+        cmd.run(file_list=[path], file_deletion_strategy='force')
+
+    def _vcs_update(self, path):
+        pass
+
+    def _parse_revision_string(self, revision=None):
+        if revision == None:
+            return revision
+        rev_opt = bzrlib.option.Option.OPTIONS['revision']
+        try:
+            rev_spec = rev_opt.type(revision)
+        except bzrlib.errors.NoSuchRevisionSpec:
+            raise base.InvalidRevision(revision)
+        return rev_spec
+
+    def _vcs_get_file_contents(self, path, revision=None):
+        if revision == None:
+            return base.VCS._vcs_get_file_contents(self, path, revision)
+        path = os.path.join(self.repo, path)
+        revision = self._parse_revision_string(revision)
+        cmd = bzrlib.builtins.cmd_cat()
+        cmd.outf = StringIO.StringIO()
+        if self.version_cmp(1,6,0) < 0:
+            # old bzrlib cmd_cat uses sys.stdout not self.outf for output.
+            stdout = sys.stdout
+            sys.stdout = cmd.outf
+        try:
+            cmd.run(filename=path, revision=revision)
+        except bzrlib.errors.BzrCommandError, e:
+            if 'not present in revision' in str(e):
+                raise base.InvalidPath(path, root=self.repo, revision=revision)
+            raise
+        finally:
+            if self.version_cmp(2,0,0) < 0:
+                cmd.outf = sys.stdout
+                sys.stdout = stdout
+        return cmd.outf.getvalue()
+
+    def _vcs_path(self, id, revision):
+        manifest = self._vcs_listdir(
+            self.repo, revision=revision, recursive=True)
+        return self._u_find_id_from_manifest(id, manifest, revision=revision)
+
+    def _vcs_isdir(self, path, revision):
+        try:
+            self._vcs_listdir(path, revision)
+        except AttributeError, e:
+            if 'children' in str(e):
+                return False
+            raise
+        return True
+
+    def _vcs_listdir(self, path, revision, recursive=False):
+        path = os.path.join(self.repo, path)
+        revision = self._parse_revision_string(revision)
+        cmd = bzrlib.builtins.cmd_ls()
+        cmd.outf = StringIO.StringIO()
+        try:
+            if self.version_cmp(2,0,0) >= 0:
+                cmd.run(revision=revision, path=path, recursive=recursive)
+            else:
+                # Pre-2.0 Bazaar (non_recursive)
+                # + working around broken non_recursive+path implementation
+                #   (https://bugs.launchpad.net/bzr/+bug/158690)
+                cmd.run(revision=revision, path=path,
+                        non_recursive=False)
+        except bzrlib.errors.BzrCommandError, e:
+            if 'not present in revision' in str(e):
+                raise base.InvalidPath(path, root=self.repo, revision=revision)
+            raise
+        children = cmd.outf.getvalue().rstrip('\n').splitlines()
+        children = [self._u_rel_path(c, path) for c in children]
+        if self.version_cmp(2,0,0) < 0 and recursive == False:
+            children = [c for c in children if os.path.sep not in c]
+        return children
+
+    def _vcs_commit(self, commitfile, allow_empty=False):
+        cmd = bzrlib.builtins.cmd_commit()
+        cmd.outf = StringIO.StringIO()
+        cwd = os.getcwd()
+        os.chdir(self.repo)
+        try:
+            cmd.run(file=commitfile, unchanged=allow_empty)
+        except bzrlib.errors.BzrCommandError, e:
+            strings = ['no changes to commit.', # bzr 1.3.1
+                       'No changes to commit.'] # bzr 1.15.1
+            if self._u_any_in_string(strings, str(e)) == True:
+                raise base.EmptyCommit()
+            raise
+        finally:
+            os.chdir(cwd)
+        return self._vcs_revision_id(-1)
+
+    def _vcs_revision_id(self, index):
+        cmd = bzrlib.builtins.cmd_revno()
+        cmd.outf = StringIO.StringIO()
+        cmd.run(location=self.repo)
+        current_revision = int(cmd.outf.getvalue())
+        if index > current_revision or index < -current_revision:
+            return None
+        if index >= 0:
+            return str(index) # bzr commit 0 is the empty tree.
+        return str(current_revision+index+1)
+
+    def _diff(self, revision):
+        revision = self._parse_revision_string(revision)
+        cmd = bzrlib.builtins.cmd_diff()
+        cmd.outf = StringIO.StringIO()
+        # for some reason, cmd_diff uses sys.stdout not self.outf for output.
+        stdout = sys.stdout
+        sys.stdout = cmd.outf
+        try:
+            status = cmd.run(revision=revision, file_list=[self.repo])
+        finally:
+            sys.stdout = stdout
+        assert status in [0,1], "Invalid status %d" % status
+        return cmd.outf.getvalue()
+
+    def _parse_diff(self, diff_text):
+        """_parse_diff(diff_text) -> (new,modified,removed)
+
+        `new`, `modified`, and `removed` are lists of files.
+
+        Example diff text::
+
+          === modified file 'dir/changed'
+          --- dir/changed      2010-01-16 01:54:53 +0000
+          +++ dir/changed      2010-01-16 01:54:54 +0000
+          @@ -1,3 +1,3 @@
+           hi
+          -there
+          +everyone and
+           joe
+          
+          === removed file 'dir/deleted'
+          --- dir/deleted      2010-01-16 01:54:53 +0000
+          +++ dir/deleted      1970-01-01 00:00:00 +0000
+          @@ -1,3 +0,0 @@
+          -in
+          -the
+          -beginning
+          
+          === removed file 'dir/moved'
+          --- dir/moved        2010-01-16 01:54:53 +0000
+          +++ dir/moved        1970-01-01 00:00:00 +0000
+          @@ -1,4 +0,0 @@
+          -the
+          -ants
+          -go
+          -marching
+          
+          === added file 'dir/moved2'
+          --- dir/moved2       1970-01-01 00:00:00 +0000
+          +++ dir/moved2       2010-01-16 01:54:34 +0000
+          @@ -0,0 +1,4 @@
+          +the
+          +ants
+          +go
+          +marching
+          
+          === added file 'dir/new'
+          --- dir/new  1970-01-01 00:00:00 +0000
+          +++ dir/new  2010-01-16 01:54:54 +0000
+          @@ -0,0 +1,2 @@
+          +hello
+          +world
+          
+        """
+        new = []
+        modified = []
+        removed = []
+        for line in diff_text.splitlines():
+            if not line.startswith('=== '):
+                continue
+            fields = line.split()
+            action = fields[1]
+            file = fields[-1].strip("'")
+            if action == 'added':
+                new.append(file)
+            elif action == 'modified':
+                modified.append(file)
+            elif action == 'removed':
+                removed.append(file)
+        return (new,modified,removed)
+
+    def _vcs_changed(self, revision):
+        return self._parse_diff(self._diff(revision))
+
+\f
+if libbe.TESTING == True:
+    base.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/storage/vcs/darcs.py b/libbe/storage/vcs/darcs.py
new file mode 100644 (file)
index 0000000..4a21888
--- /dev/null
@@ -0,0 +1,399 @@
+# Copyright (C) 2009-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+.. _Darcs: http://darcs.net/
+"""
+
+import codecs
+import os
+import re
+import shutil
+import sys
+import time # work around http://mercurial.selenic.com/bts/issue618
+import types
+try: # import core module, Python >= 2.5
+    from xml.etree import ElementTree
+except ImportError: # look for non-core module
+    from elementtree import ElementTree
+from xml.sax.saxutils import unescape
+
+import libbe
+import base
+
+if libbe.TESTING == True:
+    import doctest
+    import unittest
+
+
+def new():
+    return Darcs()
+
+class Darcs(base.VCS):
+    """:class:`base.VCS` implementation for Darcs.
+    """
+    name='darcs'
+    client='darcs'
+
+    def __init__(self, *args, **kwargs):
+        base.VCS.__init__(self, *args, **kwargs)
+        self.versioned = True
+        self.__updated = [] # work around http://mercurial.selenic.com/bts/issue618
+
+    def _vcs_version(self):
+        status,output,error = self._u_invoke_client('--version')
+        return output.strip()
+
+    def version_cmp(self, *args):
+        """Compare the installed Darcs version `V_i` with another version
+        `V_o` (given in `*args`).  Returns
+
+           === ===============
+            1  if `V_i > V_o`
+            0  if `V_i == V_o`
+           -1  if `V_i < V_o`
+           === ===============
+
+        Examples
+        --------
+
+        >>> d = Darcs(repo='.')
+        >>> d._vcs_version = lambda : "2.3.1 (release)"
+        >>> d.version_cmp(2,3,1)
+        0
+        >>> d.version_cmp(2,3,2)
+        -1
+        >>> d.version_cmp(2,3,0)
+        1
+        >>> d.version_cmp(3)
+        -1
+        >>> d._vcs_version = lambda : "2.0.0pre2"
+        >>> d._parsed_version = None
+        >>> d.version_cmp(3)
+        -1
+        >>> d.version_cmp(2,0,1)
+        Traceback (most recent call last):
+          ...
+        NotImplementedError: Cannot parse non-integer portion "0pre2" of Darcs version "2.0.0pre2"
+        """
+        if not hasattr(self, '_parsed_version') \
+                or self._parsed_version == None:
+            num_part = self._vcs_version().split(' ')[0]
+            self._parsed_version = []
+            for num in num_part.split('.'):
+                try:
+                    self._parsed_version.append(int(num))
+                except ValueError, e:
+                    self._parsed_version.append(num)
+        for current,other in zip(self._parsed_version, args):
+            if type(current) != types.IntType:
+                raise NotImplementedError(
+                    'Cannot parse non-integer portion "%s" of Darcs version "%s"'
+                    % (current, self._vcs_version()))
+            c = cmp(current,other)
+            if c != 0:
+                return c
+        return 0
+
+    def _vcs_get_user_id(self):
+        # following http://darcs.net/manual/node4.html#SECTION00410030000000000000
+        # as of June 29th, 2009
+        if self.repo == None:
+            return None
+        darcs_dir = os.path.join(self.repo, '_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_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', cwd=path)
+
+    def _vcs_destroy(self):
+        vcs_dir = os.path.join(self.repo, '_darcs')
+        if os.path.exists(vcs_dir):
+            shutil.rmtree(vcs_dir)
+
+    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.repo, path)) # darcs notices removal
+
+    def _vcs_update(self, path):
+        self.__updated.append(path) # work around http://mercurial.selenic.com/bts/issue618
+        pass # darcs notices changes
+
+    def _vcs_get_file_contents(self, path, revision=None):
+        if revision == None:
+            return base.VCS._vcs_get_file_contents(self, path, revision)
+        if self.version_cmp(2, 0, 0) == 1:
+            status,output,error = self._u_invoke_client( \
+                'show', 'contents', '--patch', revision, path)
+            return output
+        # Darcs versions < 2.0.0pre2 lack the 'show contents' command
+
+        patch = self._diff(revision, path=path, unicode_output=False)
+
+        # '--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=patch)
+
+        if os.path.exists(os.path.join(self.repo, path)) == True:
+            contents = base.VCS._vcs_get_file_contents(self, path)
+        else:
+            contents = ''
+
+        # Now restore path to it's current incarnation
+        args=['patch', path]
+        status,output,error = self._u_invoke(args, stdin=patch)
+        return contents
+
+    def _vcs_path(self, id, revision):
+        return self._u_find_id(id, revision)
+
+    def _vcs_isdir(self, path, revision):
+        if self.version_cmp(2, 3, 1) == 1:
+            # Sun Nov 15 20:32:06 EST 2009  thomashartman1@gmail.com
+            #   * add versioned show files functionality (darcs show files -p 'some patch')
+            status,output,error = self._u_invoke_client( \
+                'show', 'files', '--no-files', '--patch', revision)
+            children = output.rstrip('\n').splitlines()
+            rpath = '.'
+            children = [self._u_rel_path(c, rpath) for c in children]
+            if path in children:
+                return True
+            return False
+        raise NotImplementedError(
+            'Darcs versions <= 2.3.1 lack the --patch option for "show files"')
+
+    def _vcs_listdir(self, path, revision):
+        if self.version_cmp(2, 3, 1) == 1:
+            # Sun Nov 15 20:32:06 EST 2009  thomashartman1@gmail.com
+            #   * add versioned show files functionality (darcs show files -p 'some patch')
+            # Wed Dec  9 05:42:21 EST 2009  Luca Molteni <volothamp@gmail.com>
+            #   * resolve issue835 show file with file directory arguments
+            path = path.rstrip(os.path.sep)
+            status,output,error = self._u_invoke_client( \
+                'show', 'files', '--patch', revision, path)
+            files = output.rstrip('\n').splitlines()
+            if path == '.':
+                descendents = [self._u_rel_path(f, path) for f in files
+                               if f != '.']
+            else:
+                descendents = [self._u_rel_path(f, path) for f in files
+                               if f.startswith(path)]
+            return [f for f in descendents if f.count(os.path.sep) == 0]
+        # Darcs versions <= 2.3.1 lack the --patch option for 'show files'
+        raise NotImplementedError
+
+    def _vcs_commit(self, commitfile, allow_empty=False):
+        id = self.get_user_id()
+        if id == None or '@' 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!']
+        # work around http://mercurial.selenic.com/bts/issue618
+        if self._u_any_in_string(empty_strings, output) == True \
+                and len(self.__updated) > 0:
+            time.sleep(1)
+            for path in self.__updated:
+                os.utime(os.path.join(self.repo, path), None)
+            status,output,error = self._u_invoke_client(*args)
+        self.__updated = []
+        # end work around
+        if self._u_any_in_string(empty_strings, output) == True:
+            if allow_empty == False:
+                raise base.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 _revisions(self):
+        """
+        Return a list of revisions in the repository.
+        """
+        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()
+        return revisions
+
+    def _vcs_revision_id(self, index):
+        revisions = self._revisions()
+        try:
+            if index > 0:
+                return revisions[index-1]
+            elif index < 0:
+                return revisions[index]
+            else:
+                return None
+        except IndexError:
+            return None
+
+    def _diff(self, revision, path=None, unicode_output=True):
+        revisions = self._revisions()
+        i = revisions.index(revision)
+        args = ['diff', '--unified']
+        if i+1 < len(revisions):
+            next_rev = revisions[i+1]
+            args.extend(['--from-patch', next_rev])
+        if path != None:
+            args.append(path)
+        kwargs = {'unicode_output':unicode_output}
+        status,output,error = self._u_invoke_client(
+            *args, **kwargs)
+        return output
+
+    def _parse_diff(self, diff_text):
+        """_parse_diff(diff_text) -> (new,modified,removed)
+
+        `new`, `modified`, and `removed` are lists of files.
+
+        Example diff text::
+
+          Mon Jan 18 15:19:30 EST 2010  None <None@invalid.com>
+            * Final state
+          diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/modified new-BEtestgQtDuD/.be/dir/bugs/modified
+          --- old-BEtestgQtDuD/.be/dir/bugs/modified      2010-01-18 15:19:30.000000000 -0500
+          +++ new-BEtestgQtDuD/.be/dir/bugs/modified      2010-01-18 15:19:30.000000000 -0500
+          @@ -1 +1 @@
+          -some value to be modified
+          \ No newline at end of file
+          +a new value
+          \ No newline at end of file
+          diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved new-BEtestgQtDuD/.be/dir/bugs/moved
+          --- old-BEtestgQtDuD/.be/dir/bugs/moved 2010-01-18 15:19:30.000000000 -0500
+          +++ new-BEtestgQtDuD/.be/dir/bugs/moved 1969-12-31 19:00:00.000000000 -0500
+          @@ -1 +0,0 @@
+          -this entry will be moved
+          \ No newline at end of file
+          diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved2 new-BEtestgQtDuD/.be/dir/bugs/moved2
+          --- old-BEtestgQtDuD/.be/dir/bugs/moved2        1969-12-31 19:00:00.000000000 -0500
+          +++ new-BEtestgQtDuD/.be/dir/bugs/moved2        2010-01-18 15:19:30.000000000 -0500
+          @@ -0,0 +1 @@
+          +this entry will be moved
+          \ No newline at end of file
+          diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/new new-BEtestgQtDuD/.be/dir/bugs/new
+          --- old-BEtestgQtDuD/.be/dir/bugs/new   1969-12-31 19:00:00.000000000 -0500
+          +++ new-BEtestgQtDuD/.be/dir/bugs/new   2010-01-18 15:19:30.000000000 -0500
+          @@ -0,0 +1 @@
+          +this entry is new
+          \ No newline at end of file
+          diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/removed new-BEtestgQtDuD/.be/dir/bugs/removed
+          --- old-BEtestgQtDuD/.be/dir/bugs/removed       2010-01-18 15:19:30.000000000 -0500
+          +++ new-BEtestgQtDuD/.be/dir/bugs/removed       1969-12-31 19:00:00.000000000 -0500
+          @@ -1 +0,0 @@
+          -this entry will be deleted
+          \ No newline at end of file
+          
+        """
+        new = []
+        modified = []
+        removed = []
+        lines = diff_text.splitlines()
+        repodir = os.path.basename(self.repo) + os.path.sep
+        i = 0
+        while i < len(lines):
+            line = lines[i]; i += 1
+            if not line.startswith('diff '):
+                continue
+            file_a,file_b = line.split()[-2:]
+            assert file_a.startswith('old-'), \
+                'missformed file_a %s' % file_a
+            assert file_b.startswith('new-'), \
+                'missformed file_a %s' % file_b
+            file = file_a[4:]
+            assert file_b[4:] == file, \
+                'diff file missmatch %s != %s' % (file_a, file_b)
+            assert file.startswith(repodir), \
+                'missformed file_a %s' % file_a
+            file = file[len(repodir):]
+            lines_added = 0
+            lines_removed = 0
+            line = lines[i]; i += 1
+            assert line.startswith('--- old-'), \
+                'missformed "---" line %s' % line
+            time_a = line.split('\t')[1]
+            line = lines[i]; i += 1
+            assert line.startswith('+++ new-'), \
+                'missformed "+++" line %s' % line
+            time_b = line.split('\t')[1]
+            zero_time = time.strftime('%Y-%m-%d %H:%M:%S.000000000 ',
+                                      time.localtime(0))
+            # note that zero_time is missing the trailing timezone offset
+            if time_a.startswith(zero_time):
+                new.append(file)
+            elif time_b.startswith(zero_time):
+                removed.append(file)
+            else:
+                modified.append(file)
+        return (new,modified,removed)
+
+    def _vcs_changed(self, revision):
+        return self._parse_diff(self._diff(revision))
+
+\f
+if libbe.TESTING == True:
+    base.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/storage/vcs/git.py b/libbe/storage/vcs/git.py
new file mode 100644 (file)
index 0000000..4df9bc8
--- /dev/null
@@ -0,0 +1,269 @@
+# Copyright (C) 2008-2010 Ben Finney <benf@cybersource.com.au>
+#                         Chris Ball <cjb@laptop.org>
+#                         Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+.. _Git: http://git-scm.com/
+"""
+
+import os
+import os.path
+import re
+import shutil
+import unittest
+
+import libbe
+import libbe.ui.util.user
+import base
+
+if libbe.TESTING == True:
+    import doctest
+    import sys
+
+
+def new():
+    return Git()
+
+class Git(base.VCS):
+    """:class:`base.VCS` implementation for Git.
+    """
+    name='git'
+    client='git'
+
+    def __init__(self, *args, **kwargs):
+        base.VCS.__init__(self, *args, **kwargs)
+        self.versioned = True
+
+    def _vcs_version(self):
+        status,output,error = self._u_invoke_client('--version')
+        return output.strip()
+
+    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 = libbe.ui.util.user.get_fallback_username()
+            if email == '':
+                email = libe.ui.util.user.get_fallback_email()
+            return libbe.ui.util.user.create_user_id(name, email)
+        return None # Git has no infomation
+
+    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',
+                                                    cwd=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', cwd=path)
+
+    def _vcs_destroy(self):
+        vcs_dir = os.path.join(self.repo, '.git')
+        if os.path.exists(vcs_dir):
+            shutil.rmtree(vcs_dir)
+
+    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):
+        if revision == None:
+            return base.VCS._vcs_get_file_contents(self, path, revision)
+        else:
+            arg = '%s:%s' % (revision,path)
+            status,output,error = self._u_invoke_client('show', arg)
+            return output
+
+    def _vcs_path(self, id, revision):
+        return self._u_find_id(id, revision)
+
+    def _vcs_isdir(self, path, revision):
+        arg = '%s:%s' % (revision,path)
+        args = ['ls-tree', arg]
+        kwargs = {'expect':(0,128)}
+        status,output,error = self._u_invoke_client(*args, **kwargs)
+        if status != 0:
+            if 'not a tree object' in error:
+                return False
+            raise base.CommandError(args, status, stderr=error)
+        return True
+
+    def _vcs_listdir(self, path, revision):
+        arg = '%s:%s' % (revision,path)
+        status,output,error = self._u_invoke_client(
+            'ls-tree', '--name-only', arg)
+        return output.rstrip('\n').splitlines()
+
+    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 base.EmptyCommit()
+        full_revision = self._vcs_revision_id(-1)
+        assert full_revision[:7] in output, \
+            'Mismatched revisions:\n%s\n%s' % (full_revision, output)
+        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 base.CommandError(args, status, stderr=error)
+        revisions = output.splitlines()
+        try:
+            if index > 0:
+                return revisions[index-1]
+            elif index < 0:
+                return revisions[index]
+            else:
+                return None
+        except IndexError:
+            return None
+
+    def _diff(self, revision):
+        status,output,error = self._u_invoke_client('diff', revision)
+        return output
+
+    def _parse_diff(self, diff_text):
+        """_parse_diff(diff_text) -> (new,modified,removed)
+
+        `new`, `modified`, and `removed` are lists of files.
+
+        Example diff text::
+
+          diff --git a/dir/changed b/dir/changed
+          index 6c3ea8c..2f2f7c7 100644
+          --- a/dir/changed
+          +++ b/dir/changed
+          @@ -1,3 +1,3 @@
+           hi
+          -there
+          +everyone and
+           joe
+          diff --git a/dir/deleted b/dir/deleted
+          deleted file mode 100644
+          index 225ec04..0000000
+          --- a/dir/deleted
+          +++ /dev/null
+          @@ -1,3 +0,0 @@
+          -in
+          -the
+          -beginning
+          diff --git a/dir/moved b/dir/moved
+          deleted file mode 100644
+          index 5ef102f..0000000
+          --- a/dir/moved
+          +++ /dev/null
+          @@ -1,4 +0,0 @@
+          -the
+          -ants
+          -go
+          -marching
+          diff --git a/dir/moved2 b/dir/moved2
+          new file mode 100644
+          index 0000000..5ef102f
+          --- /dev/null
+          +++ b/dir/moved2
+          @@ -0,0 +1,4 @@
+          +the
+          +ants
+          +go
+          +marching
+          diff --git a/dir/new b/dir/new
+          new file mode 100644
+          index 0000000..94954ab
+          --- /dev/null
+          +++ b/dir/new
+          @@ -0,0 +1,2 @@
+          +hello
+          +world
+        """
+        new = []
+        modified = []
+        removed = []
+        lines = diff_text.splitlines()
+        for i,line in enumerate(lines):
+            if not line.startswith('diff '):
+                continue
+            file_a,file_b = line.split()[-2:]
+            assert file_a.startswith('a/'), \
+                'missformed file_a %s' % file_a
+            assert file_b.startswith('b/'), \
+                'missformed file_a %s' % file_b
+            file = file_a[2:]
+            assert file_b[2:] == file, \
+                'diff file missmatch %s != %s' % (file_a, file_b)
+            if lines[i+1].startswith('new '):
+                new.append(file)
+            elif lines[i+1].startswith('index '):
+                modified.append(file)
+            elif lines[i+1].startswith('deleted '):
+                removed.append(file)
+        return (new,modified,removed)
+
+    def _vcs_changed(self, revision):
+        return self._parse_diff(self._diff(revision))
+
+\f
+if libbe.TESTING == True:
+    base.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/storage/vcs/hg.py b/libbe/storage/vcs/hg.py
new file mode 100644 (file)
index 0000000..9378336
--- /dev/null
@@ -0,0 +1,257 @@
+# Copyright (C) 2007-2010 Aaron Bentley and Panometrics, Inc.
+#                         Ben Finney <benf@cybersource.com.au>
+#                         Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+.. _Mercurial: http://mercurial.selenic.com/
+"""
+
+try:
+    import mercurial
+    import mercurial.dispatch
+    import mercurial.ui
+except ImportError:
+    mercurial = None
+
+try:
+    # mercurial >= 1.2
+    from mercurial.util import version
+except ImportError:
+    try:
+        # mercurial <= 1.1.2
+        from mercurial.version import get_version as version
+    except ImportError:
+        version = None
+
+import os
+import os.path
+import re
+import shutil
+import StringIO
+import sys
+import time # work around http://mercurial.selenic.com/bts/issue618
+
+import libbe
+import base
+
+if libbe.TESTING == True:
+    import doctest
+    import unittest
+
+
+def new():
+    return Hg()
+
+class Hg(base.VCS):
+    """:class:`base.VCS` implementation for Mercurial.
+    """
+    name='hg'
+    client=None # mercurial module
+
+    def __init__(self, *args, **kwargs):
+        base.VCS.__init__(self, *args, **kwargs)
+        self.versioned = True
+        self.__updated = [] # work around http://mercurial.selenic.com/bts/issue618
+
+    def _vcs_version(self):
+        if version == None:
+            return None
+        return version()
+
+    def _u_invoke_client(self, *args, **kwargs):
+        if 'cwd' not in kwargs:
+            kwargs['cwd'] = self.repo
+        assert len(kwargs) == 1, kwargs
+        fullargs = ['--cwd', kwargs['cwd']]
+        fullargs.extend(args)
+        stdout = sys.stdout
+        tmp_stdout = StringIO.StringIO()
+        sys.stdout = tmp_stdout
+        cwd = os.getcwd()
+        mercurial.dispatch.dispatch(fullargs)
+        os.chdir(cwd)
+        sys.stdout = stdout
+        return tmp_stdout.getvalue().rstrip('\n')
+
+    def _vcs_get_user_id(self):
+        output = self._u_invoke_client(
+            'showconfig', 'ui.username').rstrip('\n')
+        if output != '':
+            return output
+        return None
+
+    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):
+        return self._u_invoke_client('root', cwd=path)
+
+    def _vcs_init(self, path):
+        self._u_invoke_client('init', cwd=path)
+
+    def _vcs_destroy(self):
+        vcs_dir = os.path.join(self.repo, '.hg')
+        if os.path.exists(vcs_dir):
+            shutil.rmtree(vcs_dir)
+
+    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):
+        self.__updated.append(path) # work around http://mercurial.selenic.com/bts/issue618
+
+    def _vcs_get_file_contents(self, path, revision=None):
+        if revision == None:
+            return base.VCS._vcs_get_file_contents(self, path, revision)
+        else:
+            return self._u_invoke_client('cat', '-r', revision, path)
+
+    def _vcs_path(self, id, revision):
+        manifest = self._u_invoke_client(
+            'manifest', '--rev', revision).splitlines()
+        return self._u_find_id_from_manifest(id, manifest, revision=revision)
+
+    def _vcs_isdir(self, path, revision):
+        output = self._u_invoke_client('manifest', '--rev', revision)
+        files = output.splitlines()
+        if path in files:
+            return False
+        return True
+
+    def _vcs_listdir(self, path, revision):
+        output = self._u_invoke_client('manifest', '--rev', revision)
+        files = output.splitlines()
+        path = path.rstrip(os.path.sep) + os.path.sep
+        return [self._u_rel_path(f, path) for f in files if f.startswith(path)]
+
+    def _vcs_commit(self, commitfile, allow_empty=False):
+        args = ['commit', '--logfile', commitfile]
+        output = self._u_invoke_client(*args)
+        # work around http://mercurial.selenic.com/bts/issue618
+        strings = ['nothing changed']
+        if self._u_any_in_string(strings, output) == True \
+                and len(self.__updated) > 0:
+            time.sleep(1)
+            for path in self.__updated:
+                os.utime(os.path.join(self.repo, path), None)
+            output = self._u_invoke_client(*args)
+        self.__updated = []
+        # end work around
+        if allow_empty == False:
+            strings = ['nothing changed']
+            if self._u_any_in_string(strings, output) == True:
+                raise base.EmptyCommit()
+        return self._vcs_revision_id(-1)
+
+    def _vcs_revision_id(self, index, style='id'):
+        if index > 0:
+            index -= 1
+        args = ['identify', '--rev', str(int(index)), '--%s' % style]
+        output = self._u_invoke_client(*args)
+        id = output.strip()
+        if id == '000000000000':
+            return None # before initial commit.
+        return id
+
+    def _diff(self, revision):
+        return self._u_invoke_client(
+            'diff', '-r', revision, '--git')
+
+    def _parse_diff(self, diff_text):
+        """_parse_diff(diff_text) -> (new,modified,removed)
+
+        `new`, `modified`, and `removed` are lists of files.
+
+        Example diff text::
+                
+          diff --git a/.be/dir/bugs/modified b/.be/dir/bugs/modified
+          --- a/.be/dir/bugs/modified
+          +++ b/.be/dir/bugs/modified
+          @@ -1,1 +1,1 @@ some value to be modified
+          -some value to be modified
+          \ No newline at end of file
+          +a new value
+          \ No newline at end of file
+          diff --git a/.be/dir/bugs/moved b/.be/dir/bugs/moved
+          deleted file mode 100644
+          --- a/.be/dir/bugs/moved
+          +++ /dev/null
+          @@ -1,1 +0,0 @@
+          -this entry will be moved
+          \ No newline at end of file
+          diff --git a/.be/dir/bugs/moved2 b/.be/dir/bugs/moved2
+          new file mode 100644
+          --- /dev/null
+          +++ b/.be/dir/bugs/moved2
+          @@ -0,0 +1,1 @@
+          +this entry will be moved
+          \ No newline at end of file
+          diff --git a/.be/dir/bugs/new b/.be/dir/bugs/new
+          new file mode 100644
+          --- /dev/null
+          +++ b/.be/dir/bugs/new
+          @@ -0,0 +1,1 @@
+          +this entry is new
+          \ No newline at end of file
+          diff --git a/.be/dir/bugs/removed b/.be/dir/bugs/removed
+          deleted file mode 100644
+          --- a/.be/dir/bugs/removed
+          +++ /dev/null
+          @@ -1,1 +0,0 @@
+          -this entry will be deleted
+          \ No newline at end of file
+        """
+        new = []
+        modified = []
+        removed = []
+        lines = diff_text.splitlines()
+        for i,line in enumerate(lines):
+            if not line.startswith('diff '):
+                continue
+            file_a,file_b = line.split()[-2:]
+            assert file_a.startswith('a/'), \
+                'missformed file_a %s' % file_a
+            assert file_b.startswith('b/'), \
+                'missformed file_a %s' % file_b
+            file = file_a[2:]
+            assert file_b[2:] == file, \
+                'diff file missmatch %s != %s' % (file_a, file_b)
+            if lines[i+1].startswith('new '):
+                new.append(file)
+            elif lines[i+1].startswith('deleted '):
+                removed.append(file)
+            else:
+                modified.append(file)
+        return (new,modified,removed)
+
+    def _vcs_changed(self, revision):
+        return self._parse_diff(self._diff(revision))
+
+\f
+if libbe.TESTING == True:
+    base.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/ui/__init__.py b/libbe/ui/__init__.py
new file mode 100644 (file)
index 0000000..3b461a5
--- /dev/null
@@ -0,0 +1,15 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# 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.
diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py
new file mode 100644 (file)
index 0000000..dd10954
--- /dev/null
@@ -0,0 +1,340 @@
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Chris Ball <cjb@laptop.org>
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+"""
+A command line interface to Bugs Everywhere.
+"""
+
+import optparse
+import os
+import sys
+
+import libbe
+import libbe.bugdir
+import libbe.command
+import libbe.command.util
+import libbe.version
+import libbe.ui.util.pager
+
+if libbe.TESTING == True:
+    import doctest
+
+class CallbackExit (Exception):
+    pass
+
+class CmdOptionParser(optparse.OptionParser):
+    def __init__(self, command):
+        self.command = command
+        optparse.OptionParser.__init__(self)
+        self.remove_option('-h')
+        self.disable_interspersed_args()
+        self._option_by_name = {}
+        for option in self.command.options:
+            self._add_option(option)
+        self.set_usage(command.usage())
+
+
+    def _add_option(self, option):
+        option.validate()
+        self._option_by_name[option.name] = option
+        long_opt = '--%s' % option.name
+        if option.short_name != None:
+            short_opt = '-%s' % option.short_name
+        assert '_' not in option.name, \
+            'Non-reconstructable option name %s' % option.name
+        kwargs = {'dest':option.name.replace('-', '_'),
+                  'help':option.help}
+        if option.arg == None: # a callback option
+            kwargs['action'] = 'callback'
+            kwargs['callback'] = self.callback
+        elif option.arg.type == 'bool':
+            kwargs['action'] = 'store_true'
+            kwargs['metavar'] = None
+            kwargs['default'] = False
+        else:
+            kwargs['type'] = option.arg.type
+            kwargs['action'] = 'store'
+            kwargs['metavar'] = option.arg.metavar
+            kwargs['default'] = option.arg.default
+        if option.short_name != None:
+            opt = optparse.Option(short_opt, long_opt, **kwargs)
+        else:
+            opt = optparse.Option(long_opt, **kwargs)
+        opt._option = option
+        self.add_option(opt)
+
+    def parse_args(self, args=None, values=None):
+        args = self._get_args(args)
+        options,parsed_args = optparse.OptionParser.parse_args(
+            self, args=args, values=values)
+        options = options.__dict__
+        for name,value in options.items():
+            if '_' in name: # reconstruct original option name
+                options[name.replace('_', '-')] = options.pop(name)
+        for name,value in options.items():
+            if value == '--complete':
+                argument = None
+                option = self._option_by_name[name]
+                if option.arg != None:
+                    argument = option.arg
+                fragment = None
+                indices = [i for i,arg in enumerate(args)
+                           if arg == '--complete']
+                for i in indices:
+                    assert i > 0  # this --complete is an option value
+                    if args[i-1] in ['--%s' % o.name
+                                     for o in self.command.options]:
+                        name = args[i-1][2:]
+                        if name == option.name:
+                            break
+                    elif option.short_name != None \
+                            and args[i-1].startswith('-') \
+                            and args[i-1].endswith(option.short_name):
+                        break
+                if i+1 < len(args):
+                    fragment = args[i+1]
+                self.complete(argument, fragment)
+        for i,arg in enumerate(parsed_args):
+            if arg == '--complete':
+                if i > 0 and self.command.name == 'be':
+                    break # let this pass through for the command parser to handle
+                elif i < len(self.command.args):
+                    argument = self.command.args[i]
+                elif len(self.command.args) == 0:
+                    break # command doesn't take arguments
+                else:
+                    argument = self.command.args[-1]
+                    if argument.repeatable == False:
+                        raise libbe.command.UserError('Too many arguments')
+                fragment = None
+                if i < len(parsed_args) - 1:
+                    fragment = parsed_args[i+1]
+                self.complete(argument, fragment)
+        if len(parsed_args) > len(self.command.args) \
+                and self.command.args[-1].repeatable == False:
+            raise libbe.command.UserError('Too many arguments')
+        for arg in self.command.args[len(parsed_args):]:
+            if arg.optional == False:
+                raise libbe.command.UserError(
+                    'Missing required argument %s' % arg.metavar)
+        return (options, parsed_args)
+
+    def callback(self, option, opt, value, parser):
+        command_option = option._option
+        if command_option.name == 'complete':
+            argument = None
+            fragment = None
+            if len(parser.rargs) > 0:
+                fragment = parser.rargs[0]
+            self.complete(argument, fragment)
+        else:
+            print >> self.command.stdout, command_option.callback(
+                self.command, command_option, value)
+        raise CallbackExit
+
+    def complete(self, argument=None, fragment=None):
+        comps = self.command.complete(argument, fragment)
+        if fragment != None:
+            comps = [c for c in comps if c.startswith(fragment)]
+        if len(comps) > 0:
+            print >> self.command.stdout, '\n'.join(comps)
+        raise CallbackExit
+
+class BE (libbe.command.Command):
+    """Class for parsing the command line arguments for `be`.
+    This class does not contain a useful _run() method.  Call this
+    module's main() function instead.
+
+    >>> ui = libbe.command.UserInterface()
+    >>> ui.io.stdout = sys.stdout
+    >>> be = BE(ui=ui)
+    >>> ui.io.setup_command(be)
+    >>> p = CmdOptionParser(be)
+    >>> p.exit_after_callback = False
+    >>> try:
+    ...     options,args = p.parse_args(['--help']) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    ... except CallbackExit:
+    ...     pass
+    usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]
+    <BLANKLINE>
+    Options:
+      -h, --help         Print a help message.
+    <BLANKLINE>
+      --complete         Print a list of possible completions.
+    <BLANKLINE>
+      --version          Print version string.
+    ...
+    >>> try:
+    ...     options,args = p.parse_args(['--complete']) # doctest: +ELLIPSIS
+    ... except CallbackExit:
+    ...     print '  got callback'
+    --help
+    --complete
+    --version
+    ...
+    subscribe
+    tag
+    target
+      got callback
+    """
+    name = 'be'
+
+    def __init__(self, *args, **kwargs):
+        libbe.command.Command.__init__(self, *args, **kwargs)
+        self.options.extend([
+                libbe.command.Option(name='version',
+                    help='Print version string.',
+                    callback=self.version),
+                libbe.command.Option(name='full-version',
+                    help='Print full version information.',
+                    callback=self.full_version),
+                libbe.command.Option(name='repo', short_name='r',
+                    help='Select BE repository (see `be help repo`) rather '
+                         'than the current directory.',
+                    arg=libbe.command.Argument(
+                        name='repo', metavar='REPO', default='.',
+                        completion_callback=libbe.command.util.complete_path)),
+                libbe.command.Option(name='paginate',
+                    help='Pipe all output into less (or if set, $PAGER).'),
+                libbe.command.Option(name='no-pager',
+                    help='Do not pipe git output into a pager.'),
+                ])
+        self.args.extend([
+                libbe.command.Argument(
+                    name='command', optional=False,
+                    completion_callback=libbe.command.util.complete_command),
+                libbe.command.Argument(
+                    name='args', optional=True, repeatable=True)
+                ])
+
+    def usage(self):
+        return 'usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]'
+
+    def _long_help(self):
+        cmdlist = []
+        for name in libbe.command.commands():
+            Class = libbe.command.get_command_class(command_name=name)
+            assert hasattr(Class, '__doc__') and Class.__doc__ != None, \
+                'Command class %s missing docstring' % Class
+            cmdlist.append((name, Class.__doc__.splitlines()[0]))
+        cmdlist.sort()
+        longest_cmd_len = max([len(name) for name,desc in cmdlist])
+        ret = ['Bugs Everywhere - Distributed bug tracking',
+               '', 'Supported commands']
+        for name, desc in cmdlist:
+            numExtraSpaces = longest_cmd_len-len(name)
+            ret.append('be %s%*s    %s' % (name, numExtraSpaces, '', desc))
+        ret.extend(['', 'Run', '  be help [command]', 'for more information.'])
+        return '\n'.join(ret)
+
+    def version(self, *args):
+        return libbe.version.version(verbose=False)
+
+    def full_version(self, *args):
+        return libbe.version.version(verbose=True)
+
+class CommandLine (libbe.command.UserInterface):
+    def __init__(self, *args, **kwargs):
+        libbe.command.UserInterface.__init__(self, *args, **kwargs)
+        self.restrict_file_access = False
+        self.storage_callbacks = None
+    def help(self):
+        be = BE(ui=self)
+        self.setup_command(be)
+        return be.help()
+
+def dispatch(ui, command, args):
+    parser = CmdOptionParser(command)
+    try:
+        options,args = parser.parse_args(args)
+        ret = ui.run(command, options, args)
+    except CallbackExit:
+        return 0
+    except UnicodeDecodeError, e:
+        print >> ui.io.stdout, '\n'.join([
+                'ERROR:', str(e),
+                'You should set a locale that supports unicode, e.g.',
+                '  export LANG=en_US.utf8',
+                'See http://docs.python.org/library/locale.html for details',
+                ])
+        return 1
+    except libbe.command.UserError, e:
+        print >> ui.io.stdout, 'ERROR:\n', e
+        return 1
+    except libbe.storage.ConnectionError, e:
+        print >> ui.io.stdout, 'Connection Error:\n', e
+        return 1
+    except (libbe.util.id.MultipleIDMatches, libbe.util.id.NoIDMatches,
+            libbe.util.id.InvalidIDStructure), e:
+        print >> ui.io.stdout, 'Invalid id:\n', e
+        return 1
+    finally:
+        command.cleanup()
+    return ret
+
+def main():
+    io = libbe.command.StdInputOutput()
+    ui = CommandLine(io)
+    be = BE(ui=ui)
+    ui.setup_command(be)
+
+    parser = CmdOptionParser(be)
+    try:
+        options,args = parser.parse_args()
+    except CallbackExit:
+        return 0
+    except libbe.command.UserError, e:
+        if str(e).endswith('COMMAND'):
+            # no command given, print usage string
+            print >> ui.io.stdout, 'ERROR:'
+            print >> ui.io.stdout, be.usage(), '\n', e
+            print >> ui.io.stdout, 'For example, try'
+            print >> ui.io.stdout, '  be help'
+        else:
+            print >> ui.io.stdout, 'ERROR:\n', e
+        return 1
+
+    command_name = args.pop(0)
+    try:
+        Class = libbe.command.get_command_class(command_name=command_name)
+    except libbe.command.UnknownCommand, e:
+        print >> ui.io.stdout, e
+        return 1
+
+    ui.storage_callbacks = libbe.command.StorageCallbacks(options['repo'])
+    command = Class(ui=ui)
+    ui.setup_command(command)
+
+    if command.name in ['comment', 'commit', 'import-xml', 'serve']:
+        paginate = 'never'
+    else:
+        paginate = 'auto'
+    if options['paginate'] == True:
+        paginate = 'always'
+    if options['no-pager'] == True:
+        paginate = 'never'
+    libbe.ui.util.pager.run_pager(paginate)
+
+    ret = dispatch(ui, command, args)
+    ui.cleanup()
+    return ret
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/libbe/ui/util/__init__.py b/libbe/ui/util/__init__.py
new file mode 100644 (file)
index 0000000..3b461a5
--- /dev/null
@@ -0,0 +1,15 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# 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.
similarity index 79%
rename from libbe/editor.py
rename to libbe/ui/util/editor.py
index ec410061fd79b384e4b83165c538966cf88b49dd..1a430c703728934db321abc2e614e50080ccb9d3 100644 (file)
@@ -1,5 +1,6 @@
 # Bugs Everywhere, a distributed bugtracker
-# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
 #
 # 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
@@ -25,10 +26,13 @@ import locale
 import os
 import sys
 import tempfile
-import doctest
 
+import libbe
+import libbe.util.encoding
+
+if libbe.TESTING == True:
+    import doctest
 
-default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding()
 
 comment_marker = u"== Anything below this line will be ignored\n"
 
@@ -52,18 +56,20 @@ def editor_string(comment=None, encoding=None):
     >>> os.environ["VISUAL"] = "echo baz > "
     >>> editor_string()
     u'baz\\n'
+    >>> os.environ["VISUAL"] = "echo 'baz\\n== Anything below this line will be ignored\\nHi' > "
+    >>> editor_string()
+    u'baz\\n'
     >>> del os.environ["EDITOR"]
     >>> del os.environ["VISUAL"]
     """
     if encoding == None:
-        encoding = default_encoding
+        encoding = libbe.util.encoding.get_filesystem_encoding()
+    editor = None
     for name in ('VISUAL', 'EDITOR'):
-        try:
+        if name in os.environ and os.environ[name] != '':
             editor = os.environ[name]
             break
-        except KeyError:
-            pass
-    else:
+    if editor == None:
         raise CantFindEditor()
     fhandle, fname = tempfile.mkstemp()
     try:
@@ -73,9 +79,9 @@ def editor_string(comment=None, encoding=None):
         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()
+        output = libbe.util.encoding.get_file_contents(
+            fname, encoding=encoding, decode=True)
+        output = trimmed_string(output)
         if output.rstrip('\n') == "":
             output = None
     finally:
@@ -105,4 +111,5 @@ def trimmed_string(instring):
         out.append(line)
     return ''.join(out)
 
-suite = doctest.DocTestSuite()
+if libbe.TESTING == True:
+    suite = doctest.DocTestSuite()
diff --git a/libbe/ui/util/pager.py b/libbe/ui/util/pager.py
new file mode 100644 (file)
index 0000000..88b58af
--- /dev/null
@@ -0,0 +1,65 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+"""
+Automatic pager for terminal output (a la Git).
+"""
+
+import sys, os, select
+
+# see http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby
+def run_pager(paginate='auto'):
+    """
+    paginate should be one of 'never', 'auto', or 'always'.
+
+    usage: just call this function and continue using sys.stdout like
+    you normally would.
+    """
+    if paginate == 'never' \
+            or sys.platform == 'win32' \
+            or not hasattr(sys.stdout, 'isatty') \
+            or sys.stdout.isatty() == False:
+        return
+
+    if paginate == 'auto':
+        if 'LESS' not in os.environ:
+            os.environ['LESS'] = '' # += doesn't work on undefined var
+        # don't page if the input is short enough
+        os.environ['LESS'] += ' -FRX'
+    if 'PAGER' in os.environ:
+        pager = os.environ['PAGER']
+    else:
+        pager = 'less'
+
+    read_fd, write_fd = os.pipe()
+    if os.fork() == 0:
+        # child process
+        os.close(read_fd)
+        os.close(0)
+        os.dup2(write_fd, 1)
+        os.close(write_fd)
+        if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() == True:
+            os.dup2(1, 2)
+        return
+
+    # parent process, become pager
+    os.close(write_fd)
+    os.dup2(read_fd, 0)
+    os.close(read_fd)
+
+    # Wait until we have input before we start the pager
+    select.select([0], [], [])
+    os.execlp(pager, pager)
diff --git a/libbe/ui/util/user.py b/libbe/ui/util/user.py
new file mode 100644 (file)
index 0000000..460a1dd
--- /dev/null
@@ -0,0 +1,134 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+"""Tools for getting, setting, creating, and parsing the user's ID.
+
+IDs will look like 'John Doe <jdoe@example.com>'.  Note that the
+:mod:`libbe.storage.vcs.arch <Arch VCS backend>` *enforces* IDs with
+this format.
+
+Do not confuse the user IDs discussed in this module, which refer to
+humans, with the "user IDs" discussed in :mod:`libbe.util.id`, which
+are human-readable tags refering to objects.
+"""
+
+try:
+    from email.utils import formataddr, parseaddr
+except ImportErrror: # adjust to old python < 2.5
+    from email.Utils import formataddr, parseaddr
+import os
+import re
+from socket import gethostname
+
+import libbe
+import libbe.storage.util.config
+
+def get_fallback_username():
+    """Return a username extracted from environmental variables.
+    """
+    name = None
+    for env in ["LOGNAME", "USERNAME"]:
+        if os.environ.has_key(env):
+            name = os.environ[env]
+            break
+    assert name != None
+    return name
+
+def get_fallback_email():
+    """Return an email address extracted from environmental variables.
+    """
+    hostname = gethostname()
+    name = get_fallback_username()
+    return "%s@%s" % (name, hostname)
+
+def create_user_id(name, email=None):
+    """Create a user ID string from given `name` and `email` strings.
+
+    Examples
+    --------
+
+    >>> create_user_id("John Doe", "jdoe@example.com")
+    'John Doe <jdoe@example.com>'
+    >>> create_user_id("John Doe")
+    'John Doe'
+
+    See Also
+    --------
+    parse_user_id : inverse
+    """
+    assert len(name) > 0
+    if email == None or len(email) == 0:
+        return name
+    else:
+        return formataddr((name, email))
+
+def parse_user_id(value):
+    """Parse a user ID string into `name` and `email` strings.
+
+    Examples
+    --------
+
+    >>> parse_user_id("John Doe <jdoe@example.com>")
+    ('John Doe', 'jdoe@example.com')
+    >>> parse_user_id("John Doe")
+    ('John Doe', None)
+    >>> parse_user_id("John Doe <jdoe@example.com><what?>")
+    ('John Doe', 'jdoe@example.com')
+    See Also
+    --------
+    create_user_id : inverse
+    """
+    if '<' not in value:
+        return (value, None)
+    return parseaddr(value)
+
+def get_user_id(storage=None):
+    """Return a user ID, checking a list of possible sources.
+
+    The source order is:
+
+    1. Global BE configuration.
+    2. `storage.get_user_id`, if that function is defined.
+    3. :func:`get_fallback_username` and :func:`get_fallback_email`.
+
+    Notes
+    -----
+    Sometimes the storage will keep track of the user ID (e.g. most
+    VCSs, see :meth:`libbe.storage.vcs.base.VCS.get_user_id`).  If so,
+    we prefer that ID to the fallback, since the user has likely
+    configured it directly.
+    """
+    user = libbe.storage.util.config.get_val('user')
+    if user != None:
+        return user
+    if storage != None and hasattr(storage, 'get_user_id'):
+        user = storage.get_user_id()
+        if user != None:
+            return user
+    name = get_fallback_username()
+    email = get_fallback_email()
+    user = create_user_id(name, email)
+    return user
+
+def set_user_id(user_id):
+    """Set the user ID in a user's BE configuration.
+
+    See Also
+    --------
+    libbe.storage.util.config.set_val
+    """
+    user = libbe.storage.util.config.set_val('user', user_id)
diff --git a/libbe/upgrade.py b/libbe/upgrade.py
deleted file mode 100644 (file)
index 4123c72..0000000
+++ /dev/null
@@ -1,187 +0,0 @@
-# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
-#
-# 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/util/__init__.py b/libbe/util/__init__.py
new file mode 100644 (file)
index 0000000..0f4850f
--- /dev/null
@@ -0,0 +1,24 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+"""
+Miscellaneous utilities.
+"""
+
+class InvalidObject (object):
+    """An object that won't come up by accident."""
+    pass
+
similarity index 61%
rename from libbe/encoding.py
rename to libbe/util/encoding.py
index fd513b56331aac3f898153c57da9c1396caddcca..8eea438a05aa1847cc8cf6df71a854a41c41412b 100644 (file)
@@ -1,5 +1,5 @@
-# Bugs Everywhere, a distributed bugtracker
-# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
 #
 # 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
@@ -22,7 +22,11 @@ Support input/output/filesystem encodings (e.g. UTF-8).
 import codecs
 import locale
 import sys
-import doctest
+import types
+
+import libbe
+if libbe.TESTING == True:
+    import doctest
 
 
 ENCODING = None # override get_encoding() output by setting this
@@ -40,6 +44,15 @@ def get_encoding():
         # Python 2.3 on windows doesn't know about 'XYZ' alias for 'cpXYZ'
     return encoding
 
+def get_input_encoding():
+    return get_encoding()
+
+def get_output_encoding():
+    return get_encoding()
+
+def get_filesystem_encoding():
+    return get_encoding()
+
 def known_encoding(encoding):
     """
     >>> known_encoding("highly-unlikely-encoding")
@@ -53,9 +66,26 @@ def known_encoding(encoding):
     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__)
+def get_file_contents(path, mode='r', encoding=None, decode=False):
+    if decode == True:
+        if encoding == None:
+            encoding = get_filesystem_encoding()
+        f = codecs.open(path, mode, encoding)
+    else:
+        f = open(path, mode)
+    contents = f.read()
+    f.close()
+    return contents
+
+def set_file_contents(path, contents, mode='w', encoding=None):
+    if type(contents) == types.UnicodeType:
+        if encoding == None:
+            encoding = get_filesystem_encoding()
+        f = codecs.open(path, mode, encoding)
+    else:
+        f = open(path, mode)
+    f.write(contents)
+    f.close()
 
-suite = doctest.DocTestSuite()
+if libbe.TESTING == True:
+    suite = doctest.DocTestSuite()
diff --git a/libbe/util/id.py b/libbe/util/id.py
new file mode 100644 (file)
index 0000000..9192ac8
--- /dev/null
@@ -0,0 +1,713 @@
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# 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 ID creation and parsing.
+
+Format
+======
+
+BE IDs are formatted::
+
+    <bug-directory>[/<bug>[/<comment>]]
+
+where each ``<..>`` is a UUID.  For example::
+
+    bea86499-824e-4e77-b085-2d581fa9ccab/3438b72c-6244-4f1d-8722-8c8d41484e35
+
+refers to bug ``3438b72c-6244-4f1d-8722-8c8d41484e35`` which is
+located in bug directory ``bea86499-824e-4e77-b085-2d581fa9ccab``.
+This is a bit of a mouthful, so you can truncate each UUID so long as
+it remains unique.  For example::
+
+    bea/343
+
+If there were two bugs ``3438...`` and ``343a...`` in ``bea``, you'd
+have to use::
+
+    bea/3438
+
+BE will only truncate each UUID down to three characters to slightly
+future-proof the short user ids.  However, if you want to save keystrokes
+and you *know* there is only one bug directory, feel free to truncate
+all the way to zero characters::
+
+    /3438
+
+Cross references
+================
+
+To refer to other bug-directories/bugs/comments from bug comments, simply
+enclose the ID in pound signs (``#``).  BE will automatically expand the
+truncations to the full UUIDs before storing the comment, and the reference
+will be appropriately truncated (and hyperlinked, if possible) when the
+comment is displayed.
+
+Scope
+=====
+
+Although bug and comment IDs always appear in compound references,
+UUIDs at each level are globally unique.  For example, comment
+``bea/343/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46`` will *only* appear
+under ``bea/343``.  The prefix (``bea/343``) allows BE to reduce
+caching global comment-lookup tables and enables easy error messages
+("I couldn't find ``bea/343/ba9`` because I don't know where the
+``bea`` bug directory is located").
+"""
+
+import os.path
+import re
+
+import libbe
+
+if libbe.TESTING == True:
+    import doctest
+    import sys
+    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')
+
+
+HIERARCHY = ['bugdir', 'bug', 'comment']
+"""Keep track of the object type hierarchy.
+"""
+
+class MultipleIDMatches (ValueError):
+    """Multiple IDs match the given user ID.
+
+    Parameters
+    ----------
+    id : str
+      The not-specific-enough truncated UUID.
+    common : str
+      The initial characters common to all matching UUIDs.
+    matches : list of str
+      The list of possibly matching UUIDs.
+    """
+    def __init__(self, id, common, matches):
+        msg = ('More than one id matches %s.  '
+               'Please be more specific (%s*).\n%s' % (id, common, matches))
+        ValueError.__init__(self, msg)
+        self.id = id
+        self.common = common
+        self.matches = matches
+
+class NoIDMatches (KeyError):
+    """No IDs match the given user ID.
+
+    Parameters
+    ----------
+    id : str
+      The not-matching, possibly truncated UUID.
+    possible_ids : list of str
+      The list of potential UUIDs at that level.
+    msg : str, optional
+      A helpful message explaining what went wrong.
+    """
+    def __init__(self, id, possible_ids, msg=None):
+        KeyError.__init__(self, id)
+        self.id = id
+        self.possible_ids = possible_ids
+        self.msg = msg
+    def __str__(self):
+        if self.msg == None:
+            return 'No id matches %s.\n%s' % (self.id, self.possible_ids)
+        return self.msg
+
+class InvalidIDStructure (KeyError):
+    """A purported ID does not have the appropriate syntax.
+
+    Parameters
+    ----------
+    id : str
+      The purported ID.
+    msg : str, optional
+      A helpful message explaining what went wrong.
+    """
+    def __init__(self, id, msg=None):
+        KeyError.__init__(self, id)
+        self.id = id
+        self.msg = msg
+    def __str__(self):
+        if self.msg == None:
+            return 'Invalid id structure "%s"' % self.id
+        return self.msg
+
+def _assemble(args, check_length=False):
+    """Join a bunch of level UUIDs into a single ID.
+
+    See Also
+    --------
+    _split : inverse
+    """
+    args = list(args)
+    for i,arg in enumerate(args):
+        if arg == None:
+            args[i] = ''
+    id = '/'.join(args)
+    if check_length == True:
+        assert len(args) > 0, args
+        if len(args) > len(HIERARCHY):
+            raise InvalidIDStructure(
+                id, '%d > %d levels in "%s"' % (len(args), len(HIERARCHY), id))
+    return id
+
+def _split(id, check_length=False):
+    """Split an ID into a list of level UUIDs.
+
+    See Also
+    --------
+    _assemble : inverse
+    """
+    args = id.split('/')
+    for i,arg in enumerate(args):
+        if arg == '':
+            args[i] = None
+    if check_length == True:
+        assert len(args) > 0, args
+        if len(args) > len(HIERARCHY):
+            raise InvalidIDStructure(
+                id, '%d > %d levels in "%s"' % (len(args), len(HIERARCHY), id))
+    return args
+
+def _truncate(uuid, other_uuids, min_length=3):
+    """Truncate a UUID to the shortest length >= `min_length` such that it
+    is *not* a truncated form of a UUID in `other_uuids`.
+
+    Parameters
+    ----------
+    uuid : str
+      The UUID to truncate.
+    other_uuids : list of str
+      The other UUIDs which the truncation *might* (but doesn't) refer
+      to.
+    min_length : int
+      Avoid rapidly outdated truncations, even if they are unique now.
+
+    See Also
+    --------
+    _expand : inverse
+    """
+    if min_length == -1:
+        return uuid
+    chars = min_length
+    for id in other_uuids:
+        if id == uuid:
+            continue
+        while (id[:chars] == uuid[:chars]):
+            chars+=1
+    return uuid[:chars]
+
+def _expand(truncated_id, common, other_ids):
+    """Expand a truncated UUID.
+
+    Parameters
+    ----------
+    truncated_id : str
+      The ID to expand.
+    common : str
+      The common portion `truncated_id` shares with the UUIDs in
+      `other_ids`.  Not used by ``_expand``, but passed on to the
+      matching exceptions if they occur.
+    other_uuids : list of str
+      The other UUIDs which the truncation *might* (but doesn't) refer
+      to.
+
+    Raises
+    ------
+    NoIDMatches
+    MultipleIDMatches
+
+    See Also
+    --------
+    _expand : inverse
+    """
+    other_ids = list(other_ids)
+    if len(other_ids) == 0:
+        raise NoIDMatches(truncated_id, other_ids)
+    if truncated_id == None:
+        if len(other_ids) == 1:
+            return other_ids[0]
+        raise MultipleIDMatches(truncated_id, common, other_ids)
+    matches = []
+    other_ids = list(other_ids)
+    for id in other_ids:
+        if id.startswith(truncated_id):
+            if id == truncated_id:
+                return id
+            matches.append(id)
+    if len(matches) > 1:
+        raise MultipleIDMatches(truncated_id, common, matches)
+    if len(matches) == 0:
+        raise NoIDMatches(truncated_id, other_ids)
+    return matches[0]
+
+
+class ID (object):
+    """Store an object ID and produce various representations.
+
+    Parameters
+    ----------
+    object : :class:`~libbe.bugdir.BugDir` or :class:`~libbe.bug.Bug` or :class:`~libbe.comment.Comment`
+      The object that the ID applies to.
+    type : 'bugdir' or 'bug' or 'comment'
+      The type of the object.
+
+    Notes
+    -----
+
+    IDs have several formats specialized for different uses.
+
+    In storage, all objects are represented by their uuid alone,
+    because that is the simplest globally unique identifier.  You can
+    generate ids of this sort with the .storage() method.  Because an
+    object's storage may be distributed across several chunks, and the
+    chunks may not have their own uuid, we generate chunk ids by
+    prepending the objects uuid to the chunk name.  The user id types
+    do not support this chunk extension feature.
+
+    For users, the full uuids are a bit overwhelming, so we truncate
+    them while retaining local uniqueness (with regards to the other
+    objects currently in storage).  We also prepend truncated parent
+    ids for two reasons:
+
+    1. So that a user can locate the repository containing the
+       referenced object.  It would be hard to find bug ``XYZ`` if
+       that's all you knew.  Much easier with ``ABC/XYZ``, where
+       ``ABC`` is the bugdir.  Each project can publish a list of
+       bugdir-id-to-location mappings, e.g.::
+
+            ABC...(full uuid)...DEF   https://server.com/projectX/be/
+
+       which is easier than publishing all-object-ids-to-location
+       mappings.
+
+    2. Because it's easier to generate and parse truncated ids if you
+       don't have to fetch all the ids in the storage repository but
+       can restrict yourself to a specific branch.
+
+    You can generate ids of this sort with the :meth:`user` method,
+    although in order to preform the truncation, your object (and its
+    parents must define a `sibling_uuids` method.
+
+    While users can use the convenient short user ids in the short
+    term, the truncation will inevitably lead to name collision.  To
+    avoid that, we provide a non-truncated form of the short user ids
+    via the :meth:`long_user` method.  These long user ids should be
+    converted to short user ids by intelligent user interfaces.
+
+    See Also
+    --------
+    parse_user : get uuids back out of the user ids.
+    short_to_long_user : convert a single short user id to a long user id.
+    long_to_short_user : convert a single long user id to a short user id.
+    short_to_long_text : scan text for user ids & convert to long user ids.
+    long_to_short_text : scan text for long user ids & convert to short user ids.
+    """
+    def __init__(self, object, type):
+        self._object = object
+        self._type = type
+        assert self._type in HIERARCHY, self._type
+
+    def storage(self, *args):
+        return _assemble([self._object.uuid]+list(args))
+
+    def _ancestors(self):
+        ret = [self._object]
+        index = HIERARCHY.index(self._type)
+        if index == 0:
+            return ret
+        o = self._object
+        for i in range(index, 0, -1):
+            parent_name = HIERARCHY[i-1]
+            o = getattr(o, parent_name, None)
+            ret.insert(0, o)
+        return ret
+
+    def long_user(self):
+        return _assemble([o.uuid for o in self._ancestors()],
+                         check_length=True)
+
+    def user(self):
+        ids = []
+        for o in self._ancestors():
+            if o == None:
+                ids.append(None)
+            else:
+                ids.append(_truncate(o.uuid, o.sibling_uuids()))
+        return _assemble(ids, check_length=True)
+
+def child_uuids(child_storage_ids):
+    """Extract uuid children from other children generated by
+    :meth:`ID.storage`.
+
+    This is useful for separating data belonging to a particular
+    object directly from entries for its child objects.  Since the
+    :class:`~libbe.storage.base.Storage` backend doesn't distinguish
+    between the two.
+
+    Examples
+    --------
+
+    >>> list(child_uuids(['abc123/values', '123abc', '123def']))
+    ['123abc', '123def']
+    """
+    for id in child_storage_ids:
+        fields = _split(id)
+        if len(fields) == 1:
+            yield fields[0]
+
+def long_to_short_user(bugdirs, id):
+    """Convert a long user ID to a short user ID (see :class:`ID`).
+    The list of bugdirs allows uniqueness-maintaining truncation of
+    the bugdir portion of the ID.
+
+    See Also
+    --------
+    short_to_long_user : inverse
+    long_to_short_text : conversion on a block of text
+    """
+    ids = _split(id, check_length=True)
+    matching_bugdirs = [bd for bd in bugdirs if bd.uuid == ids[0]]
+    if len(matching_bugdirs) == 0:
+        raise NoIDMatches(id, [bd.uuid for bd in bugdirs])
+    elif len(matching_bugdirs) > 1:
+        raise MultipleIDMatches(id, '', [bd.uuid for bd in bugdirs])
+    bugdir = matching_bugdirs[0]
+    objects = [bugdir]
+    if len(ids) >= 2:
+        bug = bugdir.bug_from_uuid(ids[1])
+        objects.append(bug)
+    if len(ids) >= 3:
+        comment = bug.comment_from_uuid(ids[2])
+        objects.append(comment)
+    for i,obj in enumerate(objects):
+        ids[i] = _truncate(ids[i], obj.sibling_uuids())
+    return _assemble(ids)
+
+def short_to_long_user(bugdirs, id):
+    """Convert a short user ID to a long user ID (see :class:`ID`).  The
+    list of bugdirs allows uniqueness-checking during expansion of the
+    bugdir portion of the ID.
+
+    See Also
+    --------
+    long_to_short_user : inverse
+    short_to_long_text : conversion on a block of text
+    """
+    ids = _split(id, check_length=True)
+    ids[0] = _expand(ids[0], common=None,
+                     other_ids=[bd.uuid for bd in bugdirs])
+    if len(ids) == 1:
+        return _assemble(ids)
+    bugdir = [bd for bd in bugdirs if bd.uuid == ids[0]][0]
+    ids[1] = _expand(ids[1], common=bugdir.id.user(),
+                     other_ids=bugdir.uuids())
+    if len(ids) == 2:
+        return _assemble(ids)
+    bug = bugdir.bug_from_uuid(ids[1])
+    ids[2] = _expand(ids[2], common=bug.id.user(),
+                     other_ids=bug.uuids())
+    return _assemble(ids)
+
+
+REGEXP = '#([-a-f0-9]*)(/[-a-g0-9]*)?(/[-a-g0-9]*)?#'
+"""Regular expression for matching IDs (both short and long) in text.
+"""
+
+class IDreplacer (object):
+    """Helper class for ID replacement in text.
+
+    Reassembles the match elements from :data:`REGEXP` matching
+    into the original ID, for easier replacement.
+
+    See Also
+    --------
+    short_to_long_text, long_to_short_text
+    """
+    def __init__(self, bugdirs, replace_fn, wrap=True):
+        self.bugdirs = bugdirs
+        self.replace_fn = replace_fn
+        self.wrap = wrap
+    def __call__(self, match):
+        ids = []
+        for m in match.groups():
+            if m == None:
+                m = ''
+            ids.append(m)
+        replacement = self.replace_fn(self.bugdirs, ''.join(ids))
+        if self.wrap == True:
+            return '#%s#' % replacement
+        return replacement
+
+def short_to_long_text(bugdirs, text):
+    """Convert short user IDs to long user IDs in text (see :class:`ID`).
+    The list of bugdirs allows uniqueness-checking during expansion of
+    the bugdir portion of the ID.
+
+    See Also
+    --------
+    short_to_long_user : conversion on a single ID
+    long_to_short_text : inverse
+    """
+    return re.sub(REGEXP, IDreplacer(bugdirs, short_to_long_user), text)
+
+def long_to_short_text(bugdirs, text):
+    """Convert long user IDs to short user IDs in text (see :class:`ID`).
+    The list of bugdirs allows uniqueness-maintaining truncation of
+    the bugdir portion of the ID.
+
+    See Also
+    --------
+    long_to_short_user : conversion on a single ID
+    short_to_long_text : inverse
+    """
+    return re.sub(REGEXP, IDreplacer(bugdirs, long_to_short_user), text)
+
+def residual(base, fragment):
+    """Split the short ID `fragment` into a portion corresponding
+    to `base`, and a portion inside `base`.
+
+    Examples
+    --------
+
+    >>> residual('ABC/DEF/', '//GHI')
+    ('//', 'GHI')
+    >>> residual('ABC/DEF/', '/D/GHI')
+    ('/D/', 'GHI')
+    >>> residual('ABC/DEF', 'A/D/GHI')
+    ('A/D/', 'GHI')
+    >>> residual('ABC/DEF', 'A/D/GHI/JKL')
+    ('A/D/', 'GHI/JKL')
+    """
+    base = base.rstrip('/') + '/'
+    ids = fragment.split('/')
+    base_count = base.count('/')
+    root_ids = ids[:base_count] + ['']
+    residual_ids = ids[base_count:]
+    return ('/'.join(root_ids), '/'.join(residual_ids))
+
+def _parse_user(id):
+    """Parse a user ID (see :class:`ID`), returning a dict of parsed
+    information.
+
+    The returned dict will contain a value for "type" (from
+    :data:`HIERARCHY`) and values for the levels that are defined.
+
+    Examples
+    --------
+
+    >>> _parse_user('ABC/DEF/GHI') == \\
+    ...     {'bugdir':'ABC', 'bug':'DEF', 'comment':'GHI', 'type':'comment'}
+    True
+    >>> _parse_user('ABC/DEF') == \\
+    ...     {'bugdir':'ABC', 'bug':'DEF', 'type':'bug'}
+    True
+    >>> _parse_user('ABC') == \\
+    ...     {'bugdir':'ABC', 'type':'bugdir'}
+    True
+    >>> _parse_user('') == \\
+    ...     {'bugdir':None, 'type':'bugdir'}
+    True
+    >>> _parse_user('/') == \\
+    ...     {'bugdir':None, 'bug':None, 'type':'bug'}
+    True
+    >>> _parse_user('/DEF/') == \\
+    ...     {'bugdir':None, 'bug':'DEF', 'comment':None, 'type':'comment'}
+    True
+    >>> _parse_user('a/b/c/d')
+    Traceback (most recent call last): 
+      ...
+    InvalidIDStructure: 4 > 3 levels in "a/b/c/d"
+    """
+    ret = {}
+    args = _split(id, check_length=True)
+    for i,(type,arg) in enumerate(zip(HIERARCHY, args)):
+        if arg != None and len(arg) == 0:
+            raise InvalidIDStructure(
+                id, 'Invalid %s part %d "%s" of id "%s"' % (type, i, arg, id))
+        ret['type'] = type
+        ret[type] = arg
+    return ret
+
+def parse_user(bugdir, id):
+    """Parse a user ID (see :class:`ID`), returning a dict of parsed
+    information.
+
+    The returned dict will contain a value for "type" (from
+    :data:`HIERARCHY`) and values for the levels that are defined.
+
+    Notes
+    -----
+    This function tries to expand IDs before parsing, so it can handle
+    both short and long IDs successfully.
+    """
+    long_id = short_to_long_user([bugdir], id)
+    return _parse_user(long_id)
+
+if libbe.TESTING == True:
+    class UUIDtestCase(unittest.TestCase):
+        def testUUID_gen(self):
+            id = uuid_gen()
+            self.failUnless(len(id) == 36, 'invalid UUID "%s"' % id)
+
+    class DummyObject (object):
+        def __init__(self, uuid, parent=None, siblings=[]):
+            self.uuid = uuid
+            self._siblings = siblings
+            if parent == None:
+                type_i = 0
+            else:
+                assert parent.type in HIERARCHY, parent
+                setattr(self, parent.type, parent)
+                type_i = HIERARCHY.index(parent.type) + 1
+            self.type = HIERARCHY[type_i]
+            self.id = ID(self, self.type)
+        def sibling_uuids(self):
+            return self._siblings
+
+    class IDtestCase(unittest.TestCase):
+        def setUp(self):
+            self.bugdir = DummyObject('1234abcd')
+            self.bug = DummyObject('abcdef', self.bugdir, ['a1234', 'ab9876'])
+            self.comment = DummyObject('12345678', self.bug, ['1234abcd', '1234cdef'])
+            self.bd_id = self.bugdir.id
+            self.b_id = self.bug.id
+            self.c_id = self.comment.id
+        def test_storage(self):
+            self.failUnless(self.bd_id.storage() == self.bugdir.uuid,
+                            self.bd_id.storage())
+            self.failUnless(self.b_id.storage() == self.bug.uuid,
+                            self.b_id.storage())
+            self.failUnless(self.c_id.storage() == self.comment.uuid,
+                            self.c_id.storage())
+            self.failUnless(self.bd_id.storage('x', 'y', 'z') == \
+                                '1234abcd/x/y/z',
+                            self.bd_id.storage('x', 'y', 'z'))
+        def test_long_user(self):
+            self.failUnless(self.bd_id.long_user() == self.bugdir.uuid,
+                            self.bd_id.long_user())
+            self.failUnless(self.b_id.long_user() == \
+                                '/'.join([self.bugdir.uuid, self.bug.uuid]),
+                            self.b_id.long_user())
+            self.failUnless(self.c_id.long_user() ==
+                                '/'.join([self.bugdir.uuid, self.bug.uuid,
+                                          self.comment.uuid]),
+                            self.c_id.long_user)
+        def test_user(self):
+            self.failUnless(self.bd_id.user() == '123',
+                            self.bd_id.user())
+            self.failUnless(self.b_id.user() == '123/abc',
+                            self.b_id.user())
+            self.failUnless(self.c_id.user() == '123/abc/12345',
+                            self.c_id.user())
+
+    class ShortLongParseTestCase(unittest.TestCase):
+        def setUp(self):
+            self.bugdir = DummyObject('1234abcd')
+            self.bug = DummyObject('abcdef', self.bugdir, ['a1234', 'ab9876'])
+            self.comment = DummyObject('12345678', self.bug, ['1234abcd', '1234cdef'])
+            self.bd_id = self.bugdir.id
+            self.b_id = self.bug.id
+            self.c_id = self.comment.id
+            self.bugdir.bug_from_uuid = lambda uuid: self.bug
+            self.bugdir.uuids = lambda : self.bug.sibling_uuids() + [self.bug.uuid]
+            self.bug.comment_from_uuid = lambda uuid: self.comment
+            self.bug.uuids = lambda : self.comment.sibling_uuids() + [self.comment.uuid]
+            self.short = 'bla bla #123/abc# bla bla #123/abc/12345# bla bla'
+            self.long = 'bla bla #1234abcd/abcdef# bla bla #1234abcd/abcdef/12345678# bla bla'
+            self.short_id_parse_pairs = [
+                ('', {'bugdir':'1234abcd', 'type':'bugdir'}),
+                ('123/abc', {'bugdir':'1234abcd', 'bug':'abcdef',
+                             'type':'bug'}),
+                ('123/abc/12345', {'bugdir':'1234abcd', 'bug':'abcdef',
+                                   'comment':'12345678', 'type':'comment'}),
+                ]
+            self.short_id_exception_pairs = [
+                ('z', NoIDMatches('z', ['1234abcd'])),
+                ('///', InvalidIDStructure(
+                        '///', msg='4 > 3 levels in "///"')),
+                ('/', MultipleIDMatches(
+                        None, '123', ['a1234', 'ab9876', 'abcdef'])),
+                ('123/', MultipleIDMatches(
+                        None, '123', ['a1234', 'ab9876', 'abcdef'])),
+                ('123/abc/', MultipleIDMatches(
+                        None, '123/abc', ['1234abcd','1234cdef','12345678'])),
+                ]
+        def test_short_to_long_text(self):
+            self.failUnless(short_to_long_text([self.bugdir], self.short) == self.long,
+                            '\n' + self.short + '\n' + short_to_long_text([self.bugdir], self.short) + '\n' + self.long)
+        def test_long_to_short_text(self):
+            self.failUnless(long_to_short_text([self.bugdir], self.long) == self.short,
+                            '\n' + long_to_short_text([self.bugdir], self.long) + '\n' + self.short)
+        def test_parse_user(self):
+            for short_id,parsed in self.short_id_parse_pairs:
+                ret = parse_user(self.bugdir, short_id)
+                self.failUnless(ret == parsed,
+                                'got %s\nexpected %s' % (ret, parsed))
+        def test_parse_user_exceptions(self):
+            for short_id,exception in self.short_id_exception_pairs:
+                try:
+                    ret = parse_user(self.bugdir, short_id)
+                    self.fail('Expected parse_user(bugdir, "%s") to raise %s,'
+                              '\n  but it returned %s'
+                              % (short_id, exception.__class__.__name__, ret))
+                except exception.__class__, e:
+                    for attr in dir(e):
+                        if attr.startswith('_') or attr == 'args':
+                            continue
+                        value = getattr(e, attr)
+                        expected = getattr(exception, attr)
+                        self.failUnless(
+                            value == expected,
+                            'Expected parse_user(bugdir, "%s") %s.%s'
+                            '\n  to be %s, but it is %s\n\n%s'
+                              % (short_id, exception.__class__.__name__,
+                                 attr, expected, value, e))
+
+    unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
+    suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
similarity index 51%
rename from libbe/plugin.py
rename to libbe/util/plugin.py
index d593d69132b115e205b99da55ff5ed72a6f03b02..e598c34e339cefb4e34a9907b1f9c6ffcb047984 100644 (file)
@@ -1,4 +1,5 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
 #                         Marien Zwart <marienz@gentoo.org>
 #                         W. Trevor King <wking@drexel.edu>
 #
@@ -24,54 +25,43 @@ 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('.')
+
+_PLUGIN_PATH = os.path.realpath(
+    os.path.dirname(
+        os.path.dirname(
+            os.path.dirname(__file__))))
+if _PLUGIN_PATH not in sys.path:
+    sys.path.append(_PLUGIN_PATH)
+
+def import_by_name(modname):
+    """
+    >>> mod = import_by_name('libbe.bugdir')
+    >>> 'BugDir' in dir(mod)
+    True
+    >>> import_by_name('libbe.highly_unlikely')
+    Traceback (most recent call last):
+      ...
+    ImportError: No module named highly_unlikely
+    """
+    module = __import__(modname)
+    components = modname.split('.')
     for comp in components[1:]:
         module = getattr(module, comp)
     return module
 
-def iter_plugins(prefix):
+def modnames(prefix):
     """
-    >>> "list" in [n for n,m in iter_plugins("becommands")]
+    >>> 'list' in [n for n in modnames('libbe.command')]
     True
-    >>> "plugin" in [n for n,m in iter_plugins("libbe")]
+    >>> 'plugin' in [n for n in modnames('libbe.util')]
     True
     """
-    modfiles = os.listdir(os.path.join(plugin_path, prefix))
+    components = prefix.split('.')
+    modfiles = os.listdir(os.path.join(_PLUGIN_PATH, *components))
     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("<module 'becommands.list' from ")
-    True
-    """
-    dirprefix = os.path.join(*prefix.split('.'))
-    command_path = os.path.join(plugin_path, dirprefix, name+".py")
-    if os.path.isfile(command_path):
-        return my_import(prefix + "." + name)
-    return None
-
-plugin_path = os.path.realpath(os.path.dirname(os.path.dirname(__file__)))
-if plugin_path not in sys.path:
-    sys.path.append(plugin_path)
-
-suite = doctest.DocTestSuite()
-
-def _test():
-    import doctest
-    doctest.testmod()
-
-if __name__ == "__main__":
-    _test()
+        if modfile.endswith('.py') and modfile != '__init__.py':
+            yield modfile[:-3]
diff --git a/libbe/util/subproc.py b/libbe/util/subproc.py
new file mode 100644 (file)
index 0000000..b02b8e8
--- /dev/null
@@ -0,0 +1,223 @@
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# 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.
+
+"""
+Functions for running external commands in subprocesses.
+"""
+
+from subprocess import Popen, PIPE
+import sys
+
+import libbe
+from encoding import get_encoding
+if libbe.TESTING == True:
+    import doctest
+
+_MSWINDOWS = sys.platform == 'win32'
+_POSIX = not _MSWINDOWS
+
+if _POSIX == True:
+    import os
+    import select
+
+class CommandError(Exception):
+    def __init__(self, command, status, stdout=None, stderr=None):
+        strerror = ['Command failed (%d):\n  %s\n' % (status, stderr),
+                    'while executing\n  %s' % str(command)]
+        Exception.__init__(self, '\n'.join(strerror))
+        self.command = command
+        self.status = status
+        self.stdout = stdout
+        self.stderr = stderr
+
+def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,),
+           cwd=None, unicode_output=True, verbose=False, encoding=None):
+    """
+    expect should be a tuple of allowed exit codes.  cwd should be
+    the directory from which the command will be executed.  When
+    unicode_output == True, convert stdout and stdin strings to
+    unicode before returing them.
+    """
+    if cwd == None:
+        cwd = '.'
+    if verbose == True:
+        print >> sys.stderr, '%s$ %s' % (cwd, ' '.join(args))
+    try :
+        if _POSIX:
+            q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd)
+        else:
+            assert _MSWINDOWS==True, 'invalid platform'
+            # win32 don't have os.execvp() so have to run command in a shell
+            q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr,
+                      shell=True, cwd=cwd)
+    except OSError, e:
+        raise CommandError(args, status=e.args[0], stderr=e)
+    stdout,stderr = q.communicate(input=stdin)
+    status = q.wait()
+    if unicode_output == True:
+        if encoding == None:
+            encoding = get_encoding()
+        if stdout != None:
+            stdout = unicode(stdout, encoding)
+        if stderr != None:
+            stderr = unicode(stderr, encoding)
+    if verbose == True:
+        print >> sys.stderr, '%d\n%s%s' % (status, stdout, stderr)
+    if status not in expect:
+        raise CommandError(args, status, stdout, stderr)
+    return status, stdout, stderr
+
+class Pipe (object):
+    """
+    Simple interface for executing POSIX-style pipes based on the
+    subprocess module.  The only complication is the adaptation of
+    subprocess.Popen._comminucate to listen to the stderrs of all
+    processes involved in the pipe, as well as the terminal process'
+    stdout.  There are two implementations of Pipe._communicate, one
+    for MS Windows, and one for POSIX systems.  The MS Windows
+    implementation is currently untested.
+
+    >>> p = Pipe([['find', '/etc/'], ['grep', '^/etc/ssh$']])
+    >>> p.stdout
+    '/etc/ssh\\n'
+    >>> p.status
+    1
+    >>> p.statuses
+    [1, 0]
+    >>> p.stderrs # doctest: +ELLIPSIS
+    [...find: ...: Permission denied..., '']
+    """
+    def __init__(self, cmds, stdin=None):
+        # spawn processes
+        self._procs = []
+        for cmd in cmds:
+            if len(self._procs) != 0:
+                stdin = self._procs[-1].stdout
+            self._procs.append(Popen(cmd, stdin=stdin, stdout=PIPE, stderr=PIPE))
+
+        self.stdout,self.stderrs = self._communicate(input=None)
+
+        # collect process statuses
+        self.statuses = []
+        self.status = 0
+        for proc in self._procs:
+            self.statuses.append(proc.wait())
+            if self.statuses[-1] != 0:
+                self.status = self.statuses[-1]
+
+    # Code excerpted from subprocess.Popen._communicate()
+    if _MSWINDOWS == True:
+        def _communicate(self, input=None):
+            assert input == None, 'stdin != None not yet supported'
+            # listen to each process' stderr
+            threads = []
+            std_X_arrays = []
+            for proc in self._procs:
+                stderr_array = []
+                thread = Thread(target=proc._readerthread,
+                                args=(proc.stderr, stderr_array))
+                thread.setDaemon(True)
+                thread.start()
+                threads.append(thread)
+                std_X_arrays.append(stderr_array)
+
+            # also listen to the last processes stdout
+            stdout_array = []
+            thread = Thread(target=proc._readerthread,
+                            args=(proc.stdout, stdout_array))
+            thread.setDaemon(True)
+            thread.start()
+            threads.append(thread)
+            std_X_arrays.append(stdout_array)
+
+            # join threads as they die
+            for thread in threads:
+                thread.join()
+
+            # read output from reader threads
+            std_X_strings = []
+            for std_X_array in std_X_arrays:
+                std_X_strings.append(std_X_array[0])
+
+            stdout = std_X_strings.pop(-1)
+            stderrs = std_X_strings
+            return (stdout, stderrs)
+    else:
+        assert _POSIX==True, 'invalid platform'
+        def _communicate(self, input=None):
+            read_set = []
+            write_set = []
+            read_arrays = []
+            stdout = None # Return
+            stderr = None # Return
+
+            if self._procs[0].stdin:
+                # Flush stdio buffer.  This might block, if the user has
+                # been writing to .stdin in an uncontrolled fashion.
+                self._procs[0].stdin.flush()
+                if input:
+                    write_set.append(self._procs[0].stdin)
+                else:
+                    self._procs[0].stdin.close()
+            for proc in self._procs:
+                read_set.append(proc.stderr)
+                read_arrays.append([])
+            read_set.append(self._procs[-1].stdout)
+            read_arrays.append([])
+
+            input_offset = 0
+            while read_set or write_set:
+                try:
+                    rlist, wlist, xlist = select.select(read_set, write_set, [])
+                except select.error, e:
+                    if e.args[0] == errno.EINTR:
+                        continue
+                    raise
+                if self._procs[0].stdin in wlist:
+                    # When select has indicated that the file is writable,
+                    # we can write up to PIPE_BUF bytes without risk
+                    # blocking.  POSIX defines PIPE_BUF >= 512
+                    chunk = input[input_offset : input_offset + 512]
+                    bytes_written = os.write(self.stdin.fileno(), chunk)
+                    input_offset += bytes_written
+                    if input_offset >= len(input):
+                        self._procs[0].stdin.close()
+                        write_set.remove(self._procs[0].stdin)
+                if self._procs[-1].stdout in rlist:
+                    data = os.read(self._procs[-1].stdout.fileno(), 1024)
+                    if data == '':
+                        self._procs[-1].stdout.close()
+                        read_set.remove(self._procs[-1].stdout)
+                    read_arrays[-1].append(data)
+                for i,proc in enumerate(self._procs):
+                    if proc.stderr in rlist:
+                        data = os.read(proc.stderr.fileno(), 1024)
+                        if data == '':
+                            proc.stderr.close()
+                            read_set.remove(proc.stderr)
+                        read_arrays[i].append(data)
+
+            # All data exchanged.  Translate lists into strings.
+            read_strings = []
+            for read_array in read_arrays:
+                read_strings.append(''.join(read_array))
+
+            stdout = read_strings.pop(-1)
+            stderrs = read_strings
+            return (stdout, stderrs)
+
+if libbe.TESTING == True:
+    suite = doctest.DocTestSuite()
similarity index 59%
rename from libbe/tree.py
rename to libbe/util/tree.py
index 06d09e55f7a6073ae84292c7c141ca1553941fcc..812b0bd2a119af79fab4af579b169f0ff9ed6e50 100644 (file)
@@ -1,5 +1,6 @@
 # Bugs Everywhere, a distributed bugtracker
-# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+#                         W. Trevor King <wking@drexel.edu>
 #
 # 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
 # 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.
+"""Define :class:`Tree`, a traversable tree structure.
 """
 
-import doctest
+import libbe
+if libbe.TESTING == True:
+    import doctest
 
 class Tree(list):
-    """
-    Construct
+    """A traversable tree structure.
+
+    Examples
+    --------
+
+    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"
@@ -40,16 +49,31 @@ class Tree(list):
     >>> a.append(c)
     >>> a.append(b)
 
+    Get the longest branch length with
+
     >>> a.branch_len()
     5
+
+    Sort the tree recursively.  Here we sort longest branch length
+    first.
+
     >>> a.sort(key=lambda node : -node.branch_len())
     >>> "".join([node.n for node in a.traverse()])
     'acfhiebdg'
+
+    And here we sort shortest branch length first.
+
     >>> a.sort(key=lambda node : node.branch_len())
     >>> "".join([node.n for node in a.traverse()])
     'abdgcefhi'
+
+    We can also do breadth-first traverses.
+
     >>> "".join([node.n for node in a.traverse(depth_first=False)])
     'abcdefghi'
+
+    Serialize the tree with depth marking branches.
+
     >>> for depth,node in a.thread():
     ...     print "%*s" % (2*depth+1, node.n)
     a
@@ -61,6 +85,10 @@ class Tree(list):
         f
           h
             i
+
+    Flattening the thread disables depth increases except at
+    branch splits.
+
     >>> for depth,node in a.thread(flatten=True):
     ...     print "%*s" % (2*depth+1, node.n)
     a
@@ -72,6 +100,9 @@ class Tree(list):
     f
     h
     i
+
+    We can also check if a node is contained in a tree.
+
     >>> a.has_descendant(g)
     True
     >>> c.has_descendant(g)
@@ -81,21 +112,32 @@ class Tree(list):
     >>> a.has_descendant(a, match_self=True)
     True
     """
+    def __cmp__(self, other):
+        return cmp(id(self), id(other))
+
     def __eq__(self, other):
-        return id(self) == id(other)
+        return self.__cmp__(other) == 0
+
+    def __ne__(self, other):
+        return self.__cmp__(other) != 0
 
     def branch_len(self):
-        """
-        Exhaustive search every time == SLOW.
+        """Return the largest number of nodes from root to leaf (inclusive).
 
-        Use only on small trees, or reimplement by overriding
-        child-addition methods to allow accurate caching.
+        For the tree::
 
-        For the tree
                +-b---d-g
              a-+   +-e
                +-c-+-f-h-i
+
         this method returns 5.
+
+        Notes
+        -----
+        Exhaustive search every time == *slow*.
+
+        Use only on small trees, or reimplement by overriding
+        child-addition methods to allow accurate caching.
         """
         if len(self) == 0:
             return 1
@@ -103,18 +145,30 @@ class Tree(list):
             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.
+        """Sort the tree recursively.
+
+        This method extends :meth:`list.sort` to Trees.
+
+        Notes
+        -----
+        This method can be slow, e.g. on a :meth:`branch_len` sort,
+        since a node at depth `N` from the root has it's
+        :meth:`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.
+        """Generate all the nodes in a tree, starting with the root node.
+
+        Parameters
+        ----------
+        depth_first : bool
+          Depth first by default, but you can set `depth_first` to
+          `False` for breadth first ordering.  Siblings are returned
+          in the order they are stored, so you might want to
+          :meth:`sort` your tree first.
         """
         if depth_first == True:
             yield self
@@ -130,25 +184,31 @@ class Tree(list):
                 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.
+        """Generate a (depth, node) tuple for every node in the tree.
+
+        When `flatten` is `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` is `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.  For example::
+
                       +-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)
+
+        would both produce (after sorting by :meth:`branch_len`)::
+
+            (0, a)
+            (1, b)
+            (1, c)
+            (0, d)
+            (0, e)
+            (0, f)
+
         """
         stack = [] # ancestry of the current node
         if flatten == True:
@@ -173,6 +233,20 @@ class Tree(list):
             stack.append(node)
 
     def has_descendant(self, descendant, depth_first=True, match_self=False):
+        """Check if a node is contained in a tree.
+
+        Parameters
+        ----------
+        descendant : Tree
+          The potential descendant.
+        depth_first : bool
+          The search order.  Set this if you feel depth/breadth would
+          be a faster search.
+        match_self : bool
+          Set to `True` for::
+
+              x.has_descendant(x, match_self=True) -> True
+        """
         if descendant == self:
             return match_self
         for d in self.traverse(depth_first):
@@ -180,4 +254,5 @@ class Tree(list):
                 return True
         return False
 
-suite = doctest.DocTestSuite()
+if libbe.TESTING == True:
+    suite = doctest.DocTestSuite()
similarity index 51%
rename from libbe/utility.py
rename to libbe/util/utility.py
index 1e4351603c1d1a0bd3b9b94bc628bf46eefcba1d..c12e9a247b5fc7c69120dda9665648c2babe701a 100644 (file)
@@ -1,4 +1,5 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Gianluca Montecchi <gian@grys.it>
 #                         W. Trevor King <wking@drexel.edu>
 #
 # This program is free software; you can redistribute it and/or modify
@@ -26,21 +27,46 @@ import shutil
 import tempfile
 import time
 import types
-import doctest
+
+import libbe
+if libbe.TESTING == True:
+    import doctest
+
+class InvalidXML(ValueError):
+    """Invalid XML while parsing for a `*.from_xml()` method.
+
+    Parameters
+    ----------
+    type : str
+        String identifying `*`, e.g. "bug", "comment", ...
+    element : :class:`ElementTree.Element`
+        ElementTree.Element instance which caused the error.
+    error : str
+        Error description.
+    """
+    def __init__(self, type, element, error):
+        msg = 'Invalid %s xml: %s\n  %s\n' \
+            % (type, error, ElementTree.tostring(element))
+        ValueError.__init__(self, msg)
+        self.type = type
+        self.element = element
+        self.error = error
 
 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.
+    of path's parents.  For example::
+
+         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)
@@ -55,7 +81,11 @@ def search_parent_directories(path, filename):
         path = os.path.dirname(path)
 
 class Dir (object):
-    "A temporary directory for testing use"
+    """A temporary directory for testing use.
+
+    Make sure you run :meth:`cleanup` after you're done using the
+    directory.
+    """
     def __init__(self):
         self.path = tempfile.mkdtemp(prefix="BEtest")
         self.removed = False
@@ -67,18 +97,47 @@ class Dir (object):
         return self.path
 
 RFC_2822_TIME_FMT = "%a, %d %b %Y %H:%M:%S +0000"
+"""RFC 2822 [#]_ format string for :func:`time.strftime` and
+:func:`time.strptime`.
 
+.. [#] See `RFC 2822`_, sections 3.3 and A.1.1.
+.. _RFC 2822: http://www.faqs.org/rfcs/rfc2822.html
+"""
 
 def time_to_str(time_val):
-    """Convert a time value into an RFC 2822-formatted string.  This format
-    lacks sub-second data.
+    """Convert a time number into an RFC 2822-formatted string.
+
+    Parameters
+    ----------
+    time_val : float
+      Float seconds since the Epoc, see :func:`time.time`.
+      Note that while `time_val` may contain sub-second data,
+      the output string will not.
+
+    Examples
+    --------
+
     >>> time_to_str(0)
     'Thu, 01 Jan 1970 00:00:00 +0000'
+
+    See Also
+    --------
+    str_to_time : inverse
+    handy_time : localtime string
     """
     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.
+
+    Parameters
+    ----------
+    str_time : str
+      An RFC 2822-formatted string.
+
+    Examples
+    --------
+
     >>> str_to_time("Thu, 01 Jan 1970 00:00:00 +0000")
     0
     >>> q = time.time()
@@ -86,6 +145,10 @@ def str_to_time(str_time):
     True
     >>> str_to_time("Thu, 01 Jan 1970 00:00:00 -1000")
     36000
+
+    See Also
+    --------
+    time_to_str : inverse
     """
     timezone_str = str_time[-5:]
     if timezone_str != "+0000":
@@ -93,14 +156,33 @@ def str_to_time(str_time):
     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 
+    timezone = timezone_tuple.tm_hour*3600 + timezone_tuple.tm_min*60
     return time_val + timesign*timezone
 
 def handy_time(time_val):
+    """Convert a time number into a useful localtime.
+
+    Where :func:`time_to_str` returns GMT +0000, `handy_time` returns
+    a string in local time.  This may be more accessible for the user.
+
+    Parameters
+    ----------
+    time_val : float
+      Float seconds since the Epoc, see :func:`time.time`.
+    """
     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.
+
+    Parameters
+    ----------
+    str_time : str
+      An RFC 2822-formatted string.
+
+    Examples
+    --------
+
     >>> time_to_gmtime("Thu, 01 Jan 1970 00:00:00 -1000")
     'Thu, 01 Jan 1970 10:00:00 +0000'
     """
@@ -108,8 +190,23 @@ def time_to_gmtime(str_time):
     return time_to_str(time_val)
 
 def iterable_full_of_strings(value, alternative=None):
-    """
-    Require an iterable full of strings.
+    """Require an iterable full of strings.
+
+    This is useful, for example, in validating `*.extra_strings`.
+    See :attr:`libbe.bugdir.BugDir.extra_strings`
+
+    Parameters
+    ----------
+    value : list or None
+      The potential list of strings.
+    alternative
+      Allow a default (e.g. `None`), such that::
+
+        iterable_full_of_strings(value=x, alternative=x) -> True
+
+    Examples
+    --------
+
     >>> iterable_full_of_strings([])
     True
     >>> iterable_full_of_strings(["abc", "def", u"hij"])
@@ -121,11 +218,31 @@ def iterable_full_of_strings(value, alternative=None):
     """
     if value == alternative:
         return True
-    elif not hasattr(value, "__iter__"):
+    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()
+def underlined(string, char='='):
+    """Produces a version of a string that is underlined.
+
+    Parameters
+    ----------
+    string : str
+      The string to underline
+    char : str
+      The character to use for the underlining.
+
+    Examples
+    --------
+
+    >>> underlined("Underlined String")
+    'Underlined String\\n================='
+    """
+    assert len(char) == 1, char
+    return '%s\n%s' % (string, char*len(string))
+
+if libbe.TESTING == True:
+    suite = doctest.DocTestSuite()
diff --git a/libbe/vcs.py b/libbe/vcs.py
deleted file mode 100644 (file)
index 7b506e8..0000000
+++ /dev/null
@@ -1,942 +0,0 @@
-# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
-#                         Alexander Belchenko <bialix@ukr.net>
-#                         Ben Finney <ben+python@benfinney.id.au>
-#                         Chris Ball <cjb@laptop.org>
-#                         W. Trevor King <wking@drexel.edu>
-#
-# 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
-        vcs.cleanup()
-    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
-        self.version = self._get_version()
-    def _vcs_version(self):
-        """
-        Return the VCS version string.
-        """
-        return "0.0"
-    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 <jdoe@example.com>").
-        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 <jdoe@example.com>").
-        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 <index>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 _get_version(self):
-        try:
-            ret = self._vcs_version()
-            return ret
-        except OSError, e:
-            if e.errno == errno.ENOENT:
-                return None
-            else:
-                raise OSError, e
-        except CommandError:
-            return None
-    def installed(self):
-        if self.version != None:
-            return True
-        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 <jdoe@example.com>").
-        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 <jdoe@example.com>").
-        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 <index>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 <jdoe@example.com>'
-        >>> 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 <jdoe@example.com>")
-        ('John Doe', 'jdoe@example.com')
-        >>> vcs._u_parse_id("John Doe")
-        ('John Doe', None)
-        >>> try:
-        ...     vcs._u_parse_id("John Doe <jdoe@example.com><what?>")
-        ... 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)
-        
-\f
-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):
-        self.vcs.cleanup()
-        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 <bogus@example.org>"
-
-        if self.vcs_supports_set_user_id:
-            self.test_new_user_id = "John Doe <jdoe@example.com>"
-            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."""
-        import sys
-        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()])
index f8eebbdfedab50fb40f01ec95f82edb7cd46fa89..2792de4583626933ba2bbdf9c050cb602ee39c55 100644 (file)
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
 #
 # 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
@@ -23,7 +23,10 @@ be bothered setting version strings" and the "I want complete control
 over the version strings" workflows.
 """
 
+import copy
+
 import libbe._version as _version
+import libbe.storage
 
 # Manually set a version string (optional, defaults to bzr revision id)
 #_VERSION = "1.2.3"
@@ -39,11 +42,14 @@ def version(verbose=False):
     else:
         string = _version.version_info["revision_id"]
     if verbose == True:
+        info = copy.copy(_version.version_info)
+        info['storage'] = libbe.storage.STORAGE_VERSION
         string += ("\n"
                    "revision: %(revno)d\n"
                    "nick: %(branch_nick)s\n"
-                   "revision id: %(revision_id)s"
-                   % _version.version_info)
+                   "revision id: %(revision_id)s\n"
+                   "storage version: %(storage)s"
+                   % info)
     return string
 
 if __name__ == "__main__":
similarity index 75%
rename from interfaces/xml/be-mbox-to-xml
rename to misc/xml/be-mail-to-xml
index a740117e32b5600c1846f84275e7651f47e34d10..5a1a88f6a73aaf4267f7aab74f1d8360b4e5c5f7 100755 (executable)
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
 #
 # 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
@@ -15,8 +15,8 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 """
-Convert an mbox into xml suitable for imput into be.
-  $ cat mbox | be-mbox-to-xml | be comment --xml <ID> -
+Convert an mbox into xml suitable for input into be.
+  $ be-mbox-to-xml file.mbox | be import-xml -c <ID> -
 mbox is a flat-file format, consisting of a series of messages.
 Messages begin with a a From_ line, followed by RFC 822 email,
 followed by a blank line.
@@ -24,15 +24,16 @@ 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 libbe.util.encoding import get_output_encoding
+from libbe.util.utility import time_to_str
+import mailbox # the mailbox people really want an on-disk copy
+import optparse
 from time import asctime, gmtime, mktime
 import types
 from xml.sax.saxutils import escape
 
-DEFAULT_ENCODING = get_encoding()
-set_IO_stream_encodings(DEFAULT_ENCODING)
+BREAK = u'--' # signature separator
+DEFAULT_ENCODING = get_output_encoding()
 
 KNOWN_IDS = []
 
@@ -40,7 +41,10 @@ def normalize_email_address(address):
     """
     Standardize whitespace, etc.
     """
-    return email.utils.formataddr(email.utils.parseaddr(address))
+    addr = email.utils.formataddr(email.utils.parseaddr(address))
+    if len(addr) == 0:
+        return None
+    return addr
 
 def normalize_RFC_2822_date(date):
     """
@@ -54,6 +58,14 @@ def normalize_RFC_2822_date(date):
         'unparsable date: "%s"' % date
     return time_to_str(mktime(time_tuple))
 
+def strip_footer(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 comment_message_to_xml(message, fields=None):
     if fields == None:
         fields = {}
@@ -125,7 +137,7 @@ def comment_message_to_xml(message, fields=None):
     body = message.get_payload(decode=True) # attempt to decode
     assert body != None, "Unable to decode?"
     if fields[u'content-type'].startswith(u"text/"):
-        body = unicode(body, encoding=charset).rstrip(u'\n')
+        body = strip_footer(unicode(body, encoding=charset))
     else:
         body = base64.encode(body)
     fields[u'body'] = body
@@ -137,15 +149,27 @@ def comment_message_to_xml(message, fields=None):
     lines.append(u"</comment>")
     return u'\n'.join(lines)
 
-def main(mbox_filename):
-    mb = mbox(mbox_filename)
+def main(argv):
+    parser = optparse.OptionParser(usage='%prog [options] mailbox')
+    formats = ['mbox', 'Maildir', 'MH', 'Babyl', 'MMDF']
+    parser.add_option('-f', '--format', type='choice', dest='format',
+                      help="Select the mailbox format from %s.  See the mailbox module's documention for descriptions of these formats." \
+                          % ', '.join(formats),
+                      default='mbox', choices=formats)
+    options,args = parser.parse_args(argv)
+    mailbox_file = args[1]
+    reader = getattr(mailbox, options.format)
+    mb = reader(mailbox_file, factory=None)
     print u'<?xml version="1.0" encoding="%s" ?>' % DEFAULT_ENCODING
-    print u"<comment-list>"
+    print u"<be-xml>"
     for message in mb:
         print comment_message_to_xml(message)
-    print u"</comment-list>"
+    print u"</be-xml>"
 
 
 if __name__ == "__main__":
     import sys
-    main(sys.argv[1])
+    import codecs
+
+    sys.stdout = codecs.getwriter(DEFAULT_ENCODING)(sys.stdout)
+    main(sys.argv)
similarity index 70%
rename from interfaces/xml/be-xml-to-mbox
rename to misc/xml/be-xml-to-mbox
index c63044708ae7855080c344fe9b5970694261b1b6..c8b7479a7a3acf160bcf168488ebe7bde3d7c493 100755 (executable)
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
-# Copyright (C) 2009 Chris Ball <cjb@laptop.org>
-#                    W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
 #
 # 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
@@ -26,10 +25,9 @@ followed by a blank line.
 """
 
 #from mailbox import mbox, Message  # the mailbox people really want an on-disk copy
-import codecs
 import email.utils
-from libbe.encoding import get_encoding, set_IO_stream_encodings
-from libbe.utility import str_to_time as rfc2822_to_gmtime_integer
+from libbe.util.encoding import get_output_encoding
+from libbe.util.utility import str_to_time as rfc2822_to_gmtime_integer
 from time import asctime, gmtime
 import types
 try: # import core module, Python >= 2.5
@@ -41,8 +39,7 @@ from xml.sax.saxutils import unescape
 
 DEFAULT_DOMAIN = "invalid.com"
 DEFAULT_EMAIL = "dummy@" + DEFAULT_DOMAIN
-DEFAULT_ENCODING = get_encoding()
-set_IO_stream_encodings(DEFAULT_ENCODING)
+DEFAULT_ENCODING = get_output_encoding()
 
 def rfc2822_to_asctime(rfc2822_string):
     """Convert an RFC 2822-fomatted string into a asctime string.
@@ -79,7 +76,6 @@ class Bug (LimitedAttrDict):
               u"severity",
               u"status",
               u"assigned",
-              u"target",
               u"reporter",
               u"creator",
               u"created",
@@ -87,21 +83,24 @@ class Bug (LimitedAttrDict):
               u"comments",
               u"extra-strings"]
     def print_to_mbox(self):
-        name,addr = email.utils.parseaddr(self["creator"])
-        print "From %s %s" % (addr, rfc2822_to_asctime(self["created"]))
-        print "Message-ID: <%s@%s>" % (self["uuid"], DEFAULT_DOMAIN)
-        print "Date: %s" % self["created"]
-        print "From: %s" % self["creator"]
-        print "Content-Type: %s; charset=%s" % ("text/plain", DEFAULT_ENCODING)
-        print "Content-Transfer-Encoding: 8bit"
-        print "Subject: %s: %s" % (self["short-name"], self["summary"])
-        print ""
-        print self["summary"]
-        print ""
-        if "extra-strings" in self:
-            print "extra strings:\n  ",
-            print '\n  '.join(self["extra_strings"])
-        print ""
+        if "creator" in self:
+            # otherwise, probably a `be show` uuid-only bug to avoid
+            # root comments.
+            name,addr = email.utils.parseaddr(self["creator"])
+            print "From %s %s" % (addr, rfc2822_to_asctime(self["created"]))
+            print "Message-id: <%s@%s>" % (self["uuid"], DEFAULT_DOMAIN)
+            print "Date: %s" % self["created"]
+            print "From: %s" % self["creator"]
+            print "Content-Type: %s; charset=%s" \
+                % ("text/plain", DEFAULT_ENCODING)
+            print "Content-Transfer-Encoding: 8bit"
+            print "Subject: %s: %s" % (self["short-name"], self["summary"])
+            if "extra-strings" in self:
+                for estr in self["extra-strings"]:
+                    print "X-Extra-String: %s" % estr
+            print ""
+            print self["summary"]
+            print ""
         if "comments" in self:
             for comment in self["comments"]:
                 comment.print_to_mbox(self)            
@@ -124,6 +123,11 @@ class Bug (LimitedAttrDict):
             else:
                 self[field.tag] = text
 
+def wrap_id(id):
+    if "@" not in id:
+        return "<%s@%s>" % (id, DEFAULT_DOMAIN)
+    return id
+
 class Comment (LimitedAttrDict):
     _attrs = [u"uuid",
               u"alt-id",
@@ -132,7 +136,8 @@ class Comment (LimitedAttrDict):
               u"author",
               u"date",
               u"content-type",
-              u"body"]
+              u"body",
+              u"extra-strings"]
     def print_to_mbox(self, bug=None):
         if bug == None:
             bug = Bug()
@@ -143,7 +148,9 @@ class Comment (LimitedAttrDict):
         elif "alt-id" in self: id = self["alt-id"]
         else:                  id = None
         if id != None:
-            print "Message-ID: <%s@%s>" % (id, DEFAULT_DOMAIN)
+            print "Message-id: %s" % wrap_id(id)
+        if "alt-id" in self:
+            print "Alt-id: %s" % wrap_id(self["alt-id"])
         print "Date: %s" % self["date"]
         print "From: %s" % self["author"]
         subject = ""
@@ -156,22 +163,33 @@ class Comment (LimitedAttrDict):
         print "Subject: %s" % subject
         if "in-reply-to" not in self.keys():
             self["in-reply-to"] = bug["uuid"]
-        print "In-Reply-To: <%s@%s>" % (self["in-reply-to"], DEFAULT_DOMAIN)
+        print "In-Reply-To: %s" % wrap_id(self["in-reply-to"])
+        if "extra-strings" in self:
+            for estr in self["extra-strings"]:
+                print "X-Extra-String: %s" % estr
         if self["content-type"].startswith("text/"):
             print "Content-Transfer-Encoding: 8bit"
-            print "Content-Type: %s; charset=%s" % (self["content-type"], DEFAULT_ENCODING)
-            print ""
-            print self["body"]
-        else: # content type and transfer encoding already in XML MIME output
-            print self["body"]
+            print "Content-Type: %s; charset=%s" \
+                % (self["content-type"], DEFAULT_ENCODING)
+        else:
+            print "Content-Transfer-Encoding: base64"
+            print "Content-Type: %s;" % (self["content-type"])
+        print ""
+        print self["body"]
         print ""
     def init_from_etree(self, element):
         assert element.tag == "comment", element.tag
         for field in element.getchildren():
             text = unescape(unicode(field.text).decode("unicode_escape").strip())
-            if field.tag == "body":
-                text+="\n"
-            self[field.tag] = text
+            if field.tag == "extra-string":
+                if "extra-strings" in self:
+                    self["extra-strings"].append(text)
+                else:
+                    self["extra-strings"] = [text]
+            else:
+                if field.tag == "body":
+                    text+="\n"
+                self[field.tag] = text
 
 def print_to_mbox(element):
     if element.tag == "bug":
@@ -182,19 +200,16 @@ def print_to_mbox(element):
         c = Comment()
         c.init_from_etree(element)
         c.print_to_mbox()
-    elif element.tag in ["bugs", "bug-list"]:
-        for b_elt in element.getchildren():
-            b = Bug()
-            b.init_from_etree(b_elt)
-            b.print_to_mbox()
-    elif element.tag in ["comments", "comment-list"]:
-        for c_elt in element.getchildren():
-            c = Comment()
-            c.init_from_etree(c_elt)
-            c.print_to_mbox()
+    elif element.tag in ["be-xml"]:
+        for elt in element.getchildren():
+            print_to_mbox(elt)
 
 if __name__ == "__main__":
+    import codecs
     import sys
+    
+    sys.stdin = codecs.getreader(DEFAULT_ENCODING)(sys.stdin)
+    sys.stdout = codecs.getwriter(DEFAULT_ENCODING)(sys.stdout)
 
     if len(sys.argv) == 1: # no filename given, use stdin
         xml_unicode = sys.stdin.read()
similarity index 100%
rename from interfaces/email/catmutt
rename to misc/xml/catmutt
diff --git a/release.py b/release.py
new file mode 100755 (executable)
index 0000000..d038cce
--- /dev/null
@@ -0,0 +1,161 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+import os.path
+import shutil
+import string
+import sys
+
+from libbe.subproc import Pipe, invoke
+from update_copyright import update_authors, update_files
+
+def validate_tag(tag):
+    """
+    >>> validate_tag('1.0.0')
+    >>> validate_tag('A.B.C-r7')
+    >>> validate_tag('A.B.C r7')
+    Traceback (most recent call last):
+      ...
+    Exception: Invalid character ' ' in tag 'A.B.C r7'
+    >>> validate_tag('"')
+    Traceback (most recent call last):
+      ...
+    Exception: Invalid character '"' in tag '"'
+    >>> validate_tag("'")
+    Traceback (most recent call last):
+      ...
+    Exception: Invalid character ''' in tag '''
+    """
+    for char in tag:
+        if char in string.digits:
+            continue
+        elif char in string.letters:
+            continue
+        elif char in ['.','-']:
+            continue
+        raise Exception("Invalid character '%s' in tag '%s'" % (char, tag))
+
+def bzr_pending_changes():
+    """Use `bzr diff`s exit status to detect change:
+    1 - changed
+    2 - unrepresentable changes
+    3 - error
+    0 - no change
+    """
+    p = Pipe([['bzr', 'diff']])
+    if p.status == 0:
+        return False
+    elif p.status in [1,2]:
+        return True
+    raise Exception("Error in bzr diff %d\n%s" % (p.status, p.stderrs[-1]))
+
+def set_release_version(tag):
+    print "set libbe.version._VERSION = '%s'" % tag
+    p = Pipe([['sed', '-i', "s/^# *_VERSION *=.*/_VERSION = '%s'/" % tag,
+               os.path.join('libbe', 'version.py')]])
+    assert p.status == 0, p.statuses
+
+def bzr_commit(commit_message):
+    print 'commit current status:', commit_message
+    p = Pipe([['bzr', 'commit', '-m', commit_message]])
+    assert p.status == 0, p.statuses
+
+def bzr_tag(tag):
+    print 'tag current revision', tag
+    p = Pipe([['bzr', 'tag', tag]])
+    assert p.status == 0, p.statuses
+
+def bzr_export(target_dir):
+    print 'export current revision to', target_dir
+    p = Pipe([['bzr', 'export', target_dir]])
+    assert p.status == 0, p.statuses
+
+def make_version():
+    print 'generate libbe/_version.py'
+    p = Pipe([['make', os.path.join('libbe', '_version.py')]])
+    assert p.status == 0, p.statuses
+
+def make_changelog(filename, tag):
+    print 'generate ChangeLog file', filename, 'up to tag', tag
+    p = invoke(['bzr', 'log', '--gnu-changelog', '-n1', '-r',
+                '..tag:%s' % tag], stdout=file(filename, 'w'))
+    status = p.wait()
+    assert status == 0, status
+
+def set_vcs_name(filename, vcs_name='None'):
+    """Exported directory is not a bzr repository, so set vcs_name to
+    something that will work.
+      vcs_name: new_vcs_name
+    """
+    print 'set vcs_name in', filename, 'to', vcs_name
+    p = Pipe([['sed', '-i', "s/^vcs_name:.*/vcs_name: %s/" % vcs_name,
+               filename]])
+    assert p.status == 0, p.statuses
+
+def create_tarball(tag):
+    release_name='be-%s' % tag
+    export_dir = release_name
+    bzr_export(export_dir)
+    make_version()
+    print 'copy libbe/_version.py to %s/libbe/_version.py' % export_dir
+    shutil.copy(os.path.join('libbe', '_version.py'),
+                os.path.join(export_dir, 'libbe', '_version.py'))
+    make_changelog(os.path.join(export_dir, 'ChangeLog'), tag)
+    set_vcs_name(os.path.join(export_dir, '.be', 'settings'))
+    tarball_file = '%s.tar.gz' % release_name
+    print 'create tarball', tarball_file
+    p = Pipe([['tar', '-czf', tarball_file, export_dir]])
+    assert p.status == 0, p.statuses
+    print 'remove', export_dir
+    shutil.rmtree(export_dir)
+
+def test():
+    import doctest
+    doctest.testmod() 
+
+if __name__ == '__main__':
+    import optparse
+    usage = """%prog [options] TAG
+
+Create a bzr tag and a release tarball from the current revision.
+For example
+  %prog 1.0.0
+"""
+    p = optparse.OptionParser(usage)
+    p.add_option('--test', dest='test', default=False,
+                 action='store_true', help='Run internal tests and exit')
+    options,args = p.parse_args()
+
+    if options.test == True:
+        test()
+        sys.exit(0)
+
+    assert len(args) == 1, '%d (!= 1) arguments: %s' % (len(args), args)
+    tag = args[0]
+    validate_tag(tag)
+
+    if bzr_pending_changes() == True:
+        print "Handle pending changes before releasing."
+        sys.exit(1)
+    set_release_version(tag)
+    update_authors()
+    update_files()
+    bzr_commit("Bumped to version %s" % tag)
+    bzr_tag(tag)
+    create_tarball(tag)
index e770419249ab026fb3bd41457bcbf8871fb8de86..ab0a6080407a90b234efc7733be3fe1a592f6585 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -9,11 +9,18 @@ rev_date = rev_id.split("-")[1]
 setup(
     name='Bugs Everywhere',
     version=rev_date,
-    description='Bugtracker built on distributed revision control',
+    description='Bugtracker supporting distributed revision control',
     url='http://bugseverywhere.org/',
-    packages=['becommands', 'libbe'],
+    packages=['libbe',
+              'libbe.command',
+              'libbe.storage',
+              'libbe.storage.util',
+              'libbe.storage.vcs',
+              'libbe.ui',
+              'libbe.ui.util',
+              'libbe.util'],
     scripts=['be'],
     data_files=[
-        ('share/man/man1', ['doc/be.1']),
+        ('share/man/man1', ['doc/man/be.1']),
         ]
     )
diff --git a/test.py b/test.py
index 1f1ffcfd804889b85ce17932f4e80485420e2140..e6e5c2c4e115a7c27249ba04b362d8bf32dc65fc 100644 (file)
--- a/test.py
+++ b/test.py
-"""Usage: python test.py [module(s) ...]
+# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+#                         Marien Zwart <marienz@gentoo.org>
+#                         W. Trevor King <wking@drexel.edu>
+#
+# 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.
 
-When called without optional module names, run the doctests from *all*
-modules.  This may raise lots of errors if you haven't installed one
-of the versioning control systems.
-
-When called with module name arguments, only run the doctests from
-those modules.
-"""
-
-from libbe import plugin
-import unittest
 import doctest
+import os
+import os.path
 import sys
+import unittest
+
+import libbe
+libbe.TESTING = True
+from libbe.util.tree import Tree
+from libbe.util.plugin import import_by_name
+from libbe.version import version
 
-suite = unittest.TestSuite()
-
-if len(sys.argv) > 1:
-    for submodname in sys.argv[1:]:
-        match = False
-        mod = plugin.get_plugin("libbe", submodname)
-        if mod is not None:
-            if hasattr(mod, "suite"):
-                suite.addTest(mod.suite)
-                match = True
-            else:
-                print "Module \"%s\" has no test suite" % submodname
-        mod = plugin.get_plugin("becommands", submodname)
-        if mod is not None:
-            suite.addTest(doctest.DocTestSuite(mod))
-            match = True
-        if not match:
-            print "No modules match \"%s\"" % submodname
-            sys.exit(1)
-else:
-    failed = False
-    for modname,module in plugin.iter_plugins("libbe"):
-        if not hasattr(module, "suite"):
+def python_tree(root_path='libbe', root_modname='libbe'):
+    tree = Tree()
+    tree.path = root_path
+    tree.parent = None
+    stack = [tree]
+    while len(stack) > 0:
+        f = stack.pop(0)
+        if f.path.endswith('.py'):
+            f.name = os.path.basename(f.path)[:-len('.py')]
+        elif os.path.isdir(f.path) \
+                and os.path.exists(os.path.join(f.path, '__init__.py')):
+            f.name = os.path.basename(f.path)
+            f.is_module = True
+            for child in os.listdir(f.path):
+                if child == '__init__.py':
+                    continue
+                c = Tree()
+                c.path = os.path.join(f.path, child)
+                c.parent = f
+                stack.append(c)
+        else:
             continue
-        suite.addTest(module.suite)
-    for modname,module in plugin.iter_plugins("becommands"):
-        suite.addTest(doctest.DocTestSuite(module))
-
-#for s in suite._tests:
-#    print s
-#exit(0)
-result = unittest.TextTestRunner(verbosity=2).run(suite)
-
-numErrors = len(result.errors)
-numFailures = len(result.failures)
-numBad = numErrors + numFailures
-if numBad > 126:
-    numBad = 1
-sys.exit(numBad)
+        if f.parent == None:
+            f.modname = root_modname
+        else:
+            f.modname = f.parent.modname + '.' + f.name
+            f.parent.append(f)
+    return tree
+
+def add_module_tests(suite, modname):
+    try:
+        mod = import_by_name(modname)
+    except ValueError, e:
+        print >> sys.stderr, 'Failed to import "%s"' % (modname)
+        raise e
+    if hasattr(mod, 'suite'):
+        s = mod.suite
+    else:
+        s = unittest.TestLoader().loadTestsFromModule(mod)
+        try:
+            sdoc = doctest.DocTestSuite(mod)
+            suite.addTest(sdoc)
+        except ValueError:
+            pass
+    suite.addTest(s)
+
+if __name__ == '__main__':
+    import optparse
+    parser = optparse.OptionParser(usage='%prog [options] [modules ...]',
+                                   description=
+"""When called without optional module names, run the test suites for
+*all* modules.  This may raise lots of errors if you haven't installed
+one of the versioning control systems.
+
+When called with module name arguments, only run the test suites from
+those modules and their submodules.  For example::
+
+    $ python test.py libbe.bugdir libbe.storage
+""")
+    parser.add_option('-q', '--quiet', action='store_true', default=False,
+                      help='Run unittests in quiet mode (verbosity 1).')
+    options,args = parser.parse_args()
+    print >> sys.stderr, 'Testing BE\n%s' % version(verbose=True)
+
+    verbosity = 2
+    if options.quiet == True:
+        verbosity = 1
+
+    suite = unittest.TestSuite()
+    tree = python_tree()
+    if len(args) == 0:
+        for node in tree.traverse():
+            add_module_tests(suite, node.modname)
+    else:
+        added = []
+        for modname in args:
+            for node in tree.traverse():
+                if node.modname == modname:
+                    for n in node.traverse():
+                        if n.modname not in added:
+                            add_module_tests(suite, n.modname)
+                            added.append(n.modname)
+                    break
+    
+    result = unittest.TextTestRunner(verbosity=verbosity).run(suite)
+    
+    numErrors = len(result.errors)
+    numFailures = len(result.failures)
+    numBad = numErrors + numFailures
+    if numBad > 126:
+        numBad = 1
+    sys.exit(numBad)
diff --git a/test_upgrade.py b/test_upgrade.py
new file mode 100755 (executable)
index 0000000..40db42a
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/bash
+#
+# Test upgrade functionality by checking out revisions with the
+# various initial on-disk versions and running `be list` on them to
+# force an auto-upgrade.
+#
+# usage: test_upgrade.sh
+
+REVS='revid:wking@drexel.edu-20090831063121-85p59rpwoi1mzk3i
+revid:wking@drexel.edu-20090831171945-73z3wwt4lrm7zbmu
+revid:wking@drexel.edu-20091205224008-z4fed13sd80bj4fe
+revid:wking@drexel.edu-20091207123614-okq7i0ahciaupuy9'
+
+ROOT=$(bzr root)
+BE="$ROOT/be"
+cd "$ROOT"
+
+echo "$REVS" | while read REV; do
+    TMPDIR=$(mktemp --directory --tmpdir "BE-upgrade.XXXXXXXXXX")
+    REPO="$TMPDIR/repo"
+    echo "Testing revision: $REV"
+    echo "  Test directory: $REPO"
+    bzr checkout --lightweight --revision="$REV" "$ROOT" "$TMPDIR/repo"
+    VERSION=$(cat "$REPO/.be/version")
+    echo "  Version: $VERSION"
+    $BE --repo "$REPO" list > /dev/null
+    RET="$?"
+    rm -rf "$TMPDIR"
+    if [ $RET -ne 0 ]; then
+       echo "Error! ($RET)"
+       exit $RET
+    fi
+done
index 13be2ff75a4501a790d091578071f4146c09707c..9b7dafeadb97d7618701ce3196cbc433ef5f0efc 100755 (executable)
@@ -4,8 +4,8 @@
 # features work, and gives an example of suggested usage to get people
 # started.
 #
-# usage: test_usage.sh RCS
-# where RCS is one of:
+# usage: test_usage.sh VCS
+# where VCS is one of:
 #   bzr, git, hg, arch, none
 #
 # Note that this script uses the *installed* version of be, not the
@@ -18,34 +18,33 @@ set -v # verbose, echo commands to stdout
 exec 6>&2 # save stderr to file descriptor 6
 exec 2>&1 # fd 2 now writes to stdout
 
-ONLY_TEST_COMMIT="true"
-
 if [ $# -gt 1 ]
 then
-    echo "usage: test_usage.sh [RCS]"
+    echo "usage: test_usage.sh [VCS]"
     echo ""
-    echo "where RCS is one of"
-    for RCS in arch bzr darcs git hg none
+    echo "where VCS is one of"
+    for VCS in arch bzr darcs git hg none
     do
-       echo "  $RCS"
+       echo "  $VCS"
     done
     exit 1
 elif [ $# -eq 0 ]
 then
-    for RCS in arch bzr darcs git hg none
+    for VCS in arch bzr darcs git hg none
     do
-       echo -e "\n\nTesting $RCS\n\n"
-       $0 "$RCS" || exit 1
+       echo -e "\n\nTesting $VCS\n\n"
+       $0 "$VCS" || exit 1
     done
     exit 0
 fi
 
-RCS="$1"
+VCS="$1"
 
 TESTDIR=`mktemp -d /tmp/BEtest.XXXXXXXXXX`
 cd $TESTDIR
 
-if [ "$RCS" == "arch" ]
+# Initialize the VCS repository
+if [ "$VCS" == "arch" ]
 then
     ID=`tla my-id`
     ARCH_PARAM_DIR="$HOME/.arch-params"
@@ -64,73 +63,79 @@ then
     sed -i 's/^source .*/source ^[._=a-zA-X0-9].*$/' '{arch}/=tagging-method'
     echo "tla import -A $ARCH_ARCHIVE --summary 'Began versioning'"
     tla import -A $ARCH_ARCHIVE --summary 'Began versioning'
-elif [ "$RCS" == "bzr" ]
+elif [ "$VCS" == "bzr" ]
 then
     ID=`bzr whoami`
     bzr init
-elif [ "$RCS" == "darcs" ]
+elif [ "$VCS" == "darcs" ]
 then
     if [ -z "$DARCS_EMAIL" ]; then
        export DARCS_EMAIL="J. Doe <jdoe@example.com>"
     fi
     ID="$DARCS_EMAIL"
     darcs init
-elif [ "$RCS" == "git" ]
+elif [ "$VCS" == "git" ]
 then
     NAME=`git config user.name`
     EMAIL=`git config user.email`
     ID="$NAME <$EMAIL>"
     git init
-elif [ "$RCS" == "hg" ]
+elif [ "$VCS" == "hg" ]
 then
     ID=`hg showconfig ui.username`
     hg init
-elif [ "$RCS" == "none" ]
+elif [ "$VCS" == "none" ]
 then
     ID=`id -nu`
 else
-    echo "Unrecognized RCS '$RCS'"
+    echo "Unrecognized VCS '$VCS'"
     exit 1
 fi
+
 if [ -z "$ID" ]
-then # set a default ID
+then # set a default ID for VCSs that aren't tracking one yet.
     ID="John Doe <jdoe@example.com>"
 fi
 echo "I am '$ID'"
 
-be init
-OUT=`be new 'having too much fun'`
+be init  # initialize the Bugs Everywhere repository
+OUT=`be new 'having too much fun'` # create a new bug
 echo "$OUT"
 BUG=`echo "$OUT" | sed -n 's/Created bug with ID //p'`
 echo "Working with bug: $BUG"
 be comment $BUG "This is an argument"
-be set user_id "$ID"    # get tired of guessing user id for none RCS
+#be set user_id "$ID"    # get tired of guessing user id for none VCS
 be set                  # show settings
-be comment $BUG:1 "No it isn't" # comment on the first comment
+be comment $BUG/ "No it isn't" # comment on the first comment
 be show $BUG            # show details on a given bug
-be close $BUG           # set bug status to 'closed'
+be status closed $BUG   # set bug status to 'closed'
 be comment $BUG "It's closed, but I can still comment."
-be open $BUG            # set bug status to 'open'
+if [ "$VCS" != 'none' ]; then
+    be commit 'Initial commit'
+fi
+be status open $BUG     # set bug status to 'open'
 be comment $BUG "Reopend, comment again"
-be status $BUG fixed    # set bug status to 'fixed'
+be status fixed $BUG    # set bug status to 'fixed'
 be list                 # list all open bugs
 be list --status fixed  # list all fixed bugs
-be assign $BUG          # assign the bug to yourself
-be list -m -s fixed     # see fixed bugs assigned to you
-be assign $BUG 'Joe'    # assign the bug to Joe
-be list -a Joe -s fixed # list the fixed bugs assigned to Joe
-be assign $BUG none     # assign the bug to noone
-be diff                 # see what has changed
+be assign - $BUG        # assign the bug to yourself
+be list -m --status fixed # see fixed bugs assigned to you
+be assign 'Joe' $BUG    # assign the bug to Joe
+be list -a Joe --status fixed # list the fixed bugs assigned to Joe
+be assign none $BUG     # un-assign the bug
+if [ "$VCS" != 'none' ]; then
+  be diff               # see what has changed
+fi
 OUT=`be new 'also having too much fun'`
 BUGB=`echo "$OUT" | sed -n 's/Created bug with ID //p'`
 be comment $BUGB "Blissfully unaware of a similar bug"
 be merge $BUG $BUGB     # join BUGB to BUG
-be show $BUG            # show bug details & comments
+be --no-pager show $BUG            # show bug details & comments
 # you can also export/import XML bugs/comments
 OUT=`be new 'yet more fun'`
 BUGC=`echo "$OUT" | sed -n 's/Created bug with ID //p'`
 be comment $BUGC "The ants go marching..."
-be show --xml $BUGC | be comment --xml ${BUG}:2 -
+be show --xml $BUGC/ | be import-xml --add-only --comment-root $BUG -
 be remove $BUG # decide that you don't like that bug after all
 be commit "You can even commit using BE"
 be commit --allow-empty "And you can add empty commits if you like"
@@ -139,7 +144,7 @@ be commit "But this will fail" || echo "Failed"
 cd /
 rm -rf $TESTDIR
 
-if [ "$RCS" == "arch" ]
+if [ "$VCS" == "arch" ]
 then
     # Cleanup everything outside of TESTDIR
     rm -rf "$ARCH_ARCHIVE_ROOT"
diff --git a/update_copyright.py b/update_copyright.py
new file mode 100755 (executable)
index 0000000..2490ba9
--- /dev/null
@@ -0,0 +1,318 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os.path
+import re
+import sys
+import time
+
+import os
+import sys
+import select
+from threading import Thread
+
+from libbe.util.subproc import Pipe
+
+COPYRIGHT_TEXT="""#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
+
+COPYRIGHT_TAG='-xyz-COPYRIGHT-zyx-' # unlikely to occur in the wild :p
+
+ALIASES = [
+    ['Ben Finney <benf@cybersource.com.au>',
+     'Ben Finney <ben+python@benfinney.id.au>',
+     'John Doe <jdoe@example.com>'],
+    ['Chris Ball <cjb@laptop.org>',
+     'Chris Ball <cjb@thunk.printf.net>'],
+    ['Gianluca Montecchi <gian@grys.it>',
+     'gian <gian@li82-39>',
+     'gianluca <gian@galactica>'],
+    ['W. Trevor King <wking@drexel.edu>',
+     'wking <wking@mjolnir>'],
+    [None,
+     'j^ <j@oil21.org>'],
+    ]
+COPYRIGHT_ALIASES = [
+    ['Aaron Bentley and Panometrics, Inc.',
+     'Aaron Bentley <abentley@panoramicfeedback.com>'],
+    ]
+EXCLUDES = [
+    ['Aaron Bentley and Panometrics, Inc.',
+     'Aaron Bentley <aaron.bentley@utoronto.ca>',]
+    ]
+
+
+IGNORED_PATHS = ['./.be/', './.bzr/', './build/']
+IGNORED_FILES = ['COPYING', 'update_copyright.py', 'catmutt']
+
+def _strip_email(*args):
+    """
+    >>> _strip_email('J Doe <jdoe@a.com>')
+    ['J Doe']
+    >>> _strip_email('J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>')
+    ['J Doe', 'JJJ Smith']
+    """
+    args = list(args)
+    for i,arg in enumerate(args):
+        if arg == None:
+            continue
+        index = arg.find('<')
+        if index > 0:
+            args[i] = arg[:index].rstrip()
+    return args
+
+def _replace_aliases(authors, with_email=True, aliases=None,
+                     excludes=None):
+    """
+    >>> aliases = [['J Doe and C, Inc.', 'J Doe <jdoe@c.com>'],
+    ...            ['J Doe <jdoe@a.com>', 'Johnny <jdoe@b.edu>'],
+    ...            ['JJJ Smith <jjjs@a.com>', 'Jingly <jjjs@b.edu>'],
+    ...            [None, 'Anonymous <a@a.com>']]
+    >>> excludes = [['J Doe and C, Inc.', 'J Doe <jdoe@a.com>']]
+    >>> _replace_aliases(['JJJ Smith <jjjs@a.com>', 'Johnny <jdoe@b.edu>',
+    ...                   'Jingly <jjjs@b.edu>', 'Anonymous <a@a.com>'],
+    ...                  with_email=True, aliases=aliases, excludes=excludes)
+    ['J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>']
+    >>> _replace_aliases(['JJJ Smith', 'Johnny', 'Jingly', 'Anonymous'],
+    ...                  with_email=False, aliases=aliases, excludes=excludes)
+    ['J Doe', 'JJJ Smith']
+    >>> _replace_aliases(['JJJ Smith <jjjs@a.com>', 'Johnny <jdoe@b.edu>',
+    ...                   'Jingly <jjjs@b.edu>', 'J Doe <jdoe@c.com>'],
+    ...                  with_email=True, aliases=aliases, excludes=excludes)
+    ['J Doe and C, Inc.', 'JJJ Smith <jjjs@a.com>']
+    """
+    if aliases == None:
+        aliases = ALIASES
+    if excludes == None:
+        excludes = EXCLUDES
+    if with_email == False:
+        aliases = [_strip_email(*alias) for alias in aliases]
+        exclude = [_strip_email(*exclude) for exclude in excludes]
+    for i,author in enumerate(authors):
+        for alias in aliases:
+            if author in alias[1:]:
+                authors[i] = alias[0]
+                break
+    for i,author in enumerate(authors):
+        for exclude in excludes:
+            if author in exclude[1:] and exclude[0] in authors:
+                authors[i] = None
+    authors = sorted(set(authors))
+    if None in authors:
+        authors.remove(None)
+    return authors
+
+def authors_list():
+    p = Pipe([['bzr', 'log', '-n0'],
+              ['grep', '^ *committer\|^ *author'],
+              ['cut', '-d:', '-f2'],
+              ['sed', 's/ <.*//;s/^ *//'],
+              ['sort'],
+              ['uniq']])
+    assert p.status == 0, p.statuses
+    authors = p.stdout.rstrip().split('\n')
+    return _replace_aliases(authors, with_email=False)
+
+def update_authors(verbose=True):
+    print "updating AUTHORS"
+    f = file('AUTHORS', 'w')
+    authors_text = 'Bugs Everywhere was written by:\n%s\n' % '\n'.join(authors_list())
+    f.write(authors_text)
+    f.close()
+
+def ignored_file(filename, ignored_paths=None, ignored_files=None):
+    """
+    >>> ignored_paths = ['./a/', './b/']
+    >>> ignored_files = ['x', 'y']
+    >>> ignored_file('./a/z', ignored_paths, ignored_files)
+    True
+    >>> ignored_file('./ab/z', ignored_paths, ignored_files)
+    False
+    >>> ignored_file('./ab/x', ignored_paths, ignored_files)
+    True
+    >>> ignored_file('./ab/xy', ignored_paths, ignored_files)
+    False
+    >>> ignored_file('./z', ignored_paths, ignored_files)
+    False
+    """
+    if ignored_paths == None:
+        ignored_paths = IGNORED_PATHS
+    if ignored_files == None:
+        ignored_files = IGNORED_FILES
+    for path in ignored_paths:
+        if filename.startswith(path):
+            return True
+    if os.path.basename(filename) in ignored_files:
+        return True
+    if os.path.abspath(filename) != os.path.realpath(filename):
+        return True # symink somewhere in path...
+    return False
+
+def _copyright_string(orig_year, final_year, authors):
+    """
+    >>> print _copyright_string(orig_year=2005,
+    ...                         final_year=2005,
+    ...                         authors=['A <a@a.com>', 'B <b@b.edu>']
+    ...                        ) # doctest: +ELLIPSIS
+    # Copyright (C) 2005 A <a@a.com>
+    #                    B <b@b.edu>
+    #
+    # This program...
+    >>> print _copyright_string(orig_year=2005,
+    ...                         final_year=2009,
+    ...                         authors=['A <a@a.com>', 'B <b@b.edu>']
+    ...                        ) # doctest: +ELLIPSIS
+    # Copyright (C) 2005-2009 A <a@a.com>
+    #                         B <b@b.edu>
+    #
+    # This program...
+    """
+    if orig_year == final_year:
+        date_range = '%s' % orig_year
+    else:
+        date_range = '%s-%s' % (orig_year, final_year)
+    lines = ['# Copyright (C) %s %s' % (date_range, authors[0])]
+    for author in authors[1:]:
+        lines.append('#' +
+                     ' '*(len(' Copyright (C) ')+len(date_range)+1) +
+                     author)
+    return '%s\n%s' % ('\n'.join(lines), COPYRIGHT_TEXT)
+
+def _tag_copyright(contents):
+    """
+    >>> contents = '''Some file
+    ... bla bla
+    ... # Copyright (copyright begins)
+    ... # (copyright continues)
+    ... # bla bla bla
+    ... (copyright ends)
+    ... bla bla bla
+    ... '''
+    >>> print _tag_copyright(contents),
+    Some file
+    bla bla
+    -xyz-COPYRIGHT-zyx-
+    (copyright ends)
+    bla bla bla
+    """
+    lines = []
+    incopy = False
+    for line in contents.splitlines():
+        if incopy == False and line.startswith('# Copyright'):
+            incopy = True
+            lines.append(COPYRIGHT_TAG)
+        elif incopy == True and not line.startswith('#'):
+            incopy = False
+        if incopy == False:
+            lines.append(line.rstrip('\n'))
+    return '\n'.join(lines)+'\n'
+
+def _update_copyright(contents, orig_year, authors):
+    current_year = time.gmtime()[0]
+    copyright_string = _copyright_string(orig_year, current_year, authors)
+    contents = _tag_copyright(contents)
+    return contents.replace(COPYRIGHT_TAG, copyright_string)
+
+def update_file(filename, verbose=True):
+    if verbose == True:
+        print "updating", filename
+    contents = file(filename, 'r').read()
+
+    p = Pipe([['bzr', 'log', '-n0', filename],
+              ['grep', '^ *timestamp: '],
+              ['tail', '-n1'],
+              ['sed', 's/^ *//;'],
+              ['cut', '-b', '16-19']])
+    if p.status != 0:
+        assert p.statuses[0] == 3, p.statuses
+        return # bzr doesn't version that file
+    assert p.status == 0, p.statuses
+    orig_year = int(p.stdout.strip())
+
+    p = Pipe([['bzr', 'log', '-n0', filename],
+              ['grep', '^ *author: \|^ *committer: '],
+              ['cut', '-d:', '-f2'],
+              ['sed', 's/^ *//;s/ *$//'],
+              ['sort'],
+              ['uniq']])
+    assert p.status == 0, p.statuses
+    authors = p.stdout.rstrip().split('\n')
+    authors = _replace_aliases(authors, with_email=True,
+                               aliases=ALIASES+COPYRIGHT_ALIASES)
+
+    contents = _update_copyright(contents, orig_year, authors)
+    f = file(filename, 'w')
+    f.write(contents)
+    f.close()
+
+def update_files(files=None):
+    if files == None or len(files) == 0:
+        p = Pipe([['grep', '-rc', '# Copyright', '.'],
+                  ['grep', '-v', ':0$'],
+                  ['cut', '-d:', '-f1']])
+        assert p.status == 0
+        files = p.stdout.rstrip().split('\n')
+
+    for filename in files:
+        if ignored_file(filename) == True:
+            continue
+        update_file(filename)
+
+def test():
+    import doctest
+    doctest.testmod() 
+
+if __name__ == '__main__':
+    import optparse
+    usage = """%prog [options] [file ...]
+
+Update copyright information in source code with information from
+the bzr repository.  Run from the BE repository root.
+
+Replaces every line starting with '^# Copyright' and continuing with
+'^#' with an auto-generated copyright blurb.  If you want to add
+#-commented material after a copyright blurb, please insert a blank
+line between the blurb and your comment (as in this file), so the
+next run of update_copyright.py doesn't clobber your comment.
+
+If no files are given, a list of files to update is generated
+automatically.
+"""
+    p = optparse.OptionParser(usage)
+    p.add_option('--test', dest='test', default=False,
+                 action='store_true', help='Run internal tests and exit')
+    options,args = p.parse_args()
+
+    if options.test == True:
+        test()
+        sys.exit(0)
+
+    update_authors()
+    update_files(files=args)
diff --git a/update_copyright.sh b/update_copyright.sh
deleted file mode 100755 (executable)
index 84a5913..0000000
+++ /dev/null
@@ -1,158 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-# Update copyright information in source code with information from
-# the bzr repository.  Run from the BE repository root.
-#
-# Replaces everything starting with '^# Copyright' and continuing with
-# '^#' with an auto-generated copyright blurb.  If you want to add
-# #-commented material after a copyright blurb, please insert a blank
-# line between the blurb and your comment (as in this file), so the
-# next run of update_copyright.sh doesn't clobber your comment.
-#
-# usage: update_copyright.sh [files ...]
-#
-# If no files are given, a list of files to update is generated
-# automatically.
-
-set -o pipefail
-
-if [ $# -gt 0 ]; then
-    FILES="$*"
-else
-    FILES=`grep -rc "# Copyright" . | grep -v ':0$' | cut -d: -f1`
-fi
-
-COPYRIGHT_TEXT="#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."
-# escape newlines and special characters
-SED_RM_TRAIL_END='s/[\]n$//'         # strip trailing newline escape
-SED_ESC_SPECIAL='s/\([()/]\)/\\\1/g' # escape special characters
-ESCAPED_TEXT=`echo "$COPYRIGHT_TEXT" | awk '{printf("%s\\\\n", $0)}' | sed "$SED_RM_TRAIL_END" | sed "$SED_ESC_SPECIAL"`
-
-# adjust the AUTHORS file
-AUTHORS=`bzr log | grep '^ *committer\|^ *author' | cut -d: -f2 | sed 's/ <.*//;s/^ *//' | sort | uniq`
-AUTHORS=`echo "$AUTHORS" | grep -v 'j\^\|wking\|John Doe\|gianluca'` # remove non-names
-echo "Bugs Everywhere was written by:" > AUTHORS
-echo "$AUTHORS" >> AUTHORS
-
-CURRENT_YEAR=`date +"%Y"`
-TMP=`mktemp BE_update_copyright.XXXXXXX`
-
-for file in $FILES
-do
-    # Ignore some files
-    if [ "${file:0:5}" == "./.be" ]; then
-       continue
-    fi
-    if [ "${file:0:6}" == "./.bzr" ]; then
-       continue
-    fi
-    if [ "${file:0:7}" == "./build" ]; then
-       continue
-    fi
-    if [ "$file" == "./COPYING" ]; then
-       continue
-    fi
-    if [ "$file" == "./update_copyright.sh" ]; then
-       continue
-    fi
-    if [ "$file" == "./xml/catmutt" ]; then
-       continue
-    fi
-    echo "Processing $file"
-
-    # Get author history from bzr
-    AUTHORS=`bzr log "$file" | grep "^ *author: \|^ *committer: " | cut -d: -f2 | sed 's/^ *//;s/ *$//' | sort | uniq`
-    if [ $? -ne 0 ]; then
-       continue # bzr doesn't version that file
-    fi
-    ORIG_YEAR=`bzr log "$file" | grep "^ *timestamp: " | tail -n1 | sed 's/^ *//;' | cut -b 16-19`
-
-    # 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 <cjb@laptop.org>'`
-    if [ -n "$GREP_OUT" ]; then
-       AUTHORS=`echo "$AUTHORS" | grep -v '^Chris Ball <cjb@thunk.printf.net>$'`
-    fi
-
-    # Consolidate Aaron Bentley
-    AUTHORS=`echo "$AUTHORS" | sed 's/<abentley@panoramicfeedback.com>/and Panometrics, Inc./'`
-    GREP_OUT=`echo "$AUTHORS" | grep 'Aaron Bentley and Panometrics, Inc.'`
-    if [ -n "$GREP_OUT" ]; then
-       AUTHORS=`echo "$AUTHORS" | grep -v '^Aaron Bentley <aaron.bentley@utoronto.ca>$'`
-    fi
-
-    # Consolidate Ben Finney
-    AUTHORS=`echo "$AUTHORS" | sed 's/John Doe <jdoe@example.com>/Ben Finney <ben+python@benfinney.id.au>/'`
-    GREP_OUt=`echo "$AUTHORS" | grep 'Ben Finney <ben+python@benfinney.id.au>'`
-    if [ -n "$GREP_OUT" ]; then
-       AUTHORS=`echo "$AUTHORS" | grep -v '^Ben Finney <benf@cybersource.com.au>$'`
-    fi
-
-    # Consolidate Trevor King
-    AUTHORS=`echo "$AUTHORS" | grep -v "wking <wking@mjolnir>"`
-
-    # Consolidate Gianluca Montecchi
-    AUTHORS=`echo "$AUTHORS" | grep -v "gianluca"`
-
-    # Sort again...
-    AUTHORS=`echo "$AUTHORS" | sort | uniq`
-
-    # Generate new Copyright string
-    if [ "$ORIG_YEAR" == "$CURRENT_YEAR" ]; then
-       DATE_RANGE="$CURRENT_YEAR"
-       DATE_SPACE="    "
-    else
-       DATE_RANGE="${ORIG_YEAR}-${CURRENT_YEAR}"
-       DATE_SPACE="         "
-    fi
-    NUM_AUTHORS=`echo "$AUTHORS" | wc -l`
-    FIRST_AUTHOR=`echo "$AUTHORS" | head -n 1`
-    COPYRIGHT="# Copyright (C) $DATE_RANGE $FIRST_AUTHOR"
-    if [ $NUM_AUTHORS -gt 1 ]; then
-       OTHER_AUTHORS=`echo "$AUTHORS" | tail -n +2`
-       while read AUTHOR; do
-           COPYRIGHT=`echo "$COPYRIGHT\\n#               $DATE_SPACE $AUTHOR"`
-       done < <(echo "$OTHER_AUTHORS")
-    fi
-    COPYRIGHT=`echo "$COPYRIGHT\\n$ESCAPED_TEXT"`
-
-    # Strip old copyright info and replace with tag
-    awk 'BEGIN{incopy==0}{if(match($0, "^# Copyright")>0){incopy=1; print "-xyz-COPYRIGHT-zyx-"}else{if(incopy==0){print $0}else{if(match($0, "^#")==0){incopy=0; print $0}}}}' "$file" > "$TMP"
-
-    # Replace tag in with new string
-    sed -i "s/^-xyz-COPYRIGHT-zyx-$/$COPYRIGHT/" "$TMP"
-    cp "$TMP" "$file"
-done
-
-rm -f "$TMP"