aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/body8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/b8e5c376-32a4-42ea-b6b2-adbee069384a/body19
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/b8e5c376-32a4-42ea-b6b2-adbee069384a/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/f5139012-e20b-4d24-90a5-10d969ddd364/body76
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/f5139012-e20b-4d24-90a5-10d969ddd364/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/comments/2f9beed6-4008-442a-8d44-a45cb7ce0a36/body9
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/comments/2f9beed6-4008-442a-8d44-a45cb7ce0a36/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/02223264-e28a-4720-9f20-1e7a27a7041d/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body7
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0ca2d112-b5bb-4df1-8ac0-e46db6cdd442/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/body34
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/body29
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/body10
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/body23
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0e0c806c-5443-4839-aa60-9615c8c10853/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/body14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/body14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/body30
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/body45
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b/body25
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/body28
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/body25
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/body93
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/body32
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/body73
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/body83
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/body70
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/body15
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/body95
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/body87
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/body37
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/body33
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/body27
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/body22
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/body26
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/comments/85770405-0ead-4044-a3cf-082615ff1b6f/body22
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/comments/85770405-0ead-4044-a3cf-082615ff1b6f/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body5
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/body5
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/body2
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/body56
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/body47
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/body8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/body26
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/body20
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/body19
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/body37
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/body20
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values24
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/27bb8bc2-05c2-417a-9d09-928471380d7a/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2b81b428-fc43-4970-9469-b442385b9c0d/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/body23
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/body47
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/body25
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/body50
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/body23
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/body123
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/body47
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/body35
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/body93
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/body32
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/body45
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/body43
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/body43
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/values20
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/body51
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46/body11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/e7d8343a-bd85-4359-bcda-bf0dc1e8177a/body42
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/e7d8343a-bd85-4359-bcda-bf0dc1e8177a/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3613e6e9-db9e-4775-8914-f31f0b4b81ac/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/body4
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/body10
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/body16
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/body5
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/body8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/body10
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/body2
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/values20
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/body24
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/body33
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/values21
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values20
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/body29
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values20
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/body2
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/body2
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/body30
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/body16
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/values22
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/body20
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body13
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/body9
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/values21
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/body36
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/body27
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/body115
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/body58
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/body96
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/body52
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/body51
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494/body38
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/body22
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8/body58
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/body36
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/body102
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4/body14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/body51
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/body30
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/body37
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/body25
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486/body102
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/body18
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/body72
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/body88
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/body2
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/body55
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/body44
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body18
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body28
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/body30
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/body4
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/1b21dabc-a90c-4687-bea0-7e9e69956e23/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/1b21dabc-a90c-4687-bea0-7e9e69956e23/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/285006ba-16fc-4d09-86f1-893ff515e487/body4
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/285006ba-16fc-4d09-86f1-893ff515e487/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body7
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/0922b8f7-b5ce-4572-8e55-c4b34dafe6cf/body9
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/0922b8f7-b5ce-4572-8e55-c4b34dafe6cf/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/212570d2-8116-462e-9664-9ea8d3976b99/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/212570d2-8116-462e-9664-9ea8d3976b99/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/5e8d490e-ee06-4403-96dd-ad8eac66b21c/body17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/5e8d490e-ee06-4403-96dd-ad8eac66b21c/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/628a050a-f969-4290-8468-f5e991528f40/body11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/628a050a-f969-4290-8468-f5e991528f40/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/f3e90a7e-b8c4-4a7c-8609-6a783ae59762/body18
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/f3e90a7e-b8c4-4a7c-8609-6a783ae59762/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/values21
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/62a74b85-0d4b-49f5-8794-74bafd871cd4/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/65776f00-34d8-4b58-874d-333196a5e245/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6622c06a-ed84-4d45-8011-a082fca219b6/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/values15
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/764b812f-a0bb-4f4d-8e2f-c255c9474a0e/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/body4
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/comments/a555d577-7f8c-49f2-96f6-263ce5fdff8e/body23
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/comments/a555d577-7f8c-49f2-96f6-263ce5fdff8e/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7d182ab9-9c0c-4b4f-885e-c5762d7a2437/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/body39
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/body5
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/body2
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/body47
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/body55
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/17d045d1-3b21-4d3d-8f81-29a5bbc5e6c1/body14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/17d045d1-3b21-4d3d-8f81-29a5bbc5e6c1/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/d463e2d9-6dcc-41a4-a6b2-647fb3bddf88/body67
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/d463e2d9-6dcc-41a4-a6b2-647fb3bddf88/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8385a1fb-63df-4ca6-81cd-28ede83bb0c2/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body15
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/body38
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/body2
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/body170
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/body4
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/432e994f-3759-42bf-a80d-7cd626c7ce7c/body40
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/432e994f-3759-42bf-a80d-7cd626c7ce7c/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/e3d802cf-1fff-4a48-a61c-a07578969333/body5
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/e3d802cf-1fff-4a48-a61c-a07578969333/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9b1a0e71-4f7d-40b1-ab32-18496bf19a3f/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/comments/7cd2d475-676f-4d60-b431-c7635468e9bd/body9
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/comments/7cd2d475-676f-4d60-b431-c7635468e9bd/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/body19
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/body17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/body9
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9daa72ee-0721-4f68-99ee-f06fec0b340e/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9f910ee0-ff0f-4fa3-b1e3-79a4118e48e9/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/body21
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a63bd76a-cd43-4f97-88ba-2323546d4572/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b1bc6f39-8166-46c5-a724-4c4a3e1e7d74/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/2a51d90a-d47e-4a67-abe7-cce19c1eafad/body21
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/2a51d90a-d47e-4a67-abe7-cce19c1eafad/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/body40
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3c6da51-3a30-42c9-8c75-587c7a1705c5/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values20
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/27a5a4cc-1782-4509-a3d2-db00c190f97d/body12
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/27a5a4cc-1782-4509-a3d2-db00c190f97d/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/76d54016-755b-42ca-ad07-eb9a1c77c33d/body2
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/76d54016-755b-42ca-ad07-eb9a1c77c33d/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/body7
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/body5
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/body6
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/body7
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/body14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/body10
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cb56c990-a757-4aef-9888-a30918a7b3d7/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/body8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values15
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/body2
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/body19
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/body26
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/body77
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/body52
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/body25
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/body28
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/body22
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/body26
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/body62
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/values20
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/da2b09ff-af24-40f3-9b8d-6ffaa5f41164/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/body10
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/body26
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c/values15
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/body43
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0155831-499f-421a-ad02-cd15fc3fecf1/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/body19
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/body27
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/body27
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/body58
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/body26
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/body38
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/body10
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/values20
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/body5
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/body3
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/2cd562f5-fcb9-4cc5-bf8c-ad5c9d960761/body15
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/2cd562f5-fcb9-4cc5-bf8c-ad5c9d960761/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/68ec74b9-d2c7-421f-ac70-602b43bbd263/body5
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/68ec74b9-d2c7-421f-ac70-602b43bbd263/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/body8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/body5
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/body5
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ecc91b94-7f3f-44a7-af58-03191d327a7f/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9525e3f3-a044-4fa9-b311-56336267b8b5/body21
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9525e3f3-a044-4fa9-b311-56336267b8b5/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9c4b8921-7b43-4bb6-b650-34144b414dc0/body23
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9c4b8921-7b43-4bb6-b650-34144b414dc0/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/c664b7be-ded5-42dd-a16a-82b2bdb52e36/body7
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/c664b7be-ded5-42dd-a16a-82b2bdb52e36/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values15
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values15
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values15
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f65b680b-4309-43a2-ae2d-e65811c9d107/values11
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f77fc673-c852-4c81-bfa2-1d59de2661c8/values17
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body5
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body9
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/body1
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/values8
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values14
-rw-r--r--.be/bea86499-824e-4e77-b085-2d581fa9ccab/settings14
-rw-r--r--.be/version2
-rw-r--r--.bzrignore12
-rw-r--r--AUTHORS15
-rw-r--r--COPYING339
-rw-r--r--Makefile81
-rw-r--r--NEWS152
-rw-r--r--README81
-rwxr-xr-xbe21
-rw-r--r--doc/Makefile94
-rw-r--r--doc/conf.py198
-rw-r--r--doc/distributed_bugtracking.txt57
-rw-r--r--doc/doc.txt23
l---------doc/email.txt1
-rw-r--r--doc/generate-libbe-txt.py52
-rw-r--r--doc/hacking.txt75
-rw-r--r--doc/html.txt7
-rw-r--r--doc/index.txt39
-rw-r--r--doc/install.txt55
-rw-r--r--doc/man/be.1.sgml134
-rw-r--r--doc/spam.txt60
-rw-r--r--doc/tutorial.txt393
-rw-r--r--interfaces/email/interactive/README160
-rw-r--r--interfaces/email/interactive/_procmailrc22
-rwxr-xr-xinterfaces/email/interactive/be-handle-mail909
-rw-r--r--interfaces/email/interactive/examples/blank (renamed from __init__.py)0
-rw-r--r--interfaces/email/interactive/examples/comment11
-rw-r--r--interfaces/email/interactive/examples/email_bugs37
-rw-r--r--interfaces/email/interactive/examples/failing_multiples16
-rw-r--r--interfaces/email/interactive/examples/invalid_command11
-rw-r--r--interfaces/email/interactive/examples/invalid_subject9
-rw-r--r--interfaces/email/interactive/examples/list11
-rw-r--r--interfaces/email/interactive/examples/missing_command11
-rw-r--r--interfaces/email/interactive/examples/multiple_commands14
-rw-r--r--interfaces/email/interactive/examples/new19
-rw-r--r--interfaces/email/interactive/examples/new_with_comment13
-rw-r--r--interfaces/email/interactive/examples/show11
-rw-r--r--interfaces/email/interactive/examples/unicode11
l---------interfaces/email/interactive/libbe1
-rw-r--r--interfaces/email/interactive/send_pgp_mime.py611
-rw-r--r--interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/body (renamed from .be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/body)0
-rw-r--r--interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/values (renamed from .be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/values)0
-rw-r--r--interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/values (renamed from .be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/values)0
-rw-r--r--interfaces/web/.be/bugs/0a234f51-2fdf-4001-a04f-b7e02c2fa47b/values (renamed from .be/bugs/0a234f51-2fdf-4001-a04f-b7e02c2fa47b/values)0
-rw-r--r--interfaces/web/.be/bugs/0be47243-c172-4de9-b71b-d5dea60f91d5/values (renamed from .be/bugs/0be47243-c172-4de9-b71b-d5dea60f91d5/values)0
-rw-r--r--interfaces/web/.be/bugs/171819aa-c092-4ddf-ace3-797635fa2572/values (renamed from .be/bugs/171819aa-c092-4ddf-ace3-797635fa2572/values)0
-rw-r--r--interfaces/web/.be/bugs/24555ea1-76b5-40a8-918f-115a28f5f36a/values (renamed from .be/bugs/24555ea1-76b5-40a8-918f-115a28f5f36a/values)0
-rw-r--r--interfaces/web/.be/bugs/312fb152-0155-45c1-9d4d-f49dd5816fbb/values (renamed from .be/bugs/312fb152-0155-45c1-9d4d-f49dd5816fbb/values)0
-rw-r--r--interfaces/web/.be/bugs/35b962a0-a64a-4b5c-82c5-ea740e8a6322/values (renamed from .be/bugs/35b962a0-a64a-4b5c-82c5-ea740e8a6322/values)0
-rw-r--r--interfaces/web/.be/bugs/42716dc2-6201-4537-b5fd-e1280812a53d/values (renamed from .be/bugs/42716dc2-6201-4537-b5fd-e1280812a53d/values)0
-rw-r--r--interfaces/web/.be/bugs/4286c0f8-5703-4bc1-b256-414dc408f067/values (renamed from .be/bugs/4286c0f8-5703-4bc1-b256-414dc408f067/values)0
-rw-r--r--interfaces/web/.be/bugs/528b2e84-a944-4628-a18f-cc1def1c7e16/values (renamed from .be/bugs/528b2e84-a944-4628-a18f-cc1def1c7e16/values)0
-rw-r--r--interfaces/web/.be/bugs/52a15454-196c-4990-b55d-be2e37d575c3/values (renamed from .be/bugs/52a15454-196c-4990-b55d-be2e37d575c3/values)0
-rw-r--r--interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/body (renamed from .be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/body)0
-rw-r--r--interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/values (renamed from .be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/values)0
-rw-r--r--interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/values (renamed from .be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/values)0
-rw-r--r--interfaces/web/.be/bugs/55e76f74-37fb-4254-8498-54b703ba54f6/values (renamed from .be/bugs/55e76f74-37fb-4254-8498-54b703ba54f6/values)0
-rw-r--r--interfaces/web/.be/bugs/615ad650-9fb9-4026-9779-58d42b4e528e/values (renamed from .be/bugs/615ad650-9fb9-4026-9779-58d42b4e528e/values)0
-rw-r--r--interfaces/web/.be/bugs/63619cf7-89eb-4e64-91e9-b8a73d2a6c72/values (renamed from .be/bugs/63619cf7-89eb-4e64-91e9-b8a73d2a6c72/values)0
-rw-r--r--interfaces/web/.be/bugs/700cd3f1-70b6-4887-89a2-c1d039732add/values (renamed from .be/bugs/700cd3f1-70b6-4887-89a2-c1d039732add/values)0
-rw-r--r--interfaces/web/.be/bugs/81f69fbd-1ca5-4f89-a6e1-79ea1e6bf4d9/values (renamed from .be/bugs/81f69fbd-1ca5-4f89-a6e1-79ea1e6bf4d9/values)0
-rw-r--r--interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/body (renamed from .be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/body)0
-rw-r--r--interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/values (renamed from .be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/values)0
-rw-r--r--interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/values (renamed from .be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/values)0
-rw-r--r--interfaces/web/.be/bugs/870d5dbe-6449-4ec4-ae6f-e84bebadbce0/values (renamed from .be/bugs/870d5dbe-6449-4ec4-ae6f-e84bebadbce0/values)0
-rw-r--r--interfaces/web/.be/bugs/8cb9045c-7266-4c40-9a76-65f3c5d5bb60/values (renamed from .be/bugs/8cb9045c-7266-4c40-9a76-65f3c5d5bb60/values)0
-rw-r--r--interfaces/web/.be/bugs/984472f6-98f5-48fc-b521-70a1e5f60614/values (renamed from .be/bugs/984472f6-98f5-48fc-b521-70a1e5f60614/values)0
-rw-r--r--interfaces/web/.be/bugs/9bc14860-b2bb-4442-85ea-0b8e7083457b/values (renamed from .be/bugs/9bc14860-b2bb-4442-85ea-0b8e7083457b/values)0
-rw-r--r--interfaces/web/.be/bugs/ac72991a-72e5-4b14-b53c-0fa38d0f31bb/values (renamed from .be/bugs/ac72991a-72e5-4b14-b53c-0fa38d0f31bb/values)0
-rw-r--r--interfaces/web/.be/bugs/bef126a0-27be-402f-84fa-85f6342c97c0/values (renamed from .be/bugs/bef126a0-27be-402f-84fa-85f6342c97c0/values)0
-rw-r--r--interfaces/web/.be/bugs/c7251ff9-24e4-402d-8d4e-605a78b9a91d/values (renamed from .be/bugs/c7251ff9-24e4-402d-8d4e-605a78b9a91d/values)0
-rw-r--r--interfaces/web/.be/bugs/cfb52b6c-d1a6-4018-a255-27cc1c878193/values (renamed from .be/bugs/cfb52b6c-d1a6-4018-a255-27cc1c878193/values)0
-rw-r--r--interfaces/web/.be/bugs/d63d0bdd-e025-4f7c-9fcf-47a71de6d4d4/values (renamed from .be/bugs/d63d0bdd-e025-4f7c-9fcf-47a71de6d4d4/values)0
-rw-r--r--interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/body (renamed from .be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/body)0
-rw-r--r--interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/values (renamed from .be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/values)0
-rw-r--r--interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/values (renamed from .be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/values)0
-rw-r--r--interfaces/web/.be/bugs/decc6e78-a3db-4cd3-ad23-2bf8ed77cb0d/values (renamed from .be/bugs/decc6e78-a3db-4cd3-ad23-2bf8ed77cb0d/values)0
-rw-r--r--interfaces/web/.be/bugs/e22a9048-9a97-41b1-91a2-d4178c674b37/values (renamed from .be/bugs/e22a9048-9a97-41b1-91a2-d4178c674b37/values)0
-rw-r--r--interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/body (renamed from .be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/body)0
-rw-r--r--interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/values (renamed from .be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/values)0
-rw-r--r--interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/body (renamed from .be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/body)0
-rw-r--r--interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/values (renamed from .be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/values)0
-rw-r--r--interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/body (renamed from .be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/body)0
-rw-r--r--interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/values (renamed from .be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/values)0
-rw-r--r--interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/values (renamed from .be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/values)0
-rw-r--r--interfaces/web/.be/bugs/fd96c69d-6f78-4c0c-af6e-e01e9b8516d3/values (renamed from .be/bugs/fd96c69d-6f78-4c0c-af6e-e01e9b8516d3/values)0
-rw-r--r--interfaces/web/.be/settings (renamed from .be/settings)0
-rw-r--r--interfaces/web/.be/version1
-rw-r--r--interfaces/web/.hgignore (renamed from .hgignore)0
-rw-r--r--interfaces/web/.hgtags (renamed from .hgtags)0
-rw-r--r--interfaces/web/LICENSE (renamed from LICENSE)0
-rw-r--r--interfaces/web/README20
-rw-r--r--interfaces/web/__init__.py0
-rwxr-xr-xinterfaces/web/cfbe.py (renamed from cfbe.py)0
-rw-r--r--interfaces/web/static/scripts/jquery.corners.min.js (renamed from static/scripts/jquery.corners.min.js)0
-rw-r--r--interfaces/web/static/style/aal.css (renamed from static/style/aal.css)0
-rw-r--r--interfaces/web/static/style/cfbe.css (renamed from static/style/cfbe.css)0
-rw-r--r--interfaces/web/templates/base.html (renamed from templates/base.html)0
-rw-r--r--interfaces/web/templates/bug.html (renamed from templates/bug.html)0
-rw-r--r--interfaces/web/templates/list.html (renamed from templates/list.html)0
-rw-r--r--interfaces/web/web.py (renamed from web.py)0
-rw-r--r--libbe/__init__.py53
-rw-r--r--libbe/bug.py853
-rw-r--r--libbe/bugdir.py563
-rw-r--r--libbe/command/__init__.py40
-rw-r--r--libbe/command/assign.py98
-rw-r--r--libbe/command/base.py554
-rw-r--r--libbe/command/close.py60
-rw-r--r--libbe/command/comment.py169
-rw-r--r--libbe/command/commit.py93
-rw-r--r--libbe/command/depend.py408
-rw-r--r--libbe/command/diff.py139
-rw-r--r--libbe/command/due.py117
-rw-r--r--libbe/command/help.py82
-rw-r--r--libbe/command/html.py719
-rw-r--r--libbe/command/import_xml.py541
-rw-r--r--libbe/command/init.py132
-rw-r--r--libbe/command/list.py279
-rw-r--r--libbe/command/merge.py189
-rw-r--r--libbe/command/new.py103
-rw-r--r--libbe/command/open.py58
-rw-r--r--libbe/command/remove.py79
-rw-r--r--libbe/command/serve.py1172
-rw-r--r--libbe/command/set.py144
-rw-r--r--libbe/command/severity.py98
-rw-r--r--libbe/command/show.py207
-rw-r--r--libbe/command/status.py108
-rw-r--r--libbe/command/subscribe.py385
-rw-r--r--libbe/command/tag.py152
-rw-r--r--libbe/command/target.py209
-rw-r--r--libbe/command/util.py203
-rw-r--r--libbe/comment.py769
-rw-r--r--libbe/diff.py691
-rw-r--r--libbe/error.py26
-rw-r--r--libbe/storage/__init__.py74
-rw-r--r--libbe/storage/base.py1070
-rw-r--r--libbe/storage/http.py446
-rw-r--r--libbe/storage/util/__init__.py0
-rw-r--r--libbe/storage/util/config.py114
-rw-r--r--libbe/storage/util/mapfile.py146
-rw-r--r--libbe/storage/util/properties.py666
-rw-r--r--libbe/storage/util/settings_object.py617
-rw-r--r--libbe/storage/util/upgrade.py331
-rw-r--r--libbe/storage/vcs/__init__.py41
-rw-r--r--libbe/storage/vcs/arch.py441
-rw-r--r--libbe/storage/vcs/base.py1127
-rw-r--r--libbe/storage/vcs/bzr.py361
-rw-r--r--libbe/storage/vcs/darcs.py399
-rw-r--r--libbe/storage/vcs/git.py269
-rw-r--r--libbe/storage/vcs/hg.py257
-rw-r--r--libbe/ui/__init__.py15
-rw-r--r--libbe/ui/command_line.py340
-rw-r--r--libbe/ui/util/__init__.py15
-rw-r--r--libbe/ui/util/editor.py115
-rw-r--r--libbe/ui/util/pager.py65
-rw-r--r--libbe/ui/util/user.py134
-rw-r--r--libbe/util/__init__.py24
-rw-r--r--libbe/util/encoding.py91
-rw-r--r--libbe/util/id.py713
-rw-r--r--libbe/util/plugin.py67
-rw-r--r--libbe/util/subproc.py223
-rw-r--r--libbe/util/tree.py258
-rw-r--r--libbe/util/utility.py248
-rw-r--r--libbe/version.py56
-rw-r--r--misc/completion/be.bash39
-rw-r--r--misc/logo.svg83
-rwxr-xr-xmisc/xml/be-mail-to-xml175
-rwxr-xr-xmisc/xml/be-xml-to-mbox220
-rwxr-xr-xmisc/xml/catmutt59
-rwxr-xr-xrelease.py161
-rwxr-xr-xsetup.py26
-rw-r--r--test.py122
-rwxr-xr-xtest_upgrade.py33
-rwxr-xr-xtest_usage.sh154
-rwxr-xr-xupdate_copyright.py318
736 files changed, 32709 insertions, 16 deletions
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/body
new file mode 100644
index 0000000..ff42ab3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/body
@@ -0,0 +1,8 @@
+The FSF offices are no longer at Temple Place, and there is a revised
+text of the GPLv2 giving the correct address and other textual
+clean-ups.
+
+The COPYING file should be updated to the new GPLv2 text, and the
+copyright notices throughout the working tree should be updated for
+the new boilerplate how-to-use-the-GPL text.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/values
new file mode 100644
index 0000000..02ee559
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b/values
@@ -0,0 +1,8 @@
+Author: benf
+
+
+Content-type: text/plain
+
+
+Date: Fri, 18 Apr 2008 11:21:03 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/body
new file mode 100644
index 0000000..19b1cf5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/body
@@ -0,0 +1 @@
+We could add this functionality to update_copyright.sh
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/values
new file mode 100644
index 0000000..beec197
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 11 Jul 2009 14:08:45 +0000
+
+
+In-reply-to: 4be73baf-e46b-4acb-a58e-4719e57c550b
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/values
new file mode 100644
index 0000000..2094f46
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/values
@@ -0,0 +1,17 @@
+assigned: Ben Finney <ben+python@benfinney.id.au>
+
+
+creator: benf
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Address is outdated for FSF offices
+
+
+time: Fri, 18 Apr 2008 11:18:58 +0000
+
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
index 0000000..6af098a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/b8e5c376-32a4-42ea-b6b2-adbee069384a/body
@@ -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
index 0000000..378cc67
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/b8e5c376-32a4-42ea-b6b2-adbee069384a/values
@@ -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
index 0000000..636137c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/f5139012-e20b-4d24-90a5-10d969ddd364/body
@@ -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
index 0000000..fb6ab4e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/comments/f5139012-e20b-4d24-90a5-10d969ddd364/values
@@ -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
index 0000000..d1b7cbe
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01c9a900-61f9-41f7-9b2f-dd8f89e25b1b/values
@@ -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
index 0000000..d20af30
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/comments/2f9beed6-4008-442a-8d44-a45cb7ce0a36/body
@@ -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
index 0000000..c7365f5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/comments/2f9beed6-4008-442a-8d44-a45cb7ce0a36/values
@@ -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
index 0000000..9efe209
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/01e7151c-6113-4c8f-9fc5-4d594431bd2b/values
@@ -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/02223264-e28a-4720-9f20-1e7a27a7041d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/02223264-e28a-4720-9f20-1e7a27a7041d/values
new file mode 100644
index 0000000..afa7cb1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/02223264-e28a-4720-9f20-1e7a27a7041d/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Needs more test cases
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body
new file mode 100644
index 0000000..3b5e0e7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body
@@ -0,0 +1 @@
+Merged from bug 597a7386-643f-4559-8dc4-6871924229b6 \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values
new file mode 100644
index 0000000..34b6514
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 13:35:41 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/body
new file mode 100644
index 0000000..21fb43d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/body
@@ -0,0 +1 @@
+Add *support*, damnit!
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values
new file mode 100644
index 0000000..66a9f19
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/rst
+
+
+Date: Thu, 06 Apr 2006 16:47:25 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body
new file mode 100644
index 0000000..9106d37
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body
@@ -0,0 +1,7 @@
+This is an *rst* comment.
+Which means newlines don't matter, except when they gang up.
+
+lala
+
+ - Bullet
+ - Bullet \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values
new file mode 100644
index 0000000..3f1fb15
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values
@@ -0,0 +1,11 @@
+Author: abentley
+
+
+Content-type: text/restructured
+
+
+Date: Thu, 06 Apr 2006 16:54:57 +0000
+
+
+In-reply-to: 144c238c-75d1-40f1-82c1-647668bcf2bc
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/values
new file mode 100644
index 0000000..bb41d02
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Support RST
+
+
+time: Thu, 06 Apr 2006 16:45:52 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0ca2d112-b5bb-4df1-8ac0-e46db6cdd442/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0ca2d112-b5bb-4df1-8ac0-e46db6cdd442/values
new file mode 100644
index 0000000..e710dd8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0ca2d112-b5bb-4df1-8ac0-e46db6cdd442/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Organize list by target, and whether it's assigned to current be id
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/body
new file mode 100644
index 0000000..595381c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/body
@@ -0,0 +1,34 @@
+When I try to do set-root on a git repository, I get:
+# be set-root .
+Traceback (most recent call last):
+ File "/usr/local/bin/be", line 55, in <module>
+ sys.exit(execute(sys.argv[1], sys.argv[2:]))
+ File "/usr/lib/python2.5/site-packages/libbe/cmdutil.py", line 105, in execute
+ File "/usr/lib/python2.5/site-packages/becommands/set_root.py", line 57, in execute
+ File "/usr/lib/python2.5/site-packages/libbe/bugdir.py", line 110, in create_bug_dir
+ File "/usr/lib/python2.5/site-packages/libbe/bugdir.py", line 70, in set_version
+ File "/usr/lib/python2.5/site-packages/libbe/git.py", line 51, in set_file_contents
+ File "/usr/lib/python2.5/site-packages/libbe/git.py", line 38, in add_id
+ File "/usr/lib/python2.5/site-packages/libbe/git.py", line 33, in invoke_client
+ File "/usr/lib/python2.5/site-packages/libbe/rcs.py", line 63, in invoke
+ File "/usr/lib/python2.5/subprocess.py", line 594, in __init__
+ errread, errwrite)
+ File "/usr/lib/python2.5/subprocess.py", line 1147, in _execute_child
+ raise child_exception
+OSError: [Errno 2] No such file or directory: ''
+
+because the cwd argument for Popen is set to '' (the empty string).
+
+The following patch fixes the issue:
+--- libbe/git.py 2008-06-22 19:52:14.000000000 -0400
++++ libbe/git.py 2008-06-23 00:53:39.000000000 -0400
+@@ -26,7 +26,7 @@
+ return filename
+
+ def invoke_client(*args, **kwargs):
+- directory = kwargs['directory']
++ directory = kwargs['directory'] or None
+ expect = kwargs.get('expect', (0, 1))
+ cl_args = ["git"]
+ cl_args.extend(args)
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values
new file mode 100644
index 0000000..8dc0882
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values
@@ -0,0 +1,8 @@
+Author: hubert
+
+
+Content-type: text/plain
+
+
+Date: Mon, 23 Jun 2008 05:02:22 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/body
new file mode 100644
index 0000000..49fe1fb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/body
@@ -0,0 +1,29 @@
+It looks like the problems with the git backend are more than just in the
+site-init command. It looks like several places expect that git_dir_for_path
+and git_repo_for_path return absolute paths, while in the current
+implementation, it may not be the case. Here is an updated patch to fix this.
+This replaces the previous patch that I gave in this bug. It seems to work for
+me, but I haven't heavily tested it.
+
+--- libbe/git.py 2008-06-22 19:52:14.000000000 -0400
++++ /libbe/git.py 2008-06-23 22:39:17.000000000 -0400
+@@ -102,11 +102,16 @@
+ """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.
+- return os.path.dirname(git_dir_for_path(path))
++ # "git rev-parse --show-cdup" gives the relative path to the top-level
++ # directory of the repository. We then join that to the requested path,
++ # and then use realpath to turn it into an absolute path and to get rid of
++ # ".." components.
++ return os.path.realpath(os.path.join(path,invoke_client("rev-parse", "--show-cdup", directory=path)[1].rstrip()))
+
+ def git_dir_for_path(path):
+ """Find the git-dir of the deepest repo containing path."""
+- return invoke_client("rev-parse", "--git-dir", directory=path)[1].rstrip()
++ repo = git_repo_for_path(path)
++ return os.path.join(repo,invoke_client("rev-parse", "--git-dir", directory=repo)[1].rstrip())
+
+ def export(spec, bug_dir, revision_dir):
+ """Check out commit 'spec' from the git repo containing bug_dir into
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values
new file mode 100644
index 0000000..176ae7f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values
@@ -0,0 +1,8 @@
+Author: hubert
+
+
+Content-type: text/plain
+
+
+Date: Tue, 24 Jun 2008 02:45:18 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/body
new file mode 100644
index 0000000..ccc18ea
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/body
@@ -0,0 +1,10 @@
+Fixed another bug in git.strip_git(). lstrip() wasn't what I had thought.
+
+>>> "/a.b/.be/x/y".lstrip("/a.b/")
+'e/x/y'
+
+So I went back to just droping the first N chars
+
+>>> "/a.b/.be/x/y"[len("/a.b/"):]
+'.be/x/y'
+
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
index 0000000..9cfd081
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values
@@ -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/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/body
new file mode 100644
index 0000000..c889a38
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/body
@@ -0,0 +1 @@
+Fixed with a simpler patch.
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
index 0000000..9e40714
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values
@@ -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/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/body
new file mode 100644
index 0000000..7c07a0f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/body
@@ -0,0 +1,23 @@
+Oops, missed a case. I now see what Hubert was saying about absolute
+paths :p. In git.strip_git(), the output of git_repo_for_path('.')
+was being subtracted from an absolute path. Obviously, if the path
+was returning '.', you'd get things like
+
+filename=
+/home/wking/src/fun/testbe/.be/bugs/c3bf839b-88f9-4609-89a2-6a5b75c415b8/values
+
+stripping 2 chars ('.' and '/')], returns
+ome/wking/src/fun/testbe/.be/bugs/c3bf839b-88f9-4609-89a2-6a5b75c415b8/values
+
+
+Now we convert the git_repo_for_path output to an absolute path and get
+
+filename=
+/home/wking/src/fun/testbe/.be/bugs/c3bf839b-88f9-4609-89a2-6a5b75c415b8/values
+absRepoPath=
+/home/wking/src/fun/testbe
+absRepoSlashedDir=
+/home/wking/src/fun/testbe/
+returns
+.be/bugs/c3bf839b-88f9-4609-89a2-6a5b75c415b8/values
+
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
index 0000000..ce0ab73
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values
@@ -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/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/values
new file mode 100644
index 0000000..1f20dfb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/values
@@ -0,0 +1,14 @@
+creator: hubert
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: set-root in git repository fails
+
+
+time: Mon, 23 Jun 2008 04:57:22 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0e0c806c-5443-4839-aa60-9615c8c10853/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0e0c806c-5443-4839-aa60-9615c8c10853/values
new file mode 100644
index 0000000..3237861
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0e0c806c-5443-4839-aa60-9615c8c10853/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: fix up command listings
+
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
index 0000000..861fb1d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/body
@@ -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
index 0000000..65e4472
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/values
@@ -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
index 0000000..df5b8c5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/body
@@ -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
index 0000000..d21650d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/values
@@ -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
index 0000000..abb898c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/body
@@ -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
index 0000000..d2e65d3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/values
@@ -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
index 0000000..21170a2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/body
@@ -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
index 0000000..f0af48d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/values
@@ -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
index 0000000..f4b1032
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/values
@@ -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/0f60a148-7024-44bd-bbed-377cbece9d1b/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b/body
new file mode 100644
index 0000000..d3f00e7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b/body
@@ -0,0 +1,25 @@
+Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> writes:
+
+> the basic idea is to take a look at all public branches (for exaple
+> all on lp/bitbucket/github) in order to tell the user of a
+> webinterface that bug foo is fixed in branch xyz, and if its merged to
+> the main branch
+
+I don't understand. The state of the bug in the main branch is right
+there in the main branch; if it's not fixed there, it's not fixed there.
+If it's merged in from a different branch, the bug state follows all the
+other changes when they come in.
+
+Can you give an example of what would be done differently?
+
+--
+ \ “The basic fact about human existence is not that it is a |
+ `\ tragedy, but that it is a bore.” —Henry L. Mencken |
+_o__) |
+Ben Finney
+
+
+_______________________________________________
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b/values
new file mode 100644
index 0000000..4c93931
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b/values
@@ -0,0 +1,14 @@
+Alt-id: <874otjmjhr.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 11 Jul 2009 23:34:08 +1000
+
+
+In-reply-to: 88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/body
new file mode 100644
index 0000000..1f6d84b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/body
@@ -0,0 +1,28 @@
+On Sat, Jul 11, 2009 at 01:54:54PM +0200, Ronny Pfannschmidt wrote:
+> 1. is there any way to aggregate over multiple public branches in order
+> to get the complete bug state
+
+Keeping the bug data with the source helps synchronize bug state and
+source code. Bug state in branch A may not apply to branch B. Some
+people like to weaken this source-bug linkage by keeping their bugs in
+a branch all by themselves (ditz [http://ditz.rubyforge.org/]
+currently supports this workflow). It sounds like you want to move
+from "bugs with code" to "bugs and code in separate branches". We
+don't have an easy way to do that in BE at the moment, since
+version-control systems like Git have a single working branch at a
+time (I think :p). What VCS are you using as a backend?
+
+> 2. is there any model for storing bigger files at a central place (for
+> some of my bugs i have multi-megabyte tarballs attached)
+
+ be comment ID "See the tarball at http://yourpage/something.tar.gz"
+Then to grab the tarball, you'd use:
+ wget `be show COMMENT-ID | sed -n 's/ *See the tarball at //p'`
+to grab it.
+
+--
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/values
new file mode 100644
index 0000000..a73aeeb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/values
@@ -0,0 +1,14 @@
+Alt-id: <20090711125030.GA18185@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 11 Jul 2009 08:50:30 -0400
+
+
+In-reply-to: 88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/body
new file mode 100644
index 0000000..bd9e63a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/body
@@ -0,0 +1,25 @@
+Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> writes:
+
+> 1. is there any way to aggregate over multiple public branches in
+> order to get the complete bug state
+
+The bug state is as complete as the source code state. It's exactly as
+aggregated as the rest of the source code; the “complete bug state”
+would be the integration branch where you merge all the feature branches
+and bug-fix branches together.
+
+If instead you want bugs to *not* be tightly linked with the rest of the
+source code state, it seems you don't want a distributed bug tracker
+like Bugs Everywhere.
+
+--
+ \ “I cannot conceive that anybody will require multiplications at |
+ `\ the rate of 40,000 or even 4,000 per hour …” —F. H. Wales, 1936 |
+_o__) |
+Ben Finney
+
+
+_______________________________________________
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/values
new file mode 100644
index 0000000..55621fb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1/values
@@ -0,0 +1,14 @@
+Alt-id: <878wivmjm1.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 11 Jul 2009 23:31:34 +1000
+
+
+In-reply-to: 88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/body
new file mode 100644
index 0000000..11f344c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/body
@@ -0,0 +1,93 @@
+On Mon, Jul 13, 2009 at 09:05:34AM +0200, Ronny Pfannschmidt wrote:
+> On Sun, 2009-07-12 at 19:55 -0400, W. Trevor King wrote:
+> > On Sun, Jul 12, 2009 at 11:20:10PM +0200, Ronny Pfannschmidt wrote:
+> > > On Sat, 2009-07-11 at 11:25 -0400, W. Trevor King wrote:
+> > > > On Sat, Jul 11, 2009 at 03:13:05PM +0200, Ronny Pfannschmidt wrote:
+> > > > > On Sat, 2009-07-11 at 08:50 -0400, W. Trevor King wrote:
+> > > > > > On Sat, Jul 11, 2009 at 01:54:54PM +0200, Ronny Pfannschmidt wrote:
+> > > > > > > 2. is there any model for storing bigger files at a central place (for
+> > > > > > > some of my bugs i have multi-megabyte tarballs attached)
+> > > > > >
+> > > > > > be comment ID "See the tarball at http://yourpage/something.tar.gz"
+> > > > > > Then to grab the tarball, you'd use:
+> > > > > > wget `be show COMMENT-ID | sed -n 's/ *See the tarball at //p'`
+> > > > > > to grab it.
+> > > > >
+> > > > > so the basic idea is to do it completely self-managed
+> > > > > and have have heterogenous sources of extended data?
+> > > >
+> > > > I assume "extended data" here refers to your tarballs. What sort of
+> > > > homogenous source did you have in mind? The comment body is currently
+> > > > just a binary blob for non-text/* types, otherwise it's text in
+> > > > whatever encoding you've configured.
+> > >
+> > > some kind of common upload target for a single project in order to have
+> > > more reliable sources of stuff thats related to bugs but doesnt fit into
+> > > the normal repository
+> >
+> > Sorry, I'm still having trouble with "doesn't fit into the normal
+> > repository". It's going to be large wherever you keep it. You
+> > worried about multiple branches all having these big tarballs in them
+> > and want a "lightweight" checkout without all the big
+> > tarballs/whatever? I still think having some sort of "resources"
+> > directory on an http server somewhere that you link to from comments
+> > is the best plan. If you use the
+> > be show --xml ID | be-xml-to-mbox | catmutt
+> > approach, you can even write your comments in text/html and get
+> > clickable links ;). A "push big file to remote and commit comment
+> > linking to it" script would be pretty simple and keep everything
+> > consistent.
+>
+> thats probably what i want to do
+
+#!/bin/bash
+REMOTE_DIR="you@webhost:./public_html/bigfiles"
+REMOTE_LINK="http://www.webhost.com/bigfiles"
+if [ $# -ne 2 ]; then
+ echo "usage: $0 ID BIGFILE"
+ exit 1
+fi
+ID="$1"
+BIGFILE="$2"
+be comment "$ID" "Large file stored at ${REMOTE_LINK}/${BIGFILE}" && scp "$BIGFILE" "${REMOTE_DIR}"
+
+> > > > On Sun, Jul 12, 2009 at 12:57:35AM +1000, Ben Finney wrote:
+> > > > > Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> writes:
+> > > > >
+> > > > > > i want to see the combination of the bug data of all branches
+> > > > >
+> > > > > How is a tool to determine the set of “all branches”? The distributed
+> > > > > VCS model means that set is indeterminate.
+> > > >
+> > > > He could just make a list of branches he likes.
+> > > >
+> > > > Ronny, are you looking to check bug status across several repos on the
+> > > > fly, or periodically run something (with cron, etc.) to update a
+> > > > static multi-repo summary?
+> > >
+> > > on the fly access
+> >
+> > Then listing bugs in a remote repo will either involve httping tons of
+> > tiny values files for each bug (slow?) or running some hypothetical
+> > BE-server locally for each repo speaking some BE-specific protocol
+> > (complicated?). And how would you handle e.g. headless git repos,
+> > where nothing's even checked out?
+> >
+> > You could always run the cron job every 15 minutes, and rely on
+> > whatever VCS you're using having some intelligent protocol/procedure
+> > to keep bandwidth down ;). If you need faster / more-efficient
+> > updates, you'll probably need to throw out polling altogether and
+> > setup all involved repos with a "push to central-repo on commit" hook
+> > or some such.
+>
+> its intended to run on the place where i publish the repositories anyway
+
+Oh, you mean all the repos you want to cover are all _already_ on the
+same host?
+
+--
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/values
new file mode 100644
index 0000000..bb2305f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/values
@@ -0,0 +1,14 @@
+Alt-id: <20090713104715.GA13723@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 13 Jul 2009 06:47:15 -0400
+
+
+In-reply-to: 6dcc910a-ce15-4eeb-b49b-4747719748ed
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/body
new file mode 100644
index 0000000..cf3c990
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/body
@@ -0,0 +1,32 @@
+On Sat, Jul 11, 2009 at 11:25:07AM -0400, W. Trevor King wrote:
+> The easiest implementation I can think of would be to keep local
+> branches (on whatever computer is hosting your web interface)
+> following your favorite repos.
+> proxectX/
+> |-- repoA
+> |-- repoB
+> `-- repoC
+> You'd pull upstream changes with a cron job.
+> Listing bugs would be something along the lines of
+> projectX$ for repo in *
+> do
+> pushd $repo
+> be list
+> popd
+> done | sort | uniq
+> ...
+
+I've reworked option handling for be, so my branch now supports
+ projectX$ for repo in *
+ do
+ be --dir $repo list
+ done | sort | uniq
+etc. This also makes it easy to use your uninstalled development
+version of be on any bug directory on your local machine.
+
+--
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/values
new file mode 100644
index 0000000..60c80a1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/values
@@ -0,0 +1,14 @@
+Alt-id: <20090713115734.GA13788@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 13 Jul 2009 07:57:34 -0400
+
+
+In-reply-to: bd98f525-95ec-446a-84e8-34c7d6fa5b40
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/body
new file mode 100644
index 0000000..c22de06
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/body
@@ -0,0 +1,73 @@
+On Sun, Jul 12, 2009 at 11:20:10PM +0200, Ronny Pfannschmidt wrote:
+> On Sat, 2009-07-11 at 11:25 -0400, W. Trevor King wrote:
+> > On Sat, Jul 11, 2009 at 03:13:05PM +0200, Ronny Pfannschmidt wrote:
+> > > On Sat, 2009-07-11 at 08:50 -0400, W. Trevor King wrote:
+> > > > On Sat, Jul 11, 2009 at 01:54:54PM +0200, Ronny Pfannschmidt wrote:
+> > > > > 2. is there any model for storing bigger files at a central place (for
+> > > > > some of my bugs i have multi-megabyte tarballs attached)
+> > > >
+> > > > be comment ID "See the tarball at http://yourpage/something.tar.gz"
+> > > > Then to grab the tarball, you'd use:
+> > > > wget `be show COMMENT-ID | sed -n 's/ *See the tarball at //p'`
+> > > > to grab it.
+> > >
+> > > so the basic idea is to do it completely self-managed
+> > > and have have heterogenous sources of extended data?
+> >
+> > I assume "extended data" here refers to your tarballs. What sort of
+> > homogenous source did you have in mind? The comment body is currently
+> > just a binary blob for non-text/* types, otherwise it's text in
+> > whatever encoding you've configured.
+>
+> some kind of common upload target for a single project in order to have
+> more reliable sources of stuff thats related to bugs but doesnt fit into
+> the normal repository
+
+Sorry, I'm still having trouble with "doesn't fit into the normal
+repository". It's going to be large wherever you keep it. You
+worried about multiple branches all having these big tarballs in them
+and want a "lightweight" checkout without all the big
+tarballs/whatever? I still think having some sort of "resources"
+directory on an http server somewhere that you link to from comments
+is the best plan. If you use the
+ be show --xml ID | be-xml-to-mbox | catmutt
+approach, you can even write your comments in text/html and get
+clickable links ;). A "push big file to remote and commit comment
+linking to it" script would be pretty simple and keep everything
+consistent.
+
+> > On Sun, Jul 12, 2009 at 12:57:35AM +1000, Ben Finney wrote:
+> > > Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> writes:
+> > >
+> > > > i want to see the combination of the bug data of all branches
+> > >
+> > > How is a tool to determine the set of “all branches”? The distributed
+> > > VCS model means that set is indeterminate.
+> >
+> > He could just make a list of branches he likes.
+> >
+> > Ronny, are you looking to check bug status across several repos on the
+> > fly, or periodically run something (with cron, etc.) to update a
+> > static multi-repo summary?
+>
+> on the fly access
+
+Then listing bugs in a remote repo will either involve httping tons of
+tiny values files for each bug (slow?) or running some hypothetical
+BE-server locally for each repo speaking some BE-specific protocol
+(complicated?). And how would you handle e.g. headless git repos,
+where nothing's even checked out?
+
+You could always run the cron job every 15 minutes, and rely on
+whatever VCS you're using having some intelligent protocol/procedure
+to keep bandwidth down ;). If you need faster / more-efficient
+updates, you'll probably need to throw out polling altogether and
+setup all involved repos with a "push to central-repo on commit" hook
+or some such.
+
+--
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/values
new file mode 100644
index 0000000..b5ebf31
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/values
@@ -0,0 +1,14 @@
+Alt-id: <20090712235502.GA10782@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 12 Jul 2009 19:55:02 -0400
+
+
+In-reply-to: 8ffc90d7-0be7-4b00-88e6-9ae1b65f7957
+
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
index 0000000..e7b48e0
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/body
@@ -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
index 0000000..adb1ae5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/values
@@ -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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/body
new file mode 100644
index 0000000..6b7d3eb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/body
@@ -0,0 +1,70 @@
+On Sun, 2009-07-12 at 19:55 -0400, W. Trevor King wrote:
+> On Sun, Jul 12, 2009 at 11:20:10PM +0200, Ronny Pfannschmidt wrote:
+> > On Sat, 2009-07-11 at 11:25 -0400, W. Trevor King wrote:
+> > > On Sat, Jul 11, 2009 at 03:13:05PM +0200, Ronny Pfannschmidt wrote:
+> > > > On Sat, 2009-07-11 at 08:50 -0400, W. Trevor King wrote:
+> > > > > On Sat, Jul 11, 2009 at 01:54:54PM +0200, Ronny Pfannschmidt wrote:
+> > > > > > 2. is there any model for storing bigger files at a central place (for
+> > > > > > some of my bugs i have multi-megabyte tarballs attached)
+> > > > >
+> > > > > be comment ID "See the tarball at http://yourpage/something.tar.gz"
+> > > > > Then to grab the tarball, you'd use:
+> > > > > wget `be show COMMENT-ID | sed -n 's/ *See the tarball at //p'`
+> > > > > to grab it.
+> > > >
+> > > > so the basic idea is to do it completely self-managed
+> > > > and have have heterogenous sources of extended data?
+> > >
+> > > I assume "extended data" here refers to your tarballs. What sort of
+> > > homogenous source did you have in mind? The comment body is currently
+> > > just a binary blob for non-text/* types, otherwise it's text in
+> > > whatever encoding you've configured.
+> >
+> > some kind of common upload target for a single project in order to have
+> > more reliable sources of stuff thats related to bugs but doesnt fit into
+> > the normal repository
+>
+> Sorry, I'm still having trouble with "doesn't fit into the normal
+> repository". It's going to be large wherever you keep it. You
+> worried about multiple branches all having these big tarballs in them
+> and want a "lightweight" checkout without all the big
+> tarballs/whatever? I still think having some sort of "resources"
+> directory on an http server somewhere that you link to from comments
+> is the best plan. If you use the
+> be show --xml ID | be-xml-to-mbox | catmutt
+> approach, you can even write your comments in text/html and get
+> clickable links ;). A "push big file to remote and commit comment
+> linking to it" script would be pretty simple and keep everything
+> consistent.
+thats probably what i want to do
+
+>
+> > > On Sun, Jul 12, 2009 at 12:57:35AM +1000, Ben Finney wrote:
+> > > > Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> writes:
+> > > >
+> > > > > i want to see the combination of the bug data of all branches
+> > > >
+> > > > How is a tool to determine the set of “all branches”? The distributed
+> > > > VCS model means that set is indeterminate.
+> > >
+> > > He could just make a list of branches he likes.
+> > >
+> > > Ronny, are you looking to check bug status across several repos on the
+> > > fly, or periodically run something (with cron, etc.) to update a
+> > > static multi-repo summary?
+> >
+> > on the fly access
+>
+> Then listing bugs in a remote repo will either involve httping tons of
+> tiny values files for each bug (slow?) or running some hypothetical
+> BE-server locally for each repo speaking some BE-specific protocol
+> (complicated?). And how would you handle e.g. headless git repos,
+> where nothing's even checked out?
+>
+> You could always run the cron job every 15 minutes, and rely on
+> whatever VCS you're using having some intelligent protocol/procedure
+> to keep bandwidth down ;). If you need faster / more-efficient
+> updates, you'll probably need to throw out polling altogether and
+> setup all involved repos with a "push to central-repo on commit" hook
+> or some such.
+its intended to run on the place where i publish the repositories anyway
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/values
new file mode 100644
index 0000000..bbeacb6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed/values
@@ -0,0 +1,14 @@
+Alt-id: <1247468734.7189.1.camel@localhost>
+
+
+Author: Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 13 Jul 2009 09:05:34 +0200
+
+
+In-reply-to: 4d192c6c-a4a8-4844-b083-2dd5926bd2d9
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/body
new file mode 100644
index 0000000..2f2c16e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/body
@@ -0,0 +1,15 @@
+Hi,
+
+1. is there any way to aggregate over multiple public branches in order
+to get the complete bug state
+
+2. is there any model for storing bigger files at a central place (for
+some of my bugs i have multi-megabyte tarballs attached)
+
+Regards Ronny
+
+
+_______________________________________________
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/values
new file mode 100644
index 0000000..a9cd364
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7/values
@@ -0,0 +1,11 @@
+Alt-id: <1247313294.7701.60.camel@localhost>
+
+
+Author: Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 11 Jul 2009 13:54:54 +0200
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/body
new file mode 100644
index 0000000..debd486
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/body
@@ -0,0 +1,95 @@
+On Sat, 2009-07-11 at 11:25 -0400, W. Trevor King wrote:
+> On Sat, Jul 11, 2009 at 03:13:05PM +0200, Ronny Pfannschmidt wrote:
+> > On Sat, 2009-07-11 at 08:50 -0400, W. Trevor King wrote:
+> > > On Sat, Jul 11, 2009 at 01:54:54PM +0200, Ronny Pfannschmidt wrote:
+> > > > 1. is there any way to aggregate over multiple public branches in order
+> > > > to get the complete bug state
+> > >
+> > > Keeping the bug data with the source helps synchronize bug state and
+> > > source code. Bug state in branch A may not apply to branch B. Some
+> > > people like to weaken this source-bug linkage by keeping their bugs in
+> > > a branch all by themselves (ditz [http://ditz.rubyforge.org/]
+> > > currently supports this workflow). It sounds like you want to move
+> > > from "bugs with code" to "bugs and code in separate branches". We
+> > > don't have an easy way to do that in BE at the moment, since
+> > > version-control systems like Git have a single working branch at a
+> > > time (I think :p). What VCS are you using as a backend?
+> >
+> > the basic idea is to take a look at all public branches (for exaple all
+> > on lp/bitbucket/github) in order to tell the user of a webinterface that
+> > bug foo is fixed in branch xyz, and if its merged to the main branch
+>
+> Hmm.
+>
+> > > > 2. is there any model for storing bigger files at a central place (for
+> > > > some of my bugs i have multi-megabyte tarballs attached)
+> > >
+> > > be comment ID "See the tarball at http://yourpage/something.tar.gz"
+> > > Then to grab the tarball, you'd use:
+> > > wget `be show COMMENT-ID | sed -n 's/ *See the tarball at //p'`
+> > > to grab it.
+> > so the basic idea is to do it completely self-managed
+>
+> Well, it's going to be managed by somebody ;). So far I'm not
+> convinced enough for the manager to be me, so I'm suggesting it be you
+> :p.
+>
+> > and have have heterogenous sources of extended data?
+>
+> I assume "extended data" here refers to your tarballs. What sort of
+> homogenous source did you have in mind? The comment body is currently
+> just a binary blob for non-text/* types, otherwise it's text in
+> whatever encoding you've configured.
+some kind of common upload target for a single project in order to have
+more reliable sources of stuff thats related to bugs but doesnt fit into
+the normal repository
+
+
+>
+> On Sun, Jul 12, 2009 at 12:57:35AM +1000, Ben Finney wrote:
+> > Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> writes:
+> >
+> > > i want to see the combination of the bug data of all branches
+> >
+> > How is a tool to determine the set of “all branches”? The distributed
+> > VCS model means that set is indeterminate.
+>
+> He could just make a list of branches he likes.
+>
+> Ronny, are you looking to check bug status across several repos on the
+> fly, or periodically run something (with cron, etc.) to update a
+> static multi-repo summary?
+on the fly access
+
+>
+> The easiest implementation I can think of would be to keep local
+> branches (on whatever computer is hosting your web interface)
+> following your favorite repos.
+> proxectX/
+> |-- repoA
+> |-- repoB
+> `-- repoC
+> You'd pull upstream changes with a cron job.
+> Listing bugs would be something along the lines of
+> projectX$ for repo in *
+> do
+> pushd $repo
+> be list
+> popd
+> done | sort | uniq
+> Then to show bug status you would have something like
+> projectX$ for repo in *
+> do
+> echo $repo
+> pushd $repo
+> be show ${BUGID}
+> popd
+> done
+> For a web frontend, you'd want to translate that to python/libbe.
+>
+
+yes, the idea is to get a web fontend for multiple branches
+and maybe a local gtk fontend for local multi-branch setups
+
+for some of my projects i have n local and m remote repos, and merging
+is not always intended soonish
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/values
new file mode 100644
index 0000000..5a64ee0
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957/values
@@ -0,0 +1,14 @@
+Alt-id: <1247433610.14803.3.camel@localhost>
+
+
+Author: Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 12 Jul 2009 23:20:10 +0200
+
+
+In-reply-to: bd98f525-95ec-446a-84e8-34c7d6fa5b40
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/body
new file mode 100644
index 0000000..5f55127
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/body
@@ -0,0 +1,87 @@
+On Sat, Jul 11, 2009 at 03:13:05PM +0200, Ronny Pfannschmidt wrote:
+> On Sat, 2009-07-11 at 08:50 -0400, W. Trevor King wrote:
+> > On Sat, Jul 11, 2009 at 01:54:54PM +0200, Ronny Pfannschmidt wrote:
+> > > 1. is there any way to aggregate over multiple public branches in order
+> > > to get the complete bug state
+> >
+> > Keeping the bug data with the source helps synchronize bug state and
+> > source code. Bug state in branch A may not apply to branch B. Some
+> > people like to weaken this source-bug linkage by keeping their bugs in
+> > a branch all by themselves (ditz [http://ditz.rubyforge.org/]
+> > currently supports this workflow). It sounds like you want to move
+> > from "bugs with code" to "bugs and code in separate branches". We
+> > don't have an easy way to do that in BE at the moment, since
+> > version-control systems like Git have a single working branch at a
+> > time (I think :p). What VCS are you using as a backend?
+>
+> the basic idea is to take a look at all public branches (for exaple all
+> on lp/bitbucket/github) in order to tell the user of a webinterface that
+> bug foo is fixed in branch xyz, and if its merged to the main branch
+
+Hmm.
+
+> > > 2. is there any model for storing bigger files at a central place (for
+> > > some of my bugs i have multi-megabyte tarballs attached)
+> >
+> > be comment ID "See the tarball at http://yourpage/something.tar.gz"
+> > Then to grab the tarball, you'd use:
+> > wget `be show COMMENT-ID | sed -n 's/ *See the tarball at //p'`
+> > to grab it.
+> so the basic idea is to do it completely self-managed
+
+Well, it's going to be managed by somebody ;). So far I'm not
+convinced enough for the manager to be me, so I'm suggesting it be you
+:p.
+
+> and have have heterogenous sources of extended data?
+
+I assume "extended data" here refers to your tarballs. What sort of
+homogenous source did you have in mind? The comment body is currently
+just a binary blob for non-text/* types, otherwise it's text in
+whatever encoding you've configured.
+
+On Sun, Jul 12, 2009 at 12:57:35AM +1000, Ben Finney wrote:
+> Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> writes:
+>
+> > i want to see the combination of the bug data of all branches
+>
+> How is a tool to determine the set of “all branches”? The distributed
+> VCS model means that set is indeterminate.
+
+He could just make a list of branches he likes.
+
+Ronny, are you looking to check bug status across several repos on the
+fly, or periodically run something (with cron, etc.) to update a
+static multi-repo summary?
+
+The easiest implementation I can think of would be to keep local
+branches (on whatever computer is hosting your web interface)
+following your favorite repos.
+ proxectX/
+ |-- repoA
+ |-- repoB
+ `-- repoC
+You'd pull upstream changes with a cron job.
+Listing bugs would be something along the lines of
+ projectX$ for repo in *
+ do
+ pushd $repo
+ be list
+ popd
+ done | sort | uniq
+Then to show bug status you would have something like
+ projectX$ for repo in *
+ do
+ echo $repo
+ pushd $repo
+ be show ${BUGID}
+ popd
+ done
+For a web frontend, you'd want to translate that to python/libbe.
+
+--
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/values
new file mode 100644
index 0000000..dbdb347
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/values
@@ -0,0 +1,14 @@
+Alt-id: <20090711152507.GA18461@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 11 Jul 2009 11:25:07 -0400
+
+
+In-reply-to: e520239c-8d69-4ff6-b1bd-0c2f74366200
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/body
new file mode 100644
index 0000000..cc3cff3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/body
@@ -0,0 +1,37 @@
+On Sun, Jul 12, 2009 at 12:57:35AM +1000, Ben Finney wrote:
+> Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> writes:
+>
+> > i want to see the combination of the bug data of all branches
+>
+> What is your definition of ???all branches???? When I'm working with
+> distributed VCS, I create branches wherever I feel like, and the VCS
+> tool doesn't have some central registry of branches to keep up to date.
+>
+> How is a tool to determine the set of ???all branches???? The distributed
+> VCS model means that set is indeterminate.
+
+In the first main Ronny spoke about "public" branches. To me it means that
+if a branch is public, he should like to have a status of that branch.
+
+We all agree (probably ;-) ) that tha main branch is the "right" branch, but
+as I see it, Ronny's question has some logic.
+I'd like to know that a certain bug is fixed in a certain branch, also if it
+is still not merged in the main branch, for various reason (ie I am interested
+in the solution since the bug stop my work)
+
+Imagine it like a rss feed aggregator: in one place there are all the bugs of
+all the branches that the developers make avaible to the public with
+a repository. This can make easier the life to who want to try a something
+since he know what branch he must check out, instead of checking all the
+branch he can find to test if he get what is looking for.
+
+Unluckyly I have no idea how to solve it. :-(
+
+bye
+Gianluca
+
+
+_______________________________________________
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/values
new file mode 100644
index 0000000..28fe7dc
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f/values
@@ -0,0 +1,14 @@
+Alt-id: <20090713085859.GA21800@grys.it>
+
+
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 13 Jul 2009 10:58:59 +0200
+
+
+In-reply-to: e520239c-8d69-4ff6-b1bd-0c2f74366200
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/body
new file mode 100644
index 0000000..93f082b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/body
@@ -0,0 +1,33 @@
+On Sat, 2009-07-11 at 23:34 +1000, Ben Finney wrote:
+> Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> writes:
+>
+> > the basic idea is to take a look at all public branches (for exaple
+> > all on lp/bitbucket/github) in order to tell the user of a
+> > webinterface that bug foo is fixed in branch xyz, and if its merged to
+> > the main branch
+>
+> I don't understand. The state of the bug in the main branch is right
+> there in the main branch; if it's not fixed there, it's not fixed there.
+> If it's merged in from a different branch, the bug state follows all the
+> other changes when they come in.
+>
+> Can you give an example of what would be done differently?
+>
+i want to see the combination of the bug data of all branches
+
+for example
+
+i got bug
+its fixed in the branch "something"
+its not fixed/merged to "main"
+
+now something like a website should tell me, this bug has been fixed in
+branch xyz and the fix is not yet merged into main
+
+
+
+
+_______________________________________________
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/values
new file mode 100644
index 0000000..a79837f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633/values
@@ -0,0 +1,14 @@
+Alt-id: <1247320857.7701.67.camel@localhost>
+
+
+Author: Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 11 Jul 2009 16:00:57 +0200
+
+
+In-reply-to: 0f60a148-7024-44bd-bbed-377cbece9d1b
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/body
new file mode 100644
index 0000000..3b417be
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/body
@@ -0,0 +1,27 @@
+On Sat, 2009-07-11 at 08:50 -0400, W. Trevor King wrote:
+> On Sat, Jul 11, 2009 at 01:54:54PM +0200, Ronny Pfannschmidt wrote:
+> > 1. is there any way to aggregate over multiple public branches in order
+> > to get the complete bug state
+>
+> Keeping the bug data with the source helps synchronize bug state and
+> source code. Bug state in branch A may not apply to branch B. Some
+> people like to weaken this source-bug linkage by keeping their bugs in
+> a branch all by themselves (ditz [http://ditz.rubyforge.org/]
+> currently supports this workflow). It sounds like you want to move
+> from "bugs with code" to "bugs and code in separate branches". We
+> don't have an easy way to do that in BE at the moment, since
+> version-control systems like Git have a single working branch at a
+> time (I think :p). What VCS are you using as a backend?
+the basic idea is to take a look at all public branches (for exaple all
+on lp/bitbucket/github) in order to tell the user of a webinterface that
+bug foo is fixed in branch xyz, and if its merged to the main branch
+>
+> > 2. is there any model for storing bigger files at a central place (for
+> > some of my bugs i have multi-megabyte tarballs attached)
+>
+> be comment ID "See the tarball at http://yourpage/something.tar.gz"
+> Then to grab the tarball, you'd use:
+> wget `be show COMMENT-ID | sed -n 's/ *See the tarball at //p'`
+> to grab it.
+so the basic idea is to do it completely self-managed and have have
+heterogenous sources of extended data?
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/values
new file mode 100644
index 0000000..00fe043
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d/values
@@ -0,0 +1,14 @@
+Alt-id: <1247317985.7701.63.camel@localhost>
+
+
+Author: Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 11 Jul 2009 15:13:05 +0200
+
+
+In-reply-to: 13012b22-2d02-444c-87c0-8cf0f17137ae
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/body
new file mode 100644
index 0000000..0263fbb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/body
@@ -0,0 +1,22 @@
+Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> writes:
+
+> i want to see the combination of the bug data of all branches
+
+What is your definition of “all branches”? When I'm working with
+distributed VCS, I create branches wherever I feel like, and the VCS
+tool doesn't have some central registry of branches to keep up to date.
+
+How is a tool to determine the set of “all branches”? The distributed
+VCS model means that set is indeterminate.
+
+--
+ \ “Pinky, are you pondering what I'm pondering?” “I think so, |
+ `\ Brain, but I find scratching just makes it worse.” —_Pinky and |
+_o__) The Brain_ |
+Ben Finney
+
+
+_______________________________________________
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/values
new file mode 100644
index 0000000..2adef07
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200/values
@@ -0,0 +1,14 @@
+Alt-id: <87zlbbl128.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 12 Jul 2009 00:57:35 +1000
+
+
+In-reply-to: 88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/body
new file mode 100644
index 0000000..9fb10bc
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/body
@@ -0,0 +1,26 @@
+On Sat, Jul 11, 2009 at 11:31:34PM +1000, Ben Finney wrote:
+> Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> writes:
+>
+> > 1. is there any way to aggregate over multiple public branches in
+> > order to get the complete bug state
+>
+> The bug state is as complete as the source code state. It's exactly as
+> aggregated as the rest of the source code; the ???complete bug state???
+> would be the integration branch where you merge all the feature branches
+> and bug-fix branches together.
+>
+> If instead you want bugs to *not* be tightly linked with the rest of the
+> source code state, it seems you don't want a distributed bug tracker
+> like Bugs Everywhere.
+
+"the complete bug state" probably means that he want to know (and in some way
+to publish it) that the bug "xyz" is fixed and merged in main while bug "abc"
+is fixed but only in branch "123" and bug "def" is still open in branch "456"
+
+bye
+Gianluca
+
+_______________________________________________
+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/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/values
new file mode 100644
index 0000000..fc2560e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72/values
@@ -0,0 +1,14 @@
+Alt-id: <20090713090341.GB21800@grys.it>
+
+
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 13 Jul 2009 11:03:41 +0200
+
+
+In-reply-to: 1f9f60de-ba37-42bc-a1c0-dc062ef255e1
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/values
new file mode 100644
index 0000000..2a3c4f3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/values
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de>
+
+
+severity: wishlist
+
+
+status: unconfirmed
+
+
+summary: Bug aggregation. Multi-repo meta-BE?
+
+
+time: Tue, 21 Jul 2009 18:32:12 +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
index 0000000..c49c15c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/comments/85770405-0ead-4044-a3cf-082615ff1b6f/body
@@ -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
index 0000000..d29604d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/comments/85770405-0ead-4044-a3cf-082615ff1b6f/values
@@ -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
index 0000000..6d46f8f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16989098-aa1d-4a08-bff9-80446b4a82c5/values
@@ -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/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body
new file mode 100644
index 0000000..6f00ded
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body
@@ -0,0 +1,5 @@
+Aaron said this was closeable in Nov. 24th email to the BE list.
+
+I think "priorities" == "bug severities", in which case this
+functionality is now available with the per-tree severity
+configuration.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values
new file mode 100644
index 0000000..a346a7c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:16:11 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values
new file mode 100644
index 0000000..400c6de
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Arbitrary numerical priorities?
+
+
+time: Wed, 04 Jan 2006 21:09:30 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/body
new file mode 100644
index 0000000..33638ab
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/body
@@ -0,0 +1,5 @@
+It's tricky to say whether we should have dependencies or reverse dependencies
+or both.
+
+In the case where a bug is removed, normal dependencies mean that its
+dependencies are erased from this system.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/values
new file mode 100644
index 0000000..b82fbcb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Mon, 17 Apr 2006 20:59:15 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body
new file mode 100644
index 0000000..4ac7a33
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body
@@ -0,0 +1,3 @@
+This could be implemented with an external frontend storing the
+dependency data in arbitrary tags.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values
new file mode 100644
index 0000000..42836a5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 21:29:13 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/body
new file mode 100644
index 0000000..b68090a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/body
@@ -0,0 +1 @@
+Merged into bug 7ec2c071-9630-42b0-b08a-9854616f9144 \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/values
new file mode 100644
index 0000000..ac20cae
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 21:29:33 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/values
new file mode 100644
index 0000000..2afafb8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Indicate bug dependencies
+
+
+time: Wed, 04 Jan 2006 21:05:37 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/body
new file mode 100644
index 0000000..8fd0355
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/body
@@ -0,0 +1,2 @@
+Target takes no options, so it does no parsing. Bad. We should probably
+use a framework more like bzr's. In any case, EVERY command should accept -h.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values
new file mode 100644
index 0000000..01296d8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Sat, 01 Apr 2006 18:32:47 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body
new file mode 100644
index 0000000..d09a4be
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body
@@ -0,0 +1 @@
+This seems to be taken care of.
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
index 0000000..7beb827
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values
@@ -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/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values
new file mode 100644
index 0000000..25dbaca
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: target (and others?) aren't parsed properly
+
+
+time: Sat, 01 Apr 2006 18:29:33 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/body
new file mode 100644
index 0000000..552b2ea
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/body
@@ -0,0 +1,56 @@
+On Mon, Jul 20, 2009 at 05:03:18PM -0400, Chris Ball wrote:
+> Hi Gianluca,
+>
+> > In any case, having the possibility to set a due date does not
+> > means that it is obligatory to do it and should be a good idea to
+> > offer as many possibilities as we can to the users of BE
+>
+> Okay, sounds reasonable. Would you like to write a patch for
+> associating due dates and open/closed with a target?
+
+I've been mulling this over, and I think that targets are a lot like
+bugs. Here's a list of issue/implementation pairs:
+
+ * Targeting normal bugs
+
+ With "be depend". I think we should remove the "target" field from
+ bugs, and move target dependencies over into the "be depend"
+ framework. Of course, we could add "blocks" (in addition to the
+ current "blocked-by") tags to make target lookup more efficient.
+
+ * "due_by"
+
+ We could add "due-by" to Bug.extra_strings as well, so that anyone
+ could set due dates for any issue they wanted.
+
+ * Bugdir-wide target
+
+ Just a pointer to the current target bug.
+
+ * Target dependency tree / time-series.
+
+ Use BLOCKS/BLOCKED-BY tags between targets, so you'd know which ones
+ came first.
+
+ * be target list
+
+ Would become "be list --severity target". A target "severity" would
+ keep target bugs distinct from other bug/issue types.
+
+ * Commenting on targets
+
+ They'd be Bug()s, so commenting already build in, e.g. to add
+ release notes, layout roadmaps, etc.
+
+If you want, we could maintain the current "be target" interface,
+and just use all this stuff behind the scenes.
+
+Thoughts?
+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
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/values
new file mode 100644
index 0000000..c93321b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/values
@@ -0,0 +1,11 @@
+Alt-id: <20090801102742.GA29000@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 1 Aug 2009 06:27:42 -0400
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/body
new file mode 100644
index 0000000..c799630
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/body
@@ -0,0 +1,47 @@
+On Sunday 19 July 2009 00:00:46 Chris Ball wrote:
+> Hi,
+>
+> > For example, let's assume we have target a, b, c There is a way
+> > to know that "a" is a past target, "b" is the current target and
+> > "c" is a future target ?
+>
+> We could add a "date due" field for each target.
+
+Good idea
+
+> > More: there is a way to know if a target is closed or open ?
+>
+> We could add a "target close" operation that moves all open bugs
+> assigned to one target to the next date-due target.
+
+Nice. But instead of moving all bugs to the next date-due target, I'd prefer
+to leave the choice to the user
+
+
+> I see problems with these ideas in general, because we're assuming
+> agreement by all parties/branches on when a target's date due is.
+> Maybe it's okay to demand that social conventions be used to handle
+> such a disagreement, or maybe not.
+
+I don't see these as problems per se. We can have two cases:
+
+1) a personal branch (like my html output or Trevor's email interface). In
+this case there is not any problem to decide the due date
+
+2) a branch with a group of delopers (let it be the canonical branch o an
+experimental branch): in this case I suppose that working together means to be
+able to agree on some things
+
+In any case, having the possibility to set a due date does not means that it
+is obligatory to do it and should be a good idea to offer as many possibilities
+as we can to the users of BE
+
+bye
+Gianluca
+
+
+
+_______________________________________________
+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/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/values
new file mode 100644
index 0000000..5b9011f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a/values
@@ -0,0 +1,14 @@
+Alt-id: <200907202259.11774.gian@grys.it>
+
+
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 20 Jul 2009 22:59:11 +0200
+
+
+In-reply-to: 6555a651-5a7f-4a8a-9793-47ad1315e9e8
+
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
index 0000000..08595d1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/body
@@ -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
index 0000000..3925aa2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/values
@@ -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
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/body
new file mode 100644
index 0000000..ef09dc0
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/body
@@ -0,0 +1,26 @@
+Hi,
+
+ > For example, let's assume we have target a, b, c There is a way
+ > to know that "a" is a past target, "b" is the current target and
+ > "c" is a future target ?
+
+We could add a "date due" field for each target.
+
+ > More: there is a way to know if a target is closed or open ?
+
+We could add a "target close" operation that moves all open bugs
+assigned to one target to the next date-due target.
+
+I see problems with these ideas in general, because we're assuming
+agreement by all parties/branches on when a target's date due is.
+Maybe it's okay to demand that social conventions be used to handle
+such a disagreement, or maybe not.
+
+- Chris.
+--
+Chris Ball <cjb@laptop.org>
+
+_______________________________________________
+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/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/values
new file mode 100644
index 0000000..5de3e0c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8/values
@@ -0,0 +1,14 @@
+Alt-id: <m3skgt648h.fsf@pullcord.laptop.org>
+
+
+Author: Chris Ball <cjb@laptop.org>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 18 Jul 2009 18:00:46 -0400
+
+
+In-reply-to: b9865d8b-46ae-4169-bc83-d75a98164729
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/body
new file mode 100644
index 0000000..874d906
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/body
@@ -0,0 +1,20 @@
+On Monday 20 July 2009 23:03:18 Chris Ball wrote:
+> Hi Gianluca,
+>
+> > In any case, having the possibility to set a due date does not
+> > means that it is obligatory to do it and should be a good idea to
+> > offer as many possibilities as we can to the users of BE
+>
+> Okay, sounds reasonable. Would you like to write a patch for
+> associating due dates and open/closed with a target?
+
+Ok. As soon as I finish a basic implementation of the html export, I will be
+glad to try to write a patch.
+
+bye
+Gianluca
+
+_______________________________________________
+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/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/values
new file mode 100644
index 0000000..0d96f8e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885/values
@@ -0,0 +1,14 @@
+Alt-id: <200907202340.39963.gian@grys.it>
+
+
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 20 Jul 2009 23:40:39 +0200
+
+
+In-reply-to: 777182da-a216-45c7-bf4d-42c84e511c66
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/body
new file mode 100644
index 0000000..13505c1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/body
@@ -0,0 +1,19 @@
+Hi Gianluca,
+
+ > In any case, having the possibility to set a due date does not
+ > means that it is obligatory to do it and should be a good idea to
+ > offer as many possibilities as we can to the users of BE
+
+Okay, sounds reasonable. Would you like to write a patch for
+associating due dates and open/closed with a target?
+
+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
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/values
new file mode 100644
index 0000000..bf069fc
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66/values
@@ -0,0 +1,14 @@
+Alt-id: <m3hbx72hk9.fsf@pullcord.laptop.org>
+
+
+Author: Chris Ball <cjb@laptop.org>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 20 Jul 2009 17:03:18 -0400
+
+
+In-reply-to: 4952e1c7-e035-42f1-882b-6b5264481d0a
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/body
new file mode 100644
index 0000000..a916904
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/body
@@ -0,0 +1,37 @@
+On Sat, Jul 18, 2009 at 06:00:46PM -0400, Chris Ball wrote:
+> > For example, let's assume we have target a, b, c There is a way
+> > to know that "a" is a past target, "b" is the current target and
+> > "c" is a future target ?
+>
+> We could add a "date due" field for each target.
+
+Another option would be a "blocked by" field, since you might miss
+deadlines, or have parallel targeted branches. Or just pick target
+names following some scheme so the alphanumeric-sort is also a
+dependency-order sort ;).
+
+> > More: there is a way to know if a target is closed or open ?
+
+There's also
+ $ be list --target 0.1
+If there are active bugs, the target is open. Otherwise, you must have
+made it ;).
+
+> We could add a "target close" operation that moves all open bugs
+> assigned to one target to the next date-due target.
+
+for bug in `be list --target 0.1 --uuids`; do
+ be target $bug $NEXT_TARGET
+done
+
+To avoid the loop, we could change status, severity, target, etc from
+ be COMMAND BUG ARG
+to
+ be COMMAND ARG BUG [MORE BUGS ...]
+
+--
+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/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/values
new file mode 100644
index 0000000..0f4ff0a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/values
@@ -0,0 +1,14 @@
+Alt-id: <20090718222701.GA304@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 18 Jul 2009 18:27:01 -0400
+
+
+In-reply-to: 6555a651-5a7f-4a8a-9793-47ad1315e9e8
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/body
new file mode 100644
index 0000000..7382bae
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/body
@@ -0,0 +1,20 @@
+Hello
+
+Just a question and only for curiosity: there is an easy way to determine the
+target succession ?
+
+For example, let's assume we have target a, b, c
+There is a way to know that "a" is a past target, "b" is the current target
+and "c" is a future target ? More: there is a way to know if a target is
+closed or open ?
+
+thanks
+
+bye
+Gianluca
+
+
+_______________________________________________
+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/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/values
new file mode 100644
index 0000000..6f56640
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729/values
@@ -0,0 +1,11 @@
+Alt-id: <200907182351.03217.gian@grys.it>
+
+
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 18 Jul 2009 23:51:03 +0200
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values
new file mode 100644
index 0000000..64928e8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values
@@ -0,0 +1,24 @@
+assigned: W. Trevor King <wking@drexel.edu>
+
+
+creator: W. Trevor King <wking@drexel.edu>
+
+
+extra_strings:
+- BLOCKED-BY:51930348-9ccc-4165-af41-6c7450de050e
+
+
+reporter: Gianluca Montecchi <gian@grys.it>
+
+
+severity: wishlist
+
+
+status: fixed
+
+
+summary: Sorting targets chronologically
+
+
+time: Tue, 21 Jul 2009 18:34:25 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/27bb8bc2-05c2-417a-9d09-928471380d7a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/27bb8bc2-05c2-417a-9d09-928471380d7a/values
new file mode 100644
index 0000000..7082a38
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/27bb8bc2-05c2-417a-9d09-928471380d7a/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Are dates still a problem?
+
+
+time: Tue, 20 Dec 2005 19:37:03 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/body
new file mode 100644
index 0000000..e936bd4
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/body
@@ -0,0 +1,3 @@
+We should support:
+WONTFIX
+EMPTY \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values
new file mode 100644
index 0000000..1402892
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Wed, 04 Jan 2006 21:03:54 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/values
new file mode 100644
index 0000000..df80422
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: More types of closed bugs
+
+
+time: Wed, 04 Jan 2006 21:03:27 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body
new file mode 100644
index 0000000..dd40bfa
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body
@@ -0,0 +1 @@
+Aaron said this was closeable in Nov. 24th email to the BE list.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values
new file mode 100644
index 0000000..4c495f7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:21:08 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values
new file mode 100644
index 0000000..1d358cd
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: implement message-change log
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2b81b428-fc43-4970-9469-b442385b9c0d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2b81b428-fc43-4970-9469-b442385b9c0d/values
new file mode 100644
index 0000000..c2861d0
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2b81b428-fc43-4970-9469-b442385b9c0d/values
@@ -0,0 +1,17 @@
+creator: gianluca <gian@galactica>
+
+
+reporter: gianluca <gian@galactica>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Use the get_parser in becommands/html.py
+
+
+time: Wed, 08 Jul 2009 21:27:37 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/body
new file mode 100644
index 0000000..5ce4f1c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/body
@@ -0,0 +1,23 @@
+"W. Trevor King" <wking@drexel.edu> writes:
+
+> On Sat, Jul 04, 2009 at 10:19:35AM +1000, Ben Finney wrote:
+> > Instead of a separate command for each output format, could we have
+> > a single "produce a static report of the bug database" command, and
+> > specify output format as an option?
+> > […]
+>
+> Do people like this architecture better than my be-xml-to-mbox
+> approach?
+
+I think this question is illuminated by the related question: Is mbox
+output a static report, or another read-write data store?
+
+It can technically be both, of course, which is why the question may be
+helpful: it may help show what is the *conceptual* purpose of the mbox
+output format for Bugs Everywhere.
+
+--
+ \ “Time is the great legalizer, even in the field of morals.” |
+ `\ —Henry L. Mencken |
+_o__) |
+Ben Finney
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/values
new file mode 100644
index 0000000..eda49f5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67/values
@@ -0,0 +1,14 @@
+Alt-id: <87hbxqrckv.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 06 Jul 2009 08:26:24 +1000
+
+
+In-reply-to: cb5689f4-7c36-4c44-b380-ca9e06e80bae
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/body
new file mode 100644
index 0000000..dbf3b1b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/body
@@ -0,0 +1,47 @@
+Gianluca Montecchi <gian@grys.it> writes:
+
+> 1) is it ok to develop this command ? I know that this is not a fully
+> featured web interface, but I am sure that it can be usefull.
+
+Yes, definitely. I can see it being a very easy way to put one's bug
+database online for browsing.
+
+> I am open to suggestion about it of course.
+
+Instead of a separate command for each output format, could we have a
+single “produce a static report of the bug database” command, and
+specify output format as an option?
+
+How about:
+
+ be report
+ be report --format ascii
+ be report --format rst
+ be report --format html
+
+Where the ‘--format’ option has a default of, e.g., “ascii”.
+
+This would mean that you are implementing the ‘html’ format of this
+putative command.
+
+> 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 ?
+
+This sounds quite sensible to me. The existence of a command implies a
+module of the same name in ‘becommand’, but there's no necessary
+implication that that module can't import modules from elsewhere to do
+its work.
+
+--
+ \ “It ain't so much the things we don't know that get us in |
+ `\ trouble. It's the things we know that ain't so.” —Artemus Ward |
+_o__) (1834–1867), U.S. journalist |
+Ben Finney
+
+
+_______________________________________________
+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/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/values
new file mode 100644
index 0000000..642697d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863/values
@@ -0,0 +1,14 @@
+Alt-id: <87y6r5qoyw.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 04 Jul 2009 10:19:35 +1000
+
+
+In-reply-to: cb5689f4-7c36-4c44-b380-ca9e06e80bae
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/body
new file mode 100644
index 0000000..4276b9c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/body
@@ -0,0 +1,25 @@
+Gianluca Montecchi <gian@grys.it> writes:
+
+> On Monday 06 July 2009 12:48:39 W. Trevor King wrote:
+> > Gianluca is clearly thinking about a static report [for a collection
+> > of HTML files as output]:
+>
+> You are right, static, but not exactly a report as I think Ben is
+> thinking
+
+I think it exactly is a report: multiple, static, browseable pages
+reporting the state of the database at a point in time.
+
+What makes you think that term doesn't apply?
+
+--
+ \ “The problem with television is that the people must sit and |
+ `\ keep their eyes glued on a screen: the average American family |
+_o__) hasn't time for it.” —_The New York Times_, 1939 |
+Ben Finney
+
+
+_______________________________________________
+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/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/values
new file mode 100644
index 0000000..d8ccad9
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937/values
@@ -0,0 +1,14 @@
+Alt-id: <87skh9p8ax.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 07 Jul 2009 11:53:58 +1000
+
+
+In-reply-to: cb5689f4-7c36-4c44-b380-ca9e06e80bae
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/body
new file mode 100644
index 0000000..8451bd7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/body
@@ -0,0 +1,50 @@
+On Mon, Jul 06, 2009 at 08:26:24AM +1000, Ben Finney wrote:
+> "W. Trevor King" <wking@drexel.edu> writes:
+>
+> > On Sat, Jul 04, 2009 at 10:19:35AM +1000, Ben Finney wrote:
+> > > Instead of a separate command for each output format, could we have
+> > > a single "produce a static report of the bug database" command, and
+> > > specify output format as an option?
+> >
+> > Do people like this architecture better than my be-xml-to-mbox
+> > approach?
+>
+> I think this question is illuminated by the related question: Is mbox
+> output a static report, or another read-write data store?
+
+Gianluca is clearly thinking about a static report:
+
+On Fri, Jul 03, 2009 at 10:50:17PM +0200, Gianluca Montecchi wrote:
+> 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
+
+I think truly interactive frontends like Steve's working on need to be
+build on top of libbe directly, since they'll need to make lots of
+small changes to the database, and it's to slow to be reloading the
+database for every change. Static dumps like my mbox or Gianluca's
+html could just parse the xml output of `be list' and other be
+commands.
+
+There should also be an xml import for `be new' and `be comment' so
+you could import new bugs/comments from whatever format after writing
+a whatever->xml converter. This would allow you to email new bugs and
+comments to the database (e.g. via some procmail-spawned
+be-parse-email script) which would give you some level of
+interactivity, but you'd have to regenerate your mbox to see your new
+comments in your mail reader.
+
+I think interactive use that gives you live-updates in your mail
+reader isn't worth the trouble, since you'd need to teach BE imap or
+smtp+mbox-locking. Hmm, maybe it smtp+mbox-locking wouldn't be so bad,
+but that would be a distinct frontend project like Steve's, not part
+of the becommands.
+
+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
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/values
new file mode 100644
index 0000000..8b10a06
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/values
@@ -0,0 +1,14 @@
+Alt-id: <20090706104839.GA19537@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 6 Jul 2009 06:48:39 -0400
+
+
+In-reply-to: 074ef29a-3f1d-46dc-8561-7a56af7e6d67
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/body
new file mode 100644
index 0000000..3b53533
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/body
@@ -0,0 +1,23 @@
+On Sat, Jul 04, 2009 at 10:19:35AM +1000, Ben Finney wrote:
+> Instead of a separate command for each output format, could we have a
+> single "produce a static report of the bug database" command, and
+> specify output format as an option?
+>
+> How about:
+>
+> be report
+> be report --format ascii
+> be report --format rst
+> be report --format html
+
+Do people like this architecture better than my be-xml-to-mbox
+approach? I think the tradeoff is easy output format implementation
+vs cluttered core codebase. Should we use both, depending on how
+useful people think the output format will be?
+
+--
+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/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/values
new file mode 100644
index 0000000..a01e2cd
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/values
@@ -0,0 +1,14 @@
+Alt-id: <20090705143108.GB10709@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 5 Jul 2009 10:31:08 -0400
+
+
+In-reply-to: 1dba8196-654b-4ca0-9a95-fb334af81863
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/body
new file mode 100644
index 0000000..9bf3851
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/body
@@ -0,0 +1,123 @@
+On Fri, Jul 03, 2009 at 10:50:17PM +0200, Gianluca Montecchi wrote:
+>
+> Hello to everyone
+>
+> 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.
+>
+> So I'd like to ask some question:
+> 1) is it ok to develop this command ? I know that this is not a fully featured
+> web interface, but I am sure that it can be usefull.
+>
+> I am open to suggestion about it of course.
+>
+> 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'd like to hear other opinion about this.
+>
+> Thanks for now
+> bye
+> Gianluca
+>
+>
+> _______________________________________________
+> Be-devel mailing list
+> Be-devel@bugseverywhere.org
+> http://void.printf.net/cgi-bin/mailman/listinfo/be-devel
+
+On Mon, Jul 06, 2009 at 10:18:33PM +0200, Gianluca Montecchi wrote:
+> This sound like an interesting idea, but what i'd like to do is not, strictly
+> speaking, a report. It is a full tree of html pages that are browseable, both
+> on line and offline
+
+I'm not sure what distinction you're making about "report". You're
+just producing a static snapshot of the current database status,
+right? The number of pages and completeness of coverage are nice, but
+it's still a static entity generated from a particular snapshot, which
+is what I mean by "report" ;).
+
+> > > 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 ?
+> >
+> > This sounds quite sensible to me. The existence of a command implies a
+> > module of the same name in ‘becommand’, but there's no necessary
+> > implication that that module can't import modules from elsewhere to do
+> > its work.
+>
+> The "elsewhere" for now is the same directory, just another module
+>
+
+On Mon, Jul 06, 2009 at 10:38:56PM +0200, Gianluca Montecchi wrote:
+> > On Fri, Jul 03, 2009 at 10:50:17PM +0200, Gianluca Montecchi wrote:
+> > > 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
+> >
+> > I think truly interactive frontends like Steve's working on need to be
+> > build on top of libbe directly, since they'll need to make lots of
+> > small changes to the database, and it's to slow to be reloading the
+> > database for every change. Static dumps like my mbox or Gianluca's
+> > html could just parse the xml output of `be list' and other be
+> > commands.
+>
+> Ok, but if I want to have an html dump that is browseable, I need to parse the
+> xml. Am I correct ?
+> If yes, should not be easiear to use directly the libbe ?
+
+Using libbe directly is easier, but also more tightly tied to the be
+internals which could weigh down future refactoring. Partly I'm
+afraid of our 2.5 different html-output mechanisms. Either their
+should be a single Right Way that tries to satisfy everyone, or a
+smorgasbord of loosely coupled translators, so it's not so painful to
+kill them if/when they go out of style :p.
+
+On Mon, Jul 06, 2009 at 10:46:54PM +0200, Gianluca Montecchi wrote:
+> On Saturday 04 July 2009 02:31:26 Chris Ball wrote:
+> > 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?
+>
+> It can be do, but this implies that CherryPy must be installed and configured,
+> a thing that I don't want to impose. My idea is to offer a simpler way to have
+> some html pages, where you just need to have BE installed.
+
+I agree that not needing CherryPy for a static html dump is good.
+Also, read-only templates will look different from the CherryPy
+interactive templates. +1 for another quasi-redundant template set
+;).
+
+> > > 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.
+>
+> I am in doubt with the "somewhere else", since for now I put the html template
+> into a separate file in the same directory. Suggestion ?
+
+I think that only code intended only for command line use only should
+go into becommands, but really, just dump it anywhere and we can shift
+it around later :p.
+
+--
+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/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/values
new file mode 100644
index 0000000..07da71c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/values
@@ -0,0 +1,14 @@
+Alt-id: <20090707013454.GA3721@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 6 Jul 2009 21:34:54 -0400
+
+
+In-reply-to: da97e18f-33d6-469e-9d93-6457b9a6bfca
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/body
new file mode 100644
index 0000000..2301eba
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/body
@@ -0,0 +1,47 @@
+On Saturday 04 July 2009 02:19:35 Ben Finney wrote:
+> Gianluca Montecchi <gian@grys.it> writes:
+
+>
+> > I am open to suggestion about it of course.
+>
+> Instead of a separate command for each output format, could we have a
+> single “produce a static report of the bug database” command, and
+> specify output format as an option?
+>
+> How about:
+>
+> be report
+> be report --format ascii
+> be report --format rst
+> be report --format html
+>
+> Where the ‘--format’ option has a default of, e.g., “ascii”.
+>
+> This would mean that you are implementing the ‘html’ format of this
+> putative command.
+>
+
+This sound like an interesting idea, but what i'd like to do is not, strictly
+speaking, a report. It is a full tree of html pages that are browseable, both
+on line and offline
+
+> > 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 ?
+>
+> This sounds quite sensible to me. The existence of a command implies a
+> module of the same name in ‘becommand’, but there's no necessary
+> implication that that module can't import modules from elsewhere to do
+> its work.
+
+The "elsewhere" for now is the same directory, just another module
+
+bye
+Gianluca
+
+
+_______________________________________________
+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/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/values
new file mode 100644
index 0000000..17513d6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8/values
@@ -0,0 +1,14 @@
+Alt-id: <200907062218.33895.gian@grys.it>
+
+
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 06 Jul 2009 22:18:33 +0200
+
+
+In-reply-to: 1dba8196-654b-4ca0-9a95-fb334af81863
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/body
new file mode 100644
index 0000000..50a30e8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/body
@@ -0,0 +1,35 @@
+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
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/values
new file mode 100644
index 0000000..ee8e589
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58/values
@@ -0,0 +1,14 @@
+Alt-id: <m3iqi9thk1.fsf@pullcord.laptop.org>
+
+
+Author: Chris Ball <cjb@laptop.org>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 03 Jul 2009 20:31:26 -0400
+
+
+In-reply-to: cb5689f4-7c36-4c44-b380-ca9e06e80bae
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/body
new file mode 100644
index 0000000..8991cfb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/body
@@ -0,0 +1,93 @@
+> On Mon, Jul 06, 2009 at 10:18:33PM +0200, Gianluca Montecchi wrote:
+>> This sound like an interesting idea, but what i'd like to do is not,
+>> strictly
+>> speaking, a report. It is a full tree of html pages that are browseable,
+>> both
+>> on line and offline
+>
+> I'm not sure what distinction you're making about "report". You're
+> just producing a static snapshot of the current database status,
+> right? The number of pages and completeness of coverage are nice, but
+> it's still a static entity generated from a particular snapshot, which
+> is what I mean by "report" ;).
+
+Mmm, my bad here.
+I normally speak about "report" as something that is not browseable, like
+the output of a report generator (reportlab or whatever), but I admit that
+basically also the html output I am working on is a report.
+
+
+> On Mon, Jul 06, 2009 at 10:38:56PM +0200, Gianluca Montecchi wrote:
+>>
+>> Ok, but if I want to have an html dump that is browseable, I need to
+>> parse the
+>> xml. Am I correct ?
+>> If yes, should not be easiear to use directly the libbe ?
+>
+> Using libbe directly is easier, but also more tightly tied to the be
+> internals which could weigh down future refactoring. Partly I'm
+> afraid of our 2.5 different html-output mechanisms. Either their
+> should be a single Right Way that tries to satisfy everyone, or a
+> smorgasbord of loosely coupled translators, so it's not so painful to
+> kill them if/when they go out of style :p.
+
+I know that using libbe I am more tightly tied to the internals, but
+I am trying to keep the command code and the presentation code crearly
+separated to minimize this problem. I am not sure this is a real problem
+anyway.
+
+
+> On Mon, Jul 06, 2009 at 10:46:54PM +0200, Gianluca Montecchi wrote:
+>> On Saturday 04 July 2009 02:31:26 Chris Ball wrote:
+>> > 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?
+>>
+>> It can be do, but this implies that CherryPy must be installed and
+>> configured,
+>> a thing that I don't want to impose. My idea is to offer a simpler way
+>> to have
+>> some html pages, where you just need to have BE installed.
+>
+> I agree that not needing CherryPy for a static html dump is good.
+> Also, read-only templates will look different from the CherryPy
+> interactive templates. +1 for another quasi-redundant template set
+> ;).
+
+The look is not a problem. I can always use the same html Steve is using.
+I am also playing with the idea to have the template themeable some time
+after I have a fully working version.
+
+>
+>> > > 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.
+>>
+>> I am in doubt with the "somewhere else", since for now I put the html
+>> template
+>> into a separate file in the same directory. Suggestion ?
+>
+> I think that only code intended only for command line use only should
+> go into becommands, but really, just dump it anywhere and we can shift
+> it around later :p.
+
+Of course.
+
+bye
+Gianluca
+
+
+_______________________________________________
+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/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/values
new file mode 100644
index 0000000..fc79745
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5/values
@@ -0,0 +1,14 @@
+Alt-id: <6f719a1c43fdcba8bdbfee1130072595.squirrel@webmail.grys.it>
+
+
+Author: gian@grys.it
+
+
+Content-type: text/plain
+
+
+Date: Tue, 07 Jul 2009 14:15:08 +0200
+
+
+In-reply-to: 83202b83-eea8-452f-8239-d468940bddba
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/body
new file mode 100644
index 0000000..d8f14fc
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/body
@@ -0,0 +1,32 @@
+Hello to everyone
+
+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.
+
+So I'd like to ask some question:
+1) is it ok to develop this command ? I know that this is not a fully featured
+web interface, but I am sure that it can be usefull.
+
+I am open to suggestion about it of course.
+
+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'd like to hear other opinion about this.
+
+Thanks for now
+bye
+Gianluca
+
+
+_______________________________________________
+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/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/values
new file mode 100644
index 0000000..9ce9085
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae/values
@@ -0,0 +1,11 @@
+Alt-id: <200907032250.17327.gian@grys.it>
+
+
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 03 Jul 2009 22:50:17 +0200
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/body
new file mode 100644
index 0000000..27dca1e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/body
@@ -0,0 +1,45 @@
+On Saturday 04 July 2009 02:31:26 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?
+
+It can be do, but this implies that CherryPy must be installed and configured,
+a thing that I don't want to impose. My idea is to offer a simpler way to have
+some html pages, where you just need to have BE installed.
+
+My very first implementation was a script that parse directly the .be directory
+to build the pages, without BE itself installed.
+
+
+> > 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.
+
+I am in doubt with the "somewhere else", since for now I put the html template
+into a separate file in the same directory. Suggestion ?
+
+thanks
+bye
+Gianluca
+
+_______________________________________________
+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/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/values
new file mode 100644
index 0000000..f989b78
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca/values
@@ -0,0 +1,14 @@
+Alt-id: <200907062246.54804.gian@grys.it>
+
+
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 06 Jul 2009 22:46:54 +0200
+
+
+In-reply-to: b900f7fd-bab6-48c4-922c-a051f933da58
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/body
new file mode 100644
index 0000000..1d2b619
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/body
@@ -0,0 +1,43 @@
+On Thursday 25 June 2009 16:23:04 Steve Losh wrote:
+> On Jun 25, 2009, at 10:02 AM, Chris Ball <cjb@laptop.org> wrote:
+> >> Oh, and obviously there must still be bugs in BE. Please submit
+> >> more ;).
+> >
+> > Perhaps it's a good time to merge Steve Losh's CherryPy web interface?
+> >
+> > http://void.printf.net/pipermail/be-devel/2009-February/000095.html
+> > http://bitbucket.org/sjl/cherryflavoredbugseverywhere/
+>
+> Hey, I haven't touched the web interface in a while, but I should have
+> some time to fix some stuff up tonight and tomorrow. Hold off on
+> merging it in until then.
+>
+> I'm still curious as to what people think the role of a web interface
+> like this should be. When I wrote it I meant it as a single-user
+> interface like the command line one. It could definitely work as a
+> public, read-only interface too.
+
+I'd really like to have some sort of web interface for BE, also in read-only
+mode.
+
+I am thinking to write (actually I wrote some test code) a tool to parse a BE
+repository to output a set of static html pages to put online, like the "ditz
+html" command, but this was before I start to play with BE sourcecode, so now
+I ma thinking to implement it as a BE command.
+
+> If the goal is to allow more than one person to add issues, how should
+> commits go? One commit per change? Commit every X minutes if necessary?
+
+I think that a simple web interface should be read-only.
+
+Eventually, to allow to add issues also from the web interface, it should be
+done to a specific branch, one commit per change.
+
+just my 2 cents...
+bye
+Gianluca
+
+_______________________________________________
+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/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/values
new file mode 100644
index 0000000..931a187
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362/values
@@ -0,0 +1,11 @@
+Alt-id: <200906252203.08535.gian@grys.it>
+
+
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 25 Jun 2009 22:03:08 +0200
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/body
new file mode 100644
index 0000000..2e4f851
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/body
@@ -0,0 +1,43 @@
+On Monday 06 July 2009 12:48:39 W. Trevor King wrote:
+> On Mon, Jul 06, 2009 at 08:26:24AM +1000, Ben Finney wrote:
+> > "W. Trevor King" <wking@drexel.edu> writes:
+> > > On Sat, Jul 04, 2009 at 10:19:35AM +1000, Ben Finney wrote:
+> > > > Instead of a separate command for each output format, could we have
+> > > > a single "produce a static report of the bug database" command, and
+> > > > specify output format as an option?
+> > >
+> > > Do people like this architecture better than my be-xml-to-mbox
+> > > approach?
+> >
+> > I think this question is illuminated by the related question: Is mbox
+> > output a static report, or another read-write data store?
+>
+> Gianluca is clearly thinking about a static report:
+
+You are right, static, but not exactly a report as I think Ben is thinking
+
+>
+> On Fri, Jul 03, 2009 at 10:50:17PM +0200, Gianluca Montecchi wrote:
+> > 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
+>
+> I think truly interactive frontends like Steve's working on need to be
+> build on top of libbe directly, since they'll need to make lots of
+> small changes to the database, and it's to slow to be reloading the
+> database for every change. Static dumps like my mbox or Gianluca's
+> html could just parse the xml output of `be list' and other be
+> commands.
+
+Ok, but if I want to have an html dump that is browseable, I need to parse the
+xml. Am I correct ?
+If yes, should not be easiear to use directly the libbe ?
+
+
+bye
+Gianluca
+
+_______________________________________________
+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/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/values
new file mode 100644
index 0000000..d4458fd
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677/values
@@ -0,0 +1,14 @@
+Alt-id: <200907062238.56930.gian@grys.it>
+
+
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 06 Jul 2009 22:38:56 +0200
+
+
+In-reply-to: 55263144-9775-4b18-ab83-29d66ed91a53
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/values
new file mode 100644
index 0000000..5f2d264
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/values
@@ -0,0 +1,20 @@
+assigned: Gianluca Montecchi <gian@grys.it>
+
+
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: Gianluca Montecchi <gian@grys.it>
+
+
+severity: wishlist
+
+
+status: fixed
+
+
+summary: Static html report generation
+
+
+time: Tue, 21 Jul 2009 18:43:06 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body
new file mode 100644
index 0000000..708159c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body
@@ -0,0 +1 @@
+Implemented
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values
new file mode 100644
index 0000000..0eaf9c9
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:40:08 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values
new file mode 100644
index 0000000..8704a7e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values
@@ -0,0 +1,14 @@
+assigned: abentley
+
+
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: 'Per-tree configuration: default-assigneed?'
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/body
new file mode 100644
index 0000000..396c06a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/body
@@ -0,0 +1,51 @@
+$ python test.py
+**********************************************************************
+File "/home/wking/src/fun/be/libbe/plugin.py", line 31, in libbe.plugin.iter_plugins
+Failed example:
+ "plugin" in [n for n,m in iter_plugins("libbe")]
+Exception raised:
+ Traceback (most recent call last):
+ File "/usr/lib/python2.5/doctest.py", line 1228, in __run
+ compileflags, 1) in test.globs
+ File "<doctest libbe.plugin.iter_plugins[1]>", line 1, in <module>
+ "plugin" in [n for n,m in iter_plugins("libbe")]
+ File "/home/wking/src/fun/be/libbe/plugin.py", line 38, in iter_plugins
+ yield modfile[:-3], my_import(prefix+"."+modfile[:-3])
+ File "/home/wking/src/fun/be/libbe/plugin.py", line 21, in my_import
+ module = __import__(mod_name)
+ File "/home/wking/src/fun/be/libbe/restconvert.py", line 27, in <module>
+ from elementtree import ElementTree
+ ImportError: No module named elementtree
+**********************************************************************
+1 items had failures:
+ 1 of 2 in libbe.plugin.iter_plugins
+***Test Failed*** 1 failures.
+Traceback (most recent call last):
+ File "test.py", line 32, in <module>
+ for module in plugin.iter_plugins("libbe"):
+ File "/home/wking/src/fun/be/libbe/plugin.py", line 38, in iter_plugins
+ yield modfile[:-3], my_import(prefix+"."+modfile[:-3])
+ File "/home/wking/src/fun/be/libbe/plugin.py", line 21, in my_import
+ module = __import__(mod_name)
+ File "/home/wking/src/fun/be/libbe/restconvert.py", line 27, in <module>
+ from elementtree import ElementTree
+ImportError: No module named elementtree
+
+
+Looking into ElementTree, I found their webpage:
+http://effbot.org/zone/element-index.htm
+
+ It’s common practice to import ElementTree under an alias, both to
+ minimize typing, and to make it easier to switch between different
+ implementations:
+
+ $ python
+ >>> import elementtree.ElementTree as ET
+ >>> import cElementTree as ET
+ >>> import lxml.etree as ET
+ >>> import xml.etree.ElementTree as ET # Python 2.5
+
+Using new import style, fall back to old if that fails.
+Affected files:
+ libbe/restconvert.py
+ Bugs-Everywhere-Web/beweb/formatting.py
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
index 0000000..2a52700
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values
@@ -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/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values
new file mode 100644
index 0000000..5c9594e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values
@@ -0,0 +1,14 @@
+creator: wking
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: elementtree module moved in Python 2.5
+
+
+time: Thu, 13 Nov 2008 16:45:24 +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
index 0000000..288fc29
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46/body
@@ -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
index 0000000..691163d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46/values
@@ -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
index 0000000..b6a0435
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/e7d8343a-bd85-4359-bcda-bf0dc1e8177a/body
@@ -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
index 0000000..3ddceba
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/comments/e7d8343a-bd85-4359-bcda-bf0dc1e8177a/values
@@ -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
index 0000000..5c72e5f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3438b72c-6244-4f1d-8722-8c8d41484e35/values
@@ -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/3613e6e9-db9e-4775-8914-f31f0b4b81ac/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3613e6e9-db9e-4775-8914-f31f0b4b81ac/values
new file mode 100644
index 0000000..70ec5f5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3613e6e9-db9e-4775-8914-f31f0b4b81ac/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: auto-add files to revision control
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values
new file mode 100644
index 0000000..ec315a3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: implement severity on bug creation
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body
new file mode 100644
index 0000000..5dde31f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body
@@ -0,0 +1 @@
+Merged from bug 4f7a4c3b-31e3-4023-8c9d-e67f627a34f0 \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values
new file mode 100644
index 0000000..ab2a567
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 13:44:33 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/body
new file mode 100644
index 0000000..00d6eb7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/body
@@ -0,0 +1,4 @@
+Trees created with tla make dotfiles precious by default, so add
+"source ^\.be$" to the root .arch-inventory.
+
+Optionally, only do this if inventory --source doesn't list .be
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values
new file mode 100644
index 0000000..f692e19
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Tue, 17 May 2005 13:42:52 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body
new file mode 100644
index 0000000..f03ef32
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body
@@ -0,0 +1 @@
+Fixed with Arch._adjust_naming_conventions on a per-tree basis instead.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values
new file mode 100644
index 0000000..a0c9a34
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 13:46:32 +0000
+
+
+In-reply-to: 9e33512e-e3cb-42ec-bc99-8e77587d0d3f
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values
new file mode 100644
index 0000000..f59cf90
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Ensure .be is source in Arch
+
+
+time: Tue, 17 May 2005 13:39:02 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/body
new file mode 100644
index 0000000..53456f6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/body
@@ -0,0 +1,10 @@
+Hmm, perhaps my thinking has been too revision-centric. I'm not
+really sure what other level of granularity is appropriate though.
+Both notifications and commits should be generated on a "per-session"
+level, so maybe I'll just ignore Arch and Mercurial (for whom revising
+history is difficult, so per-session commits can be more work) for the
+time being ;).
+
+In that case, _every_ commit will be a
+ notify-since <revision-id>
+sort of change, so I'll just use libbe.diff :).
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/values
new file mode 100644
index 0000000..797a274
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Wed, 22 Jul 2009 19:07:28 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/body
new file mode 100644
index 0000000..df90918
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/body
@@ -0,0 +1,16 @@
+Perhaps something like
+ be-handle-mail --notify-since <revision-id>
+to tell subscribers about changes since the specified revision.
+
+This would duplicate mail to P in our first example above, but that's
+not too annoying, and P might _want_ to know what R had merged from Q.
+
+On the other hand it would be annoying if 10 other repos merged Q and
+ran the notification.
+
+We could make the subscription something like
+ subscribe BUG-ID HOST-LIST
+e.g.
+ subscribe 1234 bugseverywhere.org,fancy_branch.com
+ subscribe abcd *
+To allow users to whitelist hosts they want updates from.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/values
new file mode 100644
index 0000000..e19bf0b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 21 Jul 2009 19:52:25 +0000
+
+
+In-reply-to: 950ac308-f3e1-4956-885a-e79ce3025fd5
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/body
new file mode 100644
index 0000000..8842587
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/body
@@ -0,0 +1,5 @@
+"all" and "new" might be valid shortnames?
+
+Nope, UUID string representations are restricted to hex (0-9a-f) and
+"-" as per RFC 4122 section 3.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/values
new file mode 100644
index 0000000..74d7d97
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 21 Jul 2009 19:53:02 +0000
+
+
+In-reply-to: 85a2d1ac-200a-4ae7-841f-9f4e87795dbf
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/body
new file mode 100644
index 0000000..99d9cc3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/body
@@ -0,0 +1,8 @@
+Obviously via the control interface:
+ subscribe #BUG-ID
+ subscribe new
+ subscribe all
+ unsubscribe #BUG-ID
+ ...
+Implemented via .extra_strings, although we'll need
+BugDir.extra_strings for the repo-wide new/all.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/values
new file mode 100644
index 0000000..ae4672b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 21 Jul 2009 19:34:20 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/body
new file mode 100644
index 0000000..890a4b6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/body
@@ -0,0 +1,10 @@
+This creates an interesting situation:
+ Person P subscribes to bug B in repo R.
+ Repo S merges repo R.
+ Person Q comments on B in S.
+ S notifies P :).
+which is nice. However
+ Person P subscribes to bug B in repo R.
+ Person Q comments on B in repo S.
+ R merges S.
+ P never notified about Q's comment.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/values
new file mode 100644
index 0000000..d9fcf73
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 21 Jul 2009 19:34:32 +0000
+
+
+In-reply-to: 85a2d1ac-200a-4ae7-841f-9f4e87795dbf
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/body
new file mode 100644
index 0000000..3c95f19
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/body
@@ -0,0 +1,2 @@
+The intereface changed a bit as I implemented it. See "be help
+subscribe" for details.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/values
new file mode 100644
index 0000000..f42f8ad
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Wed, 22 Jul 2009 18:54:06 +0000
+
+
+In-reply-to: 85a2d1ac-200a-4ae7-841f-9f4e87795dbf
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/values
new file mode 100644
index 0000000..aa22fab
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/values
@@ -0,0 +1,20 @@
+assigned: W. Trevor King <wking@drexel.edu>
+
+
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: 'subscribe/unsubscribe (bug #..., "new bugs", "all", etc.)'
+
+
+time: Tue, 21 Jul 2009 19:27:04 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/body
new file mode 100644
index 0000000..20b3da3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/body
@@ -0,0 +1,3 @@
+Calls to Popen() while running `test.py` raised OSError because of
+missing binaries (tla was not installed). Added catches to produce
+more useful error messages in the backtrace.
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
index 0000000..5e1f3de
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values
@@ -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/40dac9af-951e-4b98-8779-9ba02c37f8a1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values
new file mode 100644
index 0000000..59d0695
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values
@@ -0,0 +1,14 @@
+creator: wking
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Popen OSErrors not caught
+
+
+time: Thu, 13 Nov 2008 15:54:45 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/body
new file mode 100644
index 0000000..e39beb0
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/body
@@ -0,0 +1,24 @@
+> Currently, the code and interface of Bugs Everywhere speaks loosely
+> about the term “RCS”. Sometimes it means “revision control system”
+> referring in general to these types of system, and sometimes it talks
+> about GNU RCS, a specific system.
+
+I don't think we ever rever to GNU RCS. Our current libbe.rcs.RCS
+default implementation is a "don't version" backend for BE, but
+perhaps this is what you're refereing to.
+
+> I propose that “Version Control System” (“VCS”) has emerged as a
+> consensus term to refer to such systems in general, with no specific
+> reference to any particular system.
+
+Fair enough.
+
+> This will change some interface (e.g. the ‘rcs_name’ configuration
+> setting, and some of the methods on objects), but making this change
+> while Bugs Everywhere is small will be much less painful than making it
+> later.
+
+Hmm, we really need a method for upgrading the on-disk BugDir version.
+It's hard when you need to maintain backwards compatibilty with
+earlier versions in the VCS history....
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/values
new file mode 100644
index 0000000..7eb5b45
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 03 Aug 2009 23:26:22 +0000
+
+
+In-reply-to: a92f97a4-e9fe-43f7-bf56-5862b03a2641
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/body
new file mode 100644
index 0000000..f9c166b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/body
@@ -0,0 +1,33 @@
+Howdy all,
+
+Currently, the code and interface of Bugs Everywhere speaks loosely
+about the term “RCS”. Sometimes it means “revision control system”
+referring in general to these types of system, and sometimes it talks
+about GNU RCS, a specific system.
+
+I propose that “Version Control System” (“VCS”) has emerged as a
+consensus term to refer to such systems in general, with no specific
+reference to any particular system.
+
+So I'd like to modify the Bugs Everywhere code to disambiguate: the term
+“VCS” will be used consistently to refer to version control systems in
+general, and “RCS” will only ever refer to GNU RCS.
+
+This will change some interface (e.g. the ‘rcs_name’ configuration
+setting, and some of the methods on objects), but making this change
+while Bugs Everywhere is small will be much less painful than making it
+later.
+
+Any objections? Any alternative suggestions?
+
+--
+ \ “I watched the Indy 500, and I was thinking that if they left |
+ `\ earlier they wouldn't have to go so fast.” —Steven Wright |
+_o__) |
+Ben Finney
+
+
+_______________________________________________
+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/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/values
new file mode 100644
index 0000000..5f3cf73
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641/values
@@ -0,0 +1,11 @@
+Alt-id: <87d49879v7.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <ben@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 13 Jun 2009 19:37:16 +1000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/values
new file mode 100644
index 0000000..d88c668
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/values
@@ -0,0 +1,21 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+extra_strings:
+- BLOCKED-BY:51930348-9ccc-4165-af41-6c7450de050e
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: 'Terminology: Version control system vs. RCS'
+
+
+time: Mon, 03 Aug 2009 23:10:02 +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
index 0000000..f008963
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values
@@ -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/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/body
new file mode 100644
index 0000000..dfcf82c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/body
@@ -0,0 +1,29 @@
+I was having problems with `python test.py bugdir` with the Arch
+backend. Commits were failing with `archive not registered'.
+
+Adding some trace information to arch.Arch._rcs_init() and
+._rcs_cleanup() (the traceback module is great :p), I found
+that the problem was coming from bugdir.BugDir.guess_rcs().
+
+The Arch backend deletes any auto-created archives when it is cleaned
+up (RCS.__del__ -> RCS.cleanup -> Arch._rcs_cleanup). This means that
+whatever instance is used to init the archive in guess_rcs() must be
+kept around. I had been doing:
+ * installed_rcs() -> Arch-instance-A
+ * Arch-instance-A.init()
+ * store Arch-instnance-A.name as bugdir.rcs_name
+ * future calls to bugdir.rcs get new instance Arch-instance-B
+ * eventually Arch-instance-A cleaned up
+ * archive dissapears & tests crash
+
+I switched things around so .rcs is the `master attribute' and
+.rcs_name follows it. Now just save whichever rcs you used to init
+your archive as .rcs.
+
+In order to implement the fix, I had to tweak the memory/file-system
+interaction a bit. Instead of saving the settings *every*time* a
+setting_property changed, we now save only if the .be file exists.
+This file serves as a 'file-system-bugdir-active' flag. Before it is
+created (e.g., by a .save()), the BugDir lives purely in memory, and
+can freely go about configuring .rcs, .rcs_name, etc until it get's
+to the point where it's ready to go to disk.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values
new file mode 100644
index 0000000..e9bbdac
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 22 Nov 2008 18:53:20 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/values
new file mode 100644
index 0000000..2ef6ea3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/values
@@ -0,0 +1,14 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Early del-cleanup with Arch backend
+
+
+time: Sat, 22 Nov 2008 18:38:32 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body
new file mode 100644
index 0000000..ab2dc28
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body
@@ -0,0 +1 @@
+Merged into bug ae998b27-a11b-4243-abf6-11841e5b8242 \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values
new file mode 100644
index 0000000..63842d1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:05:50 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body
new file mode 100644
index 0000000..d0b8404
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body
@@ -0,0 +1 @@
+Implemented.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values
new file mode 100644
index 0000000..6a4005c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 15:42:07 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values
new file mode 100644
index 0000000..37197a7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Do we need a severity between serious and minor? EG "Moderate"?
+
+
+time: Wed, 25 Jan 2006 23:14:07 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body
new file mode 100644
index 0000000..fb08206
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body
@@ -0,0 +1 @@
+Merged into bug 381555eb-f2e3-4ef0-8303-d759c00b390a \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values
new file mode 100644
index 0000000..ab2a567
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 13:44:33 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values
new file mode 100644
index 0000000..b97cb07
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Automatically set .be as source for arch
+
+
+time: Thu, 07 Apr 2005 16:07:51 +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
index 0000000..4eebcc4
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values
@@ -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/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/body
new file mode 100644
index 0000000..16e1919
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/body
@@ -0,0 +1,2 @@
+Note that VISUAL means interactive editors like vi, emacs. It compares with
+EDITOR, which originally meant line editors like ex (or edlin?).
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/values
new file mode 100644
index 0000000..3ea62d2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790/values
@@ -0,0 +1,11 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Mon, 16 Jul 2007 15:23:47 +0000
+
+
+In-reply-to: e173c09a-1b3e-4d8a-a86a-6b8c94a76247
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/body
new file mode 100644
index 0000000..9987f21
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/body
@@ -0,0 +1,2 @@
+the $VISUAL environment variable is common for setting a users preferred
+editor. It would be nice if this would be supported by be.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/values
new file mode 100644
index 0000000..3292da3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247/values
@@ -0,0 +1,8 @@
+Author: jelmer
+
+
+Content-type: text/plain
+
+
+Date: Sun, 15 Jul 2007 13:34:52 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/values
new file mode 100644
index 0000000..c471b0f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/values
@@ -0,0 +1,14 @@
+creator: jelmer
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: should check not just EDITOR but also VISUAL.
+
+
+time: Sun, 15 Jul 2007 13:33:03 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/body
new file mode 100644
index 0000000..34d37e5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/body
@@ -0,0 +1,30 @@
+Added libbe/upgrade.py to handle upgrading on-disk bugdirs.
+
+When upgrade.BUGDIR_DISK_VERSION changes, a series of Updater
+classes handle the upgrade. For example, if
+ BUGDIR_DISK_VERSIONS = ["v1", "v2", "v3"]
+and the on-disk version is "v1", you should have defined classes
+ class Upgrade_1_to_2 (Upgrader):
+ initial_version = "v1"
+ final_version = "v2"
+ def _upgrade():
+ ....
+ class Upgrade_2_to_3 (Upgrader):
+ initial_version = "v2"
+ final_version = "v3"
+ def _upgrade():
+ ....
+and added them to upgraders:
+ upgraders = [Upgrade_1_to_2, Upgrade_2_to_3]
+If the on-disk version is v2, then only Upgrade_2_to_3.upgrade() is
+run. If the on-disk version is v1, then Upgrade_1_to_2.upgrade() is
+run, followed by Upgrade_2_to_3.upgrade().
+
+You can optionally define shortcut upgrades (e.g. Upgrade_1_to_3) for
+efficiency or to avoid data loss.
+
+This upgrade occurs during BugDir.load(), which is called by
+BugDir.__init__(from_disk=True), before any processing of the on-disk
+data except for the access of .be/version to determine if an upgrade
+was necessary.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/values
new file mode 100644
index 0000000..b296bff
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 31 Aug 2009 16:29:50 +0000
+
+
+In-reply-to: f1479ecf-4154-4cd4-bbd6-0ed6275b9f98
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/body
new file mode 100644
index 0000000..372a655
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/body
@@ -0,0 +1,16 @@
+There is no obvious means of using
+".be/version"/"libbe.bugdir.TREE_VERSION_STRING". In the past I've
+worked around this by keeping all the disk-reading backwards
+compatible (e.g. homemade mapfile -> YAML, the "From" hack in
+libbe.comment.Comment.load_settings, possibly others). However, this
+is not the road to easily maintainable code.
+
+Most projects only need to maintain backwards compatibility with the
+last few versions of their disk cache, to allow users an easy upgrade
+path. The difficulties come with "be diff", which must be able to
+read _every_ disk-image of the bugdir ever committed into something
+comparible with the current cutting edge. This makes sweeping changes
+very difficult. VCSs themselves avoid this by never showing their
+disk-cache to another program, but we've shown ours to the VCS, and
+it's difficult (or impossible, depending on the VCS) to change history
+to match the current format.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/values
new file mode 100644
index 0000000..3d4d9df
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 16 Aug 2009 19:07:06 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/values
new file mode 100644
index 0000000..75a191c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/values
@@ -0,0 +1,22 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+extra_strings:
+- BLOCKS:22b6f620-d2f7-42a5-a02e-145733a4e366
+- BLOCKS:427e0ca7-17f5-4a5a-8c68-98cc111a2495
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Upgrade path for on-disk representation
+
+
+time: Sun, 16 Aug 2009 19:05:59 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/body
new file mode 100644
index 0000000..90b386a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/body
@@ -0,0 +1,20 @@
+I'm all for flexibility, so long as it doesn't require too much
+hackery to implement it. You'll have two problems:
+
+ * 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.
+
+ * Generating meaningful commit messages.
+
+ You'd have to add this functionality to each command (and future
+ commands).
+
+This would probably not be a good idea for the Arch and Mercurial
+backends, since they have a limited ability to rewrite history when
+you screw up your commit message (as far as I can tell). Mercurial
+does have "hg rollback", but it only works once, and lots of
+typo-correction commits would just make the logs awkward.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/values
new file mode 100644
index 0000000..eb90c47
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 24 Jul 2009 12:33:58 +0000
+
+
+In-reply-to: b17a561a-6100-490e-84eb-d1ae4b617940
+
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
index 0000000..3ed77e7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body
@@ -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
index 0000000..b3dba3f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values
@@ -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
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/body
new file mode 100644
index 0000000..c88a838
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/body
@@ -0,0 +1,9 @@
+...
+Also, why doesn't be commit after it takes an action? I think it's
+kinda weird that I have to commit after creating a new bug.
+...
+
+as posted in
+ http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=477125
+ on
+ Fri, 12 Jun 2009 17:03:02 +0200
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/values
new file mode 100644
index 0000000..d9d45f7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940/values
@@ -0,0 +1,8 @@
+Author: Martin F Krafft <madduck@debian.org>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 24 Jul 2009 12:09:02 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/values
new file mode 100644
index 0000000..5b332ed
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/values
@@ -0,0 +1,21 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+extra_strings:
+- BLOCKED-BY:5fb11e65-68a0-4015-b404-737238299cdc
+
+
+reporter: Martin F Krafft <madduck@debian.org>
+
+
+severity: wishlist
+
+
+status: open
+
+
+summary: Allow autocommit option for command line interface?
+
+
+time: Fri, 24 Jul 2009 12:04:08 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/body
new file mode 100644
index 0000000..fa9e963
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/body
@@ -0,0 +1,36 @@
+* W. Trevor King (wking@drexel.edu) wrote:
+> One problem is that we don't actually have "releases". People just
+> clone a branch, install, and go.
+
+ This is actually the main reason I've manually mirrored the tree in
+the past, so that users of our projects can get BE. If tarballs were
+available I probably wouldn't even bother, but bzr really isn't a nice
+dependency for just submitting/commenting on bugs.
+
+ Isn't there a bzr web interface that at least supports creating
+tarballs/zips? It is pretty standard functionality for most other VCS'
+web interfaces so I'm guessing there must be, but loggerhead seems not
+to support it.
+
+ If it is a case of not having the hardware to host a more featureful
+web UI I may be able to offer some assistance.
+
+> If you're worried about stability, just clone from a more stable branch
+> (i.e., Chris' trunk). I think > this is good for distributed development,
+> but maybe makes it hard to package into a conventional release-based system.
+> With the bzr patch number in setup.py as the patch release number, I would be
+> releasing my 0.1.363 while Chris releases his 0.1.314, even though they're at
+> about the same point. I would rather be releasing my
+> 0.1.20090714121347
+> while Chris releases his
+> 0.1.20090713154540
+> Since then the similarity is clearer.
+
+ Both approaches seem pretty odd to me, as a user you would have no
+idea if 0.1.200910302359 has the fixes you required in a release you
+were using that was numbered 0.1.200907141554. Surely you'd at least be
+{pre,suf}fixing a branch name to the version.
+
+Thanks,
+
+James
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/values
new file mode 100644
index 0000000..e7077e7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254/values
@@ -0,0 +1,14 @@
+Alt-id: <20090714142942.GA5717@ukfsn.org>
+
+
+Author: James Rowe <jnrowe@gmail.com>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 15:29:42 +0100
+
+
+In-reply-to: ea01c122-e629-4d5c-afa7-b180f4a8748b
+
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
index 0000000..8c890f3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/body
@@ -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
index 0000000..3b45fbf
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values
@@ -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/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/body
new file mode 100644
index 0000000..7e1434b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/body
@@ -0,0 +1,115 @@
+On Tue, Jul 14, 2009 at 03:29:42PM +0100, James Rowe wrote:
+> * W. Trevor King (wking@drexel.edu) wrote:
+> > One problem is that we don't actually have "releases". People just
+> > clone a branch, install, and go.
+>
+> This is actually the main reason I've manually mirrored the tree in
+> the past, so that users of our projects can get BE. If tarballs were
+> available I probably wouldn't even bother, but bzr really isn't a nice
+> dependency for just submitting/commenting on bugs.
+
+Fair enough. It will be good when we get a commit-able web interface
+and/or email interface going.
+
+> Isn't there a bzr web interface that at least supports creating
+> tarballs/zips? It is pretty standard functionality for most other VCS'
+> web interfaces so I'm guessing there must be, but loggerhead seems not
+> to support it.
+
+Unfortunately, people would still need bzr to install the versioned source:
+
+ libbe/_version.py:
+ bzr version-info --format python > $@
+
+So you'll need a "release" target in the makefile to build a bzr-less
+install. While you're at it, you should probably compile the manpage
+too to remove the docbook-to-man dependency.
+
+Do people want a HEAD tarball too? There must be a bzr equivalent of
+ .git/hooks/post-update
+but I don't know what it is.
+
+> > If you're worried about stability, just clone from a more stable branch
+> > (i.e., Chris' trunk). I think > this is good for distributed development,
+> > but maybe makes it hard to package into a conventional release-based system.
+> > With the bzr patch number in setup.py as the patch release number, I would be
+> > releasing my 0.1.363 while Chris releases his 0.1.314, even though they're at
+> > about the same point. I would rather be releasing my
+> > 0.1.20090714121347
+> > while Chris releases his
+> > 0.1.20090713154540
+> > Since then the similarity is clearer.
+>
+> Both approaches seem pretty odd to me, as a user you would have no
+> idea if 0.1.200910302359 has the fixes you required in a release you
+> were using that was numbered 0.1.200907141554. Surely you'd at least be
+> {pre,suf}fixing a branch name to the version.
+
+"be --version" currently gives you the revision id:
+ wking@drexel.edu-20090714121347-c6rloikst1t3m5yl
+which tells you exactly which commit your installed version is based on.
+If we want stick with revision numbers, how about:
+ major.minor.revno-branch_nick
+But then we'd have to pick "unique" branch nicknames...
+
+I'd sliced out the timestamp portion of the revision id so that the
+"patch-number" would be an integer and the branch name wasn't
+references, so that "upgrading" from one branch to another could be
+transparent to the users (who just see an increading timestamp), but
+still available to the developers (who know when commits were made to
+the branches they track, and the likelyhood of to-the-second commit
+collisions in official packages is small).
+
+On Wed, Jul 15, 2009 at 12:54:05AM +1000, Ben Finney wrote:
+> "W. Trevor King" <wking@drexel.edu> writes:
+>
+> > On Tue, Jul 14, 2009 at 10:36:26PM +1000, Ben Finney wrote:
+> > > Please, no. Timestamps aren't version strings, that's conflating two
+> > > pieces of information with very different meanings. Correlating the
+> > > two is the job of a changelog.
+> >
+> > Which we don't bother keeping (also NEWS), since "bzr log" works so
+> > nicely.
+>
+> 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 need a user around to help me determine "user-visable" changes ;).
+My labmates loose interest after be init/new/comment :p. None of
+which has ever changed, other than set-root -> init ;).
+
+> > The timestamp should at least replace the patch release number, which
+> > you agree is-desirable-to increase motonically ;).
+>
+> I still disagree that a timestamp is the right thing to use there. If
+> you want a monotonically-increasing indicator of which revision we're up
+> to, that's immediately available with the revision number from VCS on
+> the main branch. That also has the advantage of producing consecutive
+> numbers for each revision, by definition.
+
+But not during branch-switches, while my method skips large regions,
+but probably increases during any reasonable branch-switch. For
+example, when I upgraded to rich root to pull Ben's patch, I'm not
+sure if Chris upgraded the trunk and merged my branch, or just ditched
+the trunk and cloned my branch. Using actual bzr revision numbers
+would make switching branches that either wrong (in the case of
+rev-id decreases) or confusing (in the case of a single
+non-consecutive increase).
+
+On Tue, Jul 14, 2009 at 11:11:31AM -0400, Chris Ball wrote:
+> > I agree that's a problem. I think the solution is to start making
+> > releases, with specific version strings, as source tarballs.
+>
+> I'm happy to do this if people think it would be useful, and I don't
+> yet have a strong opinion on whether the releases should come with
+> version numbers or timestamps.
+
+I imagine the news from 2006 to now will be somewhat abridged ;).
+
+--
+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/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/values
new file mode 100644
index 0000000..9e84a24
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/values
@@ -0,0 +1,14 @@
+Alt-id: <20090714171725.GB10445@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 13:17:25 -0400
+
+
+In-reply-to: 0c40c13a-3515-4b45-a8c3-142cceab9254
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/body
new file mode 100644
index 0000000..a0b6a14
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/body
@@ -0,0 +1,58 @@
+Chris Ball <cjb@laptop.org> writes:
+
+> 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
+
+I think I have a better understanding of why this apparent disagreement
+occurred, and it was due to my sloppy use of terms.
+
+Looking into it further, it seems there is a certain expectation (set,
+in part, by the long-standing GNU coding conventions) that a “GNU-style
+ChangeLog” contains not only a particular *format*, but information at
+a particular level of *detail*.
+
+That is, a GNU ChangeLog is intended for the style of work where one
+logs all the changes made to every file in the tree each working day,
+and then makes a new day's entry above that, and so on. This is, of
+course, entirely redundant with a VCS revision history, which makes all
+the commit messages available on request.
+
+So to disambiguate, that's not what I meant. I'm more familiar with a
+less-frequently-updated and less-fine-detail change log; perhaps more
+akin to the GNU-style “NEWS” file.
+
+> 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.
+
+Yes, that's mostly what I meant.
+
+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.
+
+--
+ \ “… Nature … is seen to do all things Herself and through |
+ `\ herself of own accord, rid of all gods.” —Titus Lucretius |
+_o__) Carus, c. 40 BCE |
+Ben Finney
+
+
+_______________________________________________
+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/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/values
new file mode 100644
index 0000000..320c484
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a/values
@@ -0,0 +1,14 @@
+Alt-id: <87hbxdhtkp.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 16 Jul 2009 19:21:10 +1000
+
+
+In-reply-to: cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/body
new file mode 100644
index 0000000..5f478b5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/body
@@ -0,0 +1,96 @@
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+W. Trevor King wrote:
+> Thinking about this some more, I think that the role of the
+> main-branch is to officially sanction the current state of the code as
+> "released". If a series of commits will leave a branch in a
+> known-unusable form, they should be carried out in some appropriately
+> named development branch. Then the log of commits to the main branch
+> ("bzr log -n 1" for bzr > ) should produce a fairly respectable
+> changelog.
+
+This is how we develop bzr itself. The mainline is controlled by PQM,
+which is a tool that merges feature branches, runs the tests, and
+commits only if the tests pass.
+
+$ bzr log --short --limit 10
+ 4534 Canonical.com Patch Queue Manager 2009-07-14 [merge]
+ (abentley) Implement merge --interactive
+
+ 4533 Canonical.com Patch Queue Manager 2009-07-14 [merge]
+ (jml) Merge in changes from 1.17 branch.
+
+ 4532 Canonical.com Patch Queue Manager 2009-07-14 [merge]
+ (igc) zc.buildout Windows build support (Sidnei da Silva)
+
+ 4531 Canonical.com Patch Queue Manager 2009-07-13 [merge]
+ (vila) Delete forgotten debug print
+
+ 4530 Canonical.com Patch Queue Manager 2009-07-13 [merge]
+ (vila) Isolate some tests from TZ
+
+ 4529 Canonical.com Patch Queue Manager 2009-07-13 [merge]
+ (igc) Bazaar 2.0 Upgrade Guide
+
+ 4528 Canonical.com Patch Queue Manager 2009-07-13 [merge]
+ (mbp) correction to news
+
+ 4527 Canonical.com Patch Queue Manager 2009-07-13 [merge]
+ (jml) Merge in 1.17 branch, updating version numbers and NEWS file.
+
+ 4526 Canonical.com Patch Queue Manager 2009-07-10 [merge]
+ (mbp, vila) Finish the *_implementation to per_* test renaming
+
+ 4525 Canonical.com Patch Queue Manager 2009-07-10 [merge]
+ (vila) Quicker check for changes in mutable trees
+
+You can also see all the merges as they come into the mainline:
+
+$ bzr log --short --limit 10 --include-merges
+ 4534 Canonical.com Patch Queue Manager 2009-07-14 [merge]
+ (abentley) Implement merge --interactive
+
+ 4526.6.15 Aaron Bentley 2009-07-14
+ Update command help
+
+ 4526.6.14 Aaron Bentley 2009-07-14
+ Use default DiffWriter.
+
+ 4526.6.13 Aaron Bentley 2009-07-14
+ Add docstring to do_interactive.
+
+ 4526.6.12 Aaron Bentley 2009-07-14
+ Updates from review.
+
+ 4526.6.11 Aaron Bentley 2009-07-13
+ Update NEWS.
+
+ 4526.6.10 Aaron Bentley 2009-07-13 [merge]
+ Merged apply-vocab into merge-interactive.
+
+ 4526.7.4 Aaron Bentley 2009-07-13 [merge]
+ Merged bzr.dev into apply-vocab.
+
+ 4526.6.9 Aaron Bentley 2009-07-13 [merge]
+ Merged apply-vocab into merge-interactive.
+
+ 4526.7.3 Aaron Bentley 2009-07-13
+ Test shelve_change.
+
+> This also means that _every_commit_ to a main branch would
+> be an official release.
+
+We don't do that. We have official releases every 4 weeks, but we do
+believe that running bzr.dev is pretty safe, because it's always tested
+and our test suite is quite thorough.
+
+Aaron
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.9 (GNU/Linux)
+Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org
+
+iEYEARECAAYFAkpcznIACgkQ0F+nu1YWqI0yhACePTFUUp6u+Dw+8IRwWOWBQRtb
+TgsAniJq4lqnDfjNACMr7IEt7xYJhx7m
+=BbGG
+-----END PGP SIGNATURE-----
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/values
new file mode 100644
index 0000000..8cfe1b0
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb/values
@@ -0,0 +1,14 @@
+Alt-id: <4A5CCE76.9040106@aaronbentley.com>
+
+
+Author: Aaron Bentley <aaron@aaronbentley.com>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 14:29:10 -0400
+
+
+In-reply-to: ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/body
new file mode 100644
index 0000000..b34e037
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/body
@@ -0,0 +1,52 @@
+On Tue, Jul 14, 2009 at 07:34:04PM +0100, jnrowe@gmail.com wrote:
+> [This time to the list]
+>
+> * W. Trevor King (wking@drexel.edu) wrote:
+> > On Tue, Jul 14, 2009 at 03:29:42PM +0100, James Rowe wrote:
+> > > Isn't there a bzr web interface that at least supports creating
+> > > tarballs/zips? It is pretty standard functionality for most other VCS'
+> > > web interfaces so I'm guessing there must be, but loggerhead seems not
+> > > to support it.
+> >
+> > Unfortunately, people would still need bzr to install the versioned source:
+> >
+> > libbe/_version.py:
+> > bzr version-info --format python > $@
+>
+> I hadn't even seen that change go in. The last upstream change in the
+> version I have installed locally was by you on 2008-11-24.
+
+It's only been in Chris' http://bzr.bugseverywhere.org/be/ branch
+since revno: 321, 2009-06-25. Obviously we may have to adjust the
+--verison output once we settle on a versioning scheme, but whatever
+we pick, I think having the auto-generated libbe/_version.py around
+for pinpointing bugs is worth the trouble of requiring bzr when
+building distribution tarballs.
+
+> > So you'll need a "release" target in the makefile to build a bzr-less
+> > install. While you're at it, you should probably compile the manpage
+> > too to remove the docbook-to-man dependency.
+>
+> Maybe for others. Our packages just don't have the manpage as it is only
+> the "be help" text reformatted, the easy option is sometimes the right
+> one :) Also, I've just noticed that it has even less documentation in
+> the bzr tree[1] making its installation much less compelling unless your
+> packaging rules require a man page like Debians.
+>
+> Out of curiosity why is the Makefile being used for this stuff anyway?
+> It is going to make it difficult to build locally when we finally get
+> around to merging. Examples: If distutils was being used exclusively it
+> would fix the #! lines in xml/*. We'd be able to point Python
+> $version_of_the_day at setup.py instead of having to sed the Makefile or
+> run setup and manually install other files.
+
+I speak Makefile better than I speak distutils ;). I'm not sure how
+to translate the be.1 generation/installation or the libbe/_version.py
+generation into distutils. Anyone else?
+
+--
+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/31beb504-c72b-4304-95ba-a66d2bcbc46a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/values
new file mode 100644
index 0000000..e0c0955
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/values
@@ -0,0 +1,14 @@
+Alt-id: <20090714191145.GB10606@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 15:11:45 -0400
+
+
+In-reply-to: 6e315abe-a080-4369-8729-4aea2dee8494
+
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
index 0000000..4ebb4f2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/body
@@ -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
index 0000000..b45a747
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values
@@ -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/6e315abe-a080-4369-8729-4aea2dee8494/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494/body
new file mode 100644
index 0000000..7ffe231
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494/body
@@ -0,0 +1,38 @@
+[This time to the list]
+
+* W. Trevor King (wking@drexel.edu) wrote:
+> On Tue, Jul 14, 2009 at 03:29:42PM +0100, James Rowe wrote:
+> > Isn't there a bzr web interface that at least supports creating
+> > tarballs/zips? It is pretty standard functionality for most other VCS'
+> > web interfaces so I'm guessing there must be, but loggerhead seems not
+> > to support it.
+>
+> Unfortunately, people would still need bzr to install the versioned source:
+>
+> libbe/_version.py:
+> bzr version-info --format python > $@
+
+ I hadn't even seen that change go in. The last upstream change in the
+version I have installed locally was by you on 2008-11-24.
+
+> So you'll need a "release" target in the makefile to build a bzr-less
+> install. While you're at it, you should probably compile the manpage
+> too to remove the docbook-to-man dependency.
+
+ Maybe for others. Our packages just don't have the manpage as it is only
+the "be help" text reformatted, the easy option is sometimes the right
+one :) Also, I've just noticed that it has even less documentation in
+the bzr tree[1] making its installation much less compelling unless your
+packaging rules require a man page like Debians.
+
+ Out of curiosity why is the Makefile being used for this stuff anyway?
+It is going to make it difficult to build locally when we finally get
+around to merging. Examples: If distutils was being used exclusively it
+would fix the #! lines in xml/*. We'd be able to point Python
+$version_of_the_day at setup.py instead of having to sed the Makefile or
+run setup and manually install other files.
+
+Thanks,
+
+James
+ 1. http://pullcord.laptop.org:4000/revision/314.1.15/doc/be.1.sgml
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494/values
new file mode 100644
index 0000000..4f1d60d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494/values
@@ -0,0 +1,14 @@
+Alt-id: <20090714183404.GB26032@ukfsn.org>
+
+
+Author: jnrowe@gmail.com
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 19:34:04 +0100
+
+
+In-reply-to: 1f40efc1-6efc-4dd8-bdd2-97907e5aa624
+
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
index 0000000..d00eb64
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/body
@@ -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
index 0000000..4fb068d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/values
@@ -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/744435b7-1521-4059-a55d-f0c403d7b4d8/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8/body
new file mode 100644
index 0000000..24ff7b0
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8/body
@@ -0,0 +1,58 @@
+"W. Trevor King" <wking@drexel.edu> writes:
+
+> Currently setup.py sets the version number for BE to 0.0.193 and the
+> url to http://panoramicfeedback.com/opensource/. These are both a bit
+> outdated ;).
+
+Right, that should change.
+
+> I've switched my branch over to the current url, and moved to
+> last-commit-timestamp version numbers.
+
+Please, no. Timestamps aren't version strings, that's conflating two
+pieces of information with very different meanings. Correlating the two
+is the job of a changelog.
+
+> This removes the "prefered branch" issues with the old scheme, and
+> version numbers should increase monotonically
+
+The English word “should” is ambiguous in this context. Are you saying
+this is desirable, or are you predicting that it will likely be the
+case?
+
+I don't see how it's either, so am doubly confused :-)
+
+> but it looses any stability information suggested by the preceding
+> 0.0.
+
+The convention for three-part version strings is often:
+
+ * Major release number (big changes in how the program works,
+ increasing monotonically per major release, with “0”indicating no
+ major release yet)
+
+ * Minor release number (smaller impact on how the program works,
+ increasing monotonically per minor release, with “0” indicating no
+ minor release yet since the previous major)
+
+ * Patch release number (bug-fix and other changes that don't affect
+ the documented interface, increasing monotonically per patch
+ release, with “0” indicating no patch release since the previous
+ major or minor)
+
+Obviously there's no standard or enforcement for this, but that's the
+interpretation I usually give to dotted version strings in the absence
+of more formal declaration specific to the project.
+
+> We can add those back in if people want. Does the first 0 mean
+> "interfaces in flux" and the second 0 mean "lots of bugs"? If so, I
+> think we're up to 0.1, since the major features are pretty calm.
+
+I disagree with your interpretation and prefer mine, above; on that
+basis, I agree that we're at least up to version 0.1 by now :-)
+
+--
+ \ “A lot of water has been passed under the bridge since this |
+ `\ variation has been played.” chess book, Russia |
+_o__) |
+Ben Finney
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8/values
new file mode 100644
index 0000000..c5d9cbb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8/values
@@ -0,0 +1,14 @@
+Alt-id: <87ocrnjvat.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 22:36:26 +1000
+
+
+In-reply-to: cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c
+
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
index 0000000..a3fc57f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/body
@@ -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
index 0000000..b6d25cb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values
@@ -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
index 0000000..5d29f85
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/body
@@ -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
index 0000000..7f205d6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values
@@ -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/a536cee5-cc8d-4b18-b491-657e0c7998b4/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4/body
new file mode 100644
index 0000000..8b32751
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4/body
@@ -0,0 +1,14 @@
+Hi,
+
+ > Which we don't bother keeping (also NEWS), since "bzr log" works
+ > so nicely. If you really want an standard changelog, see
+ > http://mail.gnome.org/archives/desktop-devel-list/2007-September/msg00186.html
+
+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.)
+
+- Chris.
+--
+Chris Ball <cjb@laptop.org>
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4/values
new file mode 100644
index 0000000..239feb5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4/values
@@ -0,0 +1,14 @@
+Alt-id: <m3ljmrfgot.fsf@pullcord.laptop.org>
+
+
+Author: Chris Ball <cjb@laptop.org>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 11:05:38 -0400
+
+
+In-reply-to: ea01c122-e629-4d5c-afa7-b180f4a8748b
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/body
new file mode 100644
index 0000000..33a8d66
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/body
@@ -0,0 +1,51 @@
+I don't think anyone's changing their mind ;), so tallying the
+comments so far:
+
+On Wed, Jul 15, 2009 at 12:54:05AM +1000, Ben Finney wrote:
+> I still disagree that a timestamp is the right thing to use there. If
+> you want a monotonically-increasing indicator of which revision we're up
+> to, that's immediately available with the revision number from VCS on
+> the main branch. That also has the advantage of producing consecutive
+> numbers for each revision, by definition.
+
++1 for trunk version number.
+
+On Tue, Jul 14, 2009 at 05:27:52PM +0200, Elena of Valhalla wrote:
+> I also have a weak preference for version numbers, as long as they
+> give useful informations on the state the release.
+
++1 for trunk version number.
+
+On Tue, Jul 14, 2009 at 02:29:10PM -0400, Aaron Bentley wrote:
+> We don't do that. We have official releases every 4 weeks, but we do
+> believe that running bzr.dev is pretty safe, because it's always tested
+> and our test suite is quite thorough.
+
++1 for by hand version bumps.
+
+On Fri, Jul 17, 2009 at 11:37:49PM +0200, Gianluca Montecchi wrote:
+> The version number of trunk _is_ should be the official version number of the
+> Bugs Everywhere releases.
+> The version number in branch does not means nothing outside the branch.
+> At least we can have a mechanism to build a version number scheme that is
+> consistent for us to be able to merge branch easily.
+
++1 for trunk version number.
+
+And me with my timestamps ;).
+
+Sounds like we should go with trunk version number, but that it should
+be set by hand whenever Chris decides to release something, since the
+rest of us don't know what version the trunk is on. Unless we do
+something like:
+ bzr log -n 0 | grep -B2 'nick: be$' | head -n1 | sed 's/ *revno: \([0-9]*\).*/\1/'
+to extract the last trunk commit referenced from our branch.
+
+Implementation preferences? (i.e. Chris vs. regexp matching :p)
+
+--
+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/a845096e-3cdf-41ed-a0e3-283439665b92/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/values
new file mode 100644
index 0000000..b757933
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/values
@@ -0,0 +1,14 @@
+Alt-id: <20090718105008.GA31639@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 18 Jul 2009 06:50:08 -0400
+
+
+In-reply-to: c35835c0-8f9f-4090-ba92-1f616867e486
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/body
new file mode 100644
index 0000000..063afcb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/body
@@ -0,0 +1,30 @@
+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.
+
+ > I agree that's a problem. I think the solution is to start making
+ > releases, with specific version strings, as source tarballs.
+
+I'm happy to do this if people think it would be useful, and I don't
+yet have a strong opinion on whether the releases should come with
+version numbers or timestamps.
+
+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
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/values
new file mode 100644
index 0000000..466be33
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8/values
@@ -0,0 +1,14 @@
+Alt-id: <m3k52bfgf0.fsf@pullcord.laptop.org>
+
+
+Author: Chris Ball <cjb@laptop.org>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 11:11:31 -0400
+
+
+In-reply-to: ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/body
new file mode 100644
index 0000000..1e2a870
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/body
@@ -0,0 +1,37 @@
+On Tue, Jul 14, 2009 at 01:17:25PM -0400, W. Trevor King wrote:
+> On Wed, Jul 15, 2009 at 12:54:05AM +1000, Ben Finney wrote:
+> > "W. Trevor King" <wking@drexel.edu> writes:
+> >
+> > > On Tue, Jul 14, 2009 at 10:36:26PM +1000, Ben Finney wrote:
+> > > > Please, no. Timestamps aren't version strings, that's conflating two
+> > > > pieces of information with very different meanings. Correlating the
+> > > > two is the job of a changelog.
+> > >
+> > > Which we don't bother keeping (also NEWS), since "bzr log" works so
+> > > nicely.
+> >
+> > 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 need a user around to help me determine "user-visable" changes ;).
+> My labmates loose interest after be init/new/comment :p. None of
+> which has ever changed, other than set-root -> init ;).
+
+Thinking about this some more, I think that the role of the
+main-branch is to officially sanction the current state of the code as
+"released". If a series of commits will leave a branch in a
+known-unusable form, they should be carried out in some appropriately
+named development branch. Then the log of commits to the main branch
+("bzr log -n 1" for bzr > ) should produce a fairly respectable
+changelog. Obviously we are all quite guilty of doing most of our
+development in single branches, but it may be a useful model going
+forward. This also means that _every_commit_ to a main branch would
+be an official release.
+
+--
+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/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/values
new file mode 100644
index 0000000..b5c41c9
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/values
@@ -0,0 +1,14 @@
+Alt-id: <20090714182034.GA10606@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 14:20:34 -0400
+
+
+In-reply-to: 1f40efc1-6efc-4dd8-bdd2-97907e5aa624
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/body
new file mode 100644
index 0000000..e02bd38
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/body
@@ -0,0 +1,25 @@
+On Tue, Jul 14, 2009 at 5:11 PM, Chris Ball<cjb@laptop.org> wrote:
+>   > I agree that's a problem. I think the solution is to start making
+>   > releases, with specific version strings, as source tarballs.
+>
+> I'm happy to do this if people think it would be useful, and I don't
+> yet have a strong opinion on whether the releases should come with
+> version numbers or timestamps.
+
+as an user of be that plans to try and "package" it for openembedded,
+a release would be very useful: writing a recipe that downloads a
+specific commit from bzr and builds it is probably feasible, but
+definitely not ideal.
+
+I also have a weak preference for version numbers, as long as they
+give useful informations on the state the release.
+
+--
+Elena ``of Valhalla''
+
+email: elena.valhalla@gmail.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/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/values
new file mode 100644
index 0000000..57b408f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b/values
@@ -0,0 +1,14 @@
+Alt-id: <5c5e5c350907140827u218553e8rc5773325d43c2bf2@mail.gmail.com>
+
+
+Author: Elena of Valhalla <elena.valhalla@gmail.com>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 17:27:52 +0200
+
+
+In-reply-to: aad59898-8949-44fb-ad0b-2acea6eb2ef8
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486/body
new file mode 100644
index 0000000..d8014d2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486/body
@@ -0,0 +1,102 @@
+On Thursday 16 July 2009 12:38:55 W. Trevor King wrote:
+> On Thu, Jul 16, 2009 at 07:32:31PM +1000, Ben Finney wrote:
+> > "W. Trevor King" <wking@drexel.edu> writes:
+> > > On Wed, Jul 15, 2009 at 12:54:05AM +1000, Ben Finney wrote:
+> > > > "W. Trevor King" <wking@drexel.edu> writes:
+> > > > > On Tue, Jul 14, 2009 at 10:36:26PM +1000, Ben Finney wrote:
+> > > > > > Please, no. Timestamps aren't version strings, that's conflating
+> > > > > > two pieces of information with very different meanings.
+> > > > > > Correlating the two is the job of a [NEWS file].
+> > > >
+> > > > If you want a monotonically-increasing indicator of which revision
+> > > > we're up to, that's immediately available with the revision number
+> > > > from VCS on the main branch. That also has the advantage of
+> > > > producing consecutive numbers for each revision, by definition.
+> > >
+> > > But not during branch-switches, while my method skips large regions,
+> > > but probably increases during any reasonable branch-switch.
+> >
+> > I've read this several times now, and I don't see what it's saying.
+> >
+> > The assumption I'm making is that there is a single canonical “main
+> > branch”, from which releases will be made.
+>
+> I don't think you need to assume this. See my "virtual branch"
+> argument below.
+
+But if we have a canonical "main branch" that we release, and the packager
+get, we can refer to it as the stable branch, that it is not a bad idea.
+
+
+
+> > The version number set in that branch is the one which determines
+> > the version of Bugs Everywhere as a whole.
+>
+> If you are suggesting that the dev branches adjust their release
+> number _by_hand_ to match the current trunk release number, that
+> allows switching, but sounds like a lot of work and isn't correct
+> anyway, since they are not in the same state as the trunk.
+
+The version number of trunk _is_ should be the official version number of the
+Bugs Everywhere releases.
+The version number in branch does not means nothing outside the branch.
+At least we can have a mechanism to build a version number scheme that is
+consistent for us to be able to merge branch easily.
+
+> > The revision number is only useful in the context of the branch, so it
+> > only matters when comparing versions within a branch. When you switch
+> > between branches, if you're interested in the revision number you'll
+> > still need to know which branch you're talking about.
+>
+> I think this is our main disagreement. I see all the branches as part
+> of the same codebase, with monotonically increasing timestamp patch
+> numbers. If you were to collapse all the commit snapshots down into a
+> single chronological "virtual branch", it would still make sense, it
+> would just be a bit unorganized. We do all try to move in the same
+> general direction ;).
+
+I don't think that, outside the developers, a version number like
+
+cjb@laptop.org-20090713154540-ve4pmydqzb1ghgvc
+
+is a good choice, not for the user of BE, not for the packager of BE
+
+
+> > This, then, is an argument for not having the revision number in the
+> > version string at all. The version then becomes a more traditional
+> > “major.minor.patch” tuple, and is only ever updated when some release
+> > manager of the canonical branch decides it's correct to do so.
+>
+> It is an argument for not using the revision number. You can avoid
+> revision numbers by using hand-coded patch numbers, or by using
+> timestamps, which is what we're trying to decide on :p.
+
+We can use both.
+During the development we can use version number like
+
+x.y.z.timestamp
+
+As we decide to release a stable version, the release manager set the version
+number to a more traditional x.y.z format, and create a branch (stable branch)
+
+This way we have these advantages:
+
+1) an user have a simple version number to use for bug report/feature
+request/help request
+
+2) a packager have an easy life to choose to package a stable or a trunk
+version, knowing what are they doing
+
+bonus) we can maintain a stable and a developmente source tree/branch, where
+in the development tree we can make also backward incompatible modification to
+the source without making any damage to the users/packagers, while in the
+stable branch we can make only bugfix/security fix or port from the devel branch
+some interesting features as long as they don't break compatibility.
+
+bye
+Gianluca
+
+_______________________________________________
+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/c35835c0-8f9f-4090-ba92-1f616867e486/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486/values
new file mode 100644
index 0000000..c7c0273
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486/values
@@ -0,0 +1,14 @@
+Alt-id: <200907172337.49779.gian@grys.it>
+
+
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 17 Jul 2009 23:37:49 +0200
+
+
+In-reply-to: f925e56f-26f9-4620-82fb-a0f160f27921
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/body
new file mode 100644
index 0000000..4e8445a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/body
@@ -0,0 +1,18 @@
+Currently setup.py sets the version number for BE to 0.0.193 and the
+url to http://panoramicfeedback.com/opensource/. These are both a bit
+outdated ;). I've switched my branch over to the current url, and
+moved to last-commit-timestamp version numbers. This removes the
+"prefered branch" issues with the old scheme, and version numbers
+should increase monotonically, but it looses any stability information
+suggested by the preceding 0.0.
+
+We can add those back in if people want. Does the first 0 mean
+"interfaces in flux" and the second 0 mean "lots of bugs"? If so, I
+think we're up to 0.1, since the major features are pretty calm.
+
+--
+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/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/values
new file mode 100644
index 0000000..00309a2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/values
@@ -0,0 +1,11 @@
+Alt-id: <20090714110543.GB4855@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 07:05:43 -0400
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/body
new file mode 100644
index 0000000..fce4941
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/body
@@ -0,0 +1,72 @@
+On Tue, Jul 14, 2009 at 10:36:26PM +1000, Ben Finney wrote:
+> "W. Trevor King" <wking@drexel.edu> writes:
+> > I've switched my branch over to the current url, and moved to
+> > last-commit-timestamp version numbers.
+>
+> Please, no. Timestamps aren't version strings, that's conflating two
+> pieces of information with very different meanings. Correlating the two
+> is the job of a changelog.
+
+Which we don't bother keeping (also NEWS), since "bzr log" works so nicely.
+If you really want an standard changelog, see
+ http://mail.gnome.org/archives/desktop-devel-list/2007-September/msg00186.html
+
+> > This removes the "prefered branch" issues with the old scheme, and
+> > version numbers should increase monotonically
+>
+> The English word “should” is ambiguous in this context. Are you saying
+> this is desirable, or are you predicting that it will likely be the
+> case?
+
+Both.
+
+> I don't see how it's either, so am doubly confused :-)
+
+The timestamp should at least replace the patch release number, which
+you agree is-desirable-to increase motonically ;). I also predict
+that it will increase monotonically for any given branch, since the
+branch HEAD will have both the most recent commit and the highest
+version number. The only problem I can think of is having your clock
+_way_ off, and that is unlikely enough to ignore. If you hop between
+branches, the timestamp is much more likely to increase going to the
+more modern branch than the bzr revision number, which desynchronize
+between branches fairly quickly.
+
+> The convention for three-part version strings is often:
+>
+> * Major release number (big changes in how the program works,
+> increasing monotonically per major release, with “0”indicating no
+> major release yet)
+>
+> * Minor release number (smaller impact on how the program works,
+> increasing monotonically per minor release, with “0” indicating no
+> minor release yet since the previous major)
+>
+> * Patch release number (bug-fix and other changes that don't affect
+> the documented interface, increasing monotonically per patch
+> release, with “0” indicating no patch release since the previous
+> major or minor)
+
+One problem is that we don't actually have "releases". People just
+clone a branch, install, and go. If you're worried about stability,
+just clone from a more stable branch (i.e., Chris' trunk). I think
+this is good for distributed development, but maybe makes it hard to
+package into a conventional release-based system. With the bzr patch
+number in setup.py as the patch release number, I would be releasing
+my 0.1.363 while Chris releases his 0.1.314, even though they're at
+about the same point. I would rather be releasing my
+ 0.1.20090714121347
+while Chris releases his
+ 0.1.20090713154540
+Since then the similarity is clearer.
+
+At any rate, I think the two approaches are close enough that an
+auto-updating timestamp beats a manually bumped patch number, since
+no-one ever actually bumps the patch number ;).
+
+--
+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/ea01c122-e629-4d5c-afa7-b180f4a8748b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/values
new file mode 100644
index 0000000..a3f74c4
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/values
@@ -0,0 +1,14 @@
+Alt-id: <20090714133732.GB6160@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 14 Jul 2009 09:37:32 -0400
+
+
+In-reply-to: 744435b7-1521-4059-a55d-f0c403d7b4d8
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/body
new file mode 100644
index 0000000..5eeb353
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/body
@@ -0,0 +1,88 @@
+On Thu, Jul 16, 2009 at 07:32:31PM +1000, Ben Finney wrote:
+> "W. Trevor King" <wking@drexel.edu> writes:
+>
+> > On Wed, Jul 15, 2009 at 12:54:05AM +1000, Ben Finney wrote:
+> > > "W. Trevor King" <wking@drexel.edu> writes:
+> > >
+> > > > On Tue, Jul 14, 2009 at 10:36:26PM +1000, Ben Finney wrote:
+> > > > > Please, no. Timestamps aren't version strings, that's conflating
+> > > > > two pieces of information with very different meanings.
+> > > > > Correlating the two is the job of a [NEWS file].
+> >
+> > > If you want a monotonically-increasing indicator of which revision
+> > > we're up to, that's immediately available with the revision number
+> > > from VCS on the main branch. That also has the advantage of
+> > > producing consecutive numbers for each revision, by definition.
+> >
+> > But not during branch-switches, while my method skips large regions,
+> > but probably increases during any reasonable branch-switch.
+>
+> I've read this several times now, and I don't see what it's saying.
+>
+> The assumption I'm making is that there is a single canonical “main
+> branch”, from which releases will be made.
+
+I don't think you need to assume this. See my "virtual branch"
+argument below.
+
+> The version number set in that branch is the one which determines
+> the version of Bugs Everywhere as a whole.
+
+If you are suggesting that the dev branches adjust their release
+number _by_hand_ to match the current trunk release number, that
+allows switching, but sounds like a lot of work and isn't correct
+anyway, since they are not in the same state as the trunk.
+
+> The revision number is only useful in the context of the branch, so it
+> only matters when comparing versions within a branch. When you switch
+> between branches, if you're interested in the revision number you'll
+> still need to know which branch you're talking about.
+
+I think this is our main disagreement. I see all the branches as part
+of the same codebase, with monotonically increasing timestamp patch
+numbers. If you were to collapse all the commit snapshots down into a
+single chronological "virtual branch", it would still make sense, it
+would just be a bit unorganized. We do all try to move in the same
+general direction ;).
+
+> Switching between branches doesn't change the canonical version string.
+
+Different released code should have different version numbers.
+
+> > For example, when I upgraded to rich root to pull Ben's patch, I'm not
+> > sure if Chris upgraded the trunk and merged my branch, or just ditched
+> > the trunk and cloned my branch. Using actual bzr revision numbers
+> > would make switching branches that either wrong (in the case of rev-id
+> > decreases) or confusing (in the case of a single non-consecutive
+> > increase).
+>
+> This, then, is an argument for not having the revision number in the
+> version string at all. The version then becomes a more traditional
+> “major.minor.patch” tuple, and is only ever updated when some release
+> manager of the canonical branch decides it's correct to do so.
+
+It is an argument for not using the revision number. You can avoid
+revision numbers by using hand-coded patch numbers, or by using
+timestamps, which is what we're trying to decide on :p.
+
+> If we use the ‘bzr version-info --format=python > foo_version.py’
+> command in some build routine, the branch's revision number will be
+> available directly within Python by importing that module. That would
+> allow it to be output in some UI, if that's what you're interested in
+> seeing.
+
+True. Which means that whichever version string wins out, the other
+side will still be able to access the info we both want included ;).
+We can certainly suggest that bug reporters submit their
+ be --verbose-version
+when they submit bugs. The only role of the official "version string"
+is to make life easy for packagers. If they woln't be switching
+branches, then either of our proposals are fine. If they will, then
+I think timestamps work better.
+
+--
+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/f925e56f-26f9-4620-82fb-a0f160f27921/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/values
new file mode 100644
index 0000000..63a2cae
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/values
@@ -0,0 +1,14 @@
+Alt-id: <20090716103855.GA8579@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 16 Jul 2009 06:38:55 -0400
+
+
+In-reply-to: fdb615a4-168a-467b-8090-875c998455e5
+
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
index 0000000..dee72c7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/body
@@ -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
index 0000000..2e85e56
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 20 Nov 2009 21:45:50 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/body
new file mode 100644
index 0000000..b36292a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/body
@@ -0,0 +1,55 @@
+"W. Trevor King" <wking@drexel.edu> writes:
+
+> On Wed, Jul 15, 2009 at 12:54:05AM +1000, Ben Finney wrote:
+> > "W. Trevor King" <wking@drexel.edu> writes:
+> >
+> > > On Tue, Jul 14, 2009 at 10:36:26PM +1000, Ben Finney wrote:
+> > > > Please, no. Timestamps aren't version strings, that's conflating
+> > > > two pieces of information with very different meanings.
+> > > > Correlating the two is the job of a [NEWS file].
+>
+> > If you want a monotonically-increasing indicator of which revision
+> > we're up to, that's immediately available with the revision number
+> > from VCS on the main branch. That also has the advantage of
+> > producing consecutive numbers for each revision, by definition.
+>
+> But not during branch-switches, while my method skips large regions,
+> but probably increases during any reasonable branch-switch.
+
+I've read this several times now, and I don't see what it's saying.
+
+The assumption I'm making is that there is a single canonical “main
+branch”, from which releases will be made. The version number set in
+that branch is the one which determines the version of Bugs Everywhere
+as a whole.
+
+The revision number is only useful in the context of the branch, so it
+only matters when comparing versions within a branch. When you switch
+between branches, if you're interested in the revision number you'll
+still need to know which branch you're talking about.
+
+Switching between branches doesn't change the canonical version string.
+
+> For example, when I upgraded to rich root to pull Ben's patch, I'm not
+> sure if Chris upgraded the trunk and merged my branch, or just ditched
+> the trunk and cloned my branch. Using actual bzr revision numbers
+> would make switching branches that either wrong (in the case of rev-id
+> decreases) or confusing (in the case of a single non-consecutive
+> increase).
+
+This, then, is an argument for not having the revision number in the
+version string at all. The version then becomes a more traditional
+“major.minor.patch” tuple, and is only ever updated when some release
+manager of the canonical branch decides it's correct to do so.
+
+If we use the ‘bzr version-info --format=python > foo_version.py’
+command in some build routine, the branch's revision number will be
+available directly within Python by importing that module. That would
+allow it to be output in some UI, if that's what you're interested in
+seeing.
+
+--
+ \ “Never do anything against conscience even if the state demands |
+ `\ it.” —Albert Einstein |
+_o__) |
+Ben Finney
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/values
new file mode 100644
index 0000000..3a42917
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5/values
@@ -0,0 +1,14 @@
+Alt-id: <87d481ht1s.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 16 Jul 2009 19:32:31 +1000
+
+
+In-reply-to: cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/body
new file mode 100644
index 0000000..30e3cbd
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/body
@@ -0,0 +1,44 @@
+"W. Trevor King" <wking@drexel.edu> writes:
+
+> On Tue, Jul 14, 2009 at 10:36:26PM +1000, Ben Finney wrote:
+> > Please, no. Timestamps aren't version strings, that's conflating two
+> > pieces of information with very different meanings. Correlating the
+> > two is the job of a changelog.
+>
+> Which we don't bother keeping (also NEWS), since "bzr log" works so
+> nicely.
+
+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.
+
+> The timestamp should at least replace the patch release number, which
+> you agree is-desirable-to increase motonically ;).
+
+I still disagree that a timestamp is the right thing to use there. If
+you want a monotonically-increasing indicator of which revision we're up
+to, that's immediately available with the revision number from VCS on
+the main branch. That also has the advantage of producing consecutive
+numbers for each revision, by definition.
+
+> One problem is that we don't actually have "releases". People just
+> clone a branch, install, and go.
+
+I agree that's a problem. I think the solution is to start making
+releases, with specific version strings, as source tarballs.
+
+James Rowe <jnrowe@gmail.com> writes:
+
+> Isn't there a bzr web interface that at least supports creating
+> tarballs/zips?
+
+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.
+
+--
+ \ “Pinky, are you pondering what I'm pondering?” “Well, I think |
+ `\ so (hiccup), but Kevin Costner with an English accent?” —_Pinky |
+_o__) and The Brain_ |
+Ben Finney
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/values
new file mode 100644
index 0000000..56bef0b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42/values
@@ -0,0 +1,14 @@
+Alt-id: <87k52bjoxe.fsf_-_@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Wed, 15 Jul 2009 00:54:05 +1000
+
+
+In-reply-to: cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values
new file mode 100644
index 0000000..89203d2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: wishlist
+
+
+status: fixed
+
+
+summary: How should we version BE?
+
+
+time: Tue, 21 Jul 2009 17:19:22 +0000
+
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
index 0000000..8596c92
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body
@@ -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
index 0000000..4bd8f81
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values
@@ -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
index 0000000..d3d9d0c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body
@@ -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
index 0000000..e77ec55
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values
@@ -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
index 0000000..2d546cb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/values
@@ -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/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/body
new file mode 100644
index 0000000..fd86659
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/body
@@ -0,0 +1 @@
+<html><head></head><body>Hello world</body></html>
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/values
new file mode 100644
index 0000000..f460840
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/html
+
+
+Date: Mon, 22 Jun 2009 20:05:00 +0000
+
+
+In-reply-to: c454aa67-ca30-43e8-9be4-58cbddd01b63
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/body
new file mode 100644
index 0000000..f673cc5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/body
@@ -0,0 +1,30 @@
+Excerpt from my mail to the list on Sat, 20 Jun 2009 21:55:54 -0400:
+
+On Mon, Nov 24, 2008 at 07:15:08PM -0500, Aaron Bentley wrote:
+> 576:om: Allow attachments
+> Sensible.
+
+I'm not as convinced they are a good idea as I once was. I've just
+added comments-from-stdin, e.g.
+ some-invalid-command | be comment <bug-id> -
+Which is mostly what I'd be using attachments for anyway. If you
+really want to support the attachments/mime-types etc. like we had
+maybe been leaning towards before, you'd need to look at the output of
+`be show ...' with an email client, which seems a bit excessive. Do
+we even want mime types at all? With the xml output a la Thomas, you
+should be able to pipe into whatever sort of `viewer' you want, and it
+doesn't end up being hardcoded into the main repo.
+
+
+Notes since my email:
+
+be->xml->mutt has since been implemented, and it preserves comment
+mime-type. This allows those that want to go crazy to attach whatever
+they want to their comments:
+
+ $ echo "<html><head></head><body>Hello world</body></html>" | be comment --content-type text/html 576:2 -
+
+I think non-text attachments without a browser/mail-viewer don't make
+sense, so I'm closing this bug. Feel free to keep it open in your own
+repo, or argue with me on the list ;).
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/values
new file mode 100644
index 0000000..0c7cd6c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 20:03:27 +0000
+
+
+In-reply-to: d83a5436-85e3-42c7-9a89-a6d50df9d279
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/body
new file mode 100644
index 0000000..118afc8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/body
@@ -0,0 +1,4 @@
+I've added comments-from-stdin, so we can add tracebacks, e.g. with
+
+ $ be list --invalid-option | be comment <bug-id> -
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/values
new file mode 100644
index 0000000..7c8dc1c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 19 Jun 2009 20:22:19 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/values
new file mode 100644
index 0000000..16906f1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Allow attachments
+
+
+time: Wed, 25 Jan 2006 15:43:46 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/1b21dabc-a90c-4687-bea0-7e9e69956e23/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/1b21dabc-a90c-4687-bea0-7e9e69956e23/body
new file mode 100644
index 0000000..7485ebd
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/1b21dabc-a90c-4687-bea0-7e9e69956e23/body
@@ -0,0 +1,3 @@
+> libbe.mapfile.IllegalValue: Illegal value "Foo Bar <foo@example.com>\n"
+
+The trailing endline was the problem. Fixed now.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/1b21dabc-a90c-4687-bea0-7e9e69956e23/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/1b21dabc-a90c-4687-bea0-7e9e69956e23/values
new file mode 100644
index 0000000..6275e96
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/1b21dabc-a90c-4687-bea0-7e9e69956e23/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Wed, 19 May 2010 11:16:23 +0000
+
+
+In-reply-to: 285006ba-16fc-4d09-86f1-893ff515e487
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/285006ba-16fc-4d09-86f1-893ff515e487/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/285006ba-16fc-4d09-86f1-893ff515e487/body
new file mode 100644
index 0000000..8aa34de
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/285006ba-16fc-4d09-86f1-893ff515e487/body
@@ -0,0 +1,4 @@
+If I have just "foo@example.com" in _darcs/prefs/author, be is perfectly happy.
+
+But having instead "Foo Bar <foo@example.com>" causes this error on be new:
+libbe.mapfile.IllegalValue: Illegal value "Foo Bar <foo@example.com>\n"
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/285006ba-16fc-4d09-86f1-893ff515e487/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/285006ba-16fc-4d09-86f1-893ff515e487/values
new file mode 100644
index 0000000..012e25d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/comments/285006ba-16fc-4d09-86f1-893ff515e487/values
@@ -0,0 +1,8 @@
+Author: Eric Kow <eric.kow@gmail.com>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 29 Mar 2010 15:52:53 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/values
new file mode 100644
index 0000000..de23989
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5920ef40-ce56-44e0-9e2d-e9b888ab2880/values
@@ -0,0 +1,17 @@
+creator: Eric Kow <eric.kow@gmail.com>
+
+
+reporter: Eric Kow <eric.kow@gmail.com>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Fancy _darcs/prefs/author contents confuse be
+
+
+time: Mon, 29 Mar 2010 15:50:39 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body
new file mode 100644
index 0000000..bd80264
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body
@@ -0,0 +1 @@
+Merged into bug 09f84059-fc8e-4954-b24d-a2b33ef21bf4 \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values
new file mode 100644
index 0000000..929dce6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 13:35:42 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body
new file mode 100644
index 0000000..9106d37
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body
@@ -0,0 +1,7 @@
+This is an *rst* comment.
+Which means newlines don't matter, except when they gang up.
+
+lala
+
+ - Bullet
+ - Bullet \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values
new file mode 100644
index 0000000..54570b2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/restructured
+
+
+Date: Thu, 06 Apr 2006 16:54:57 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/values
new file mode 100644
index 0000000..b0bfcf3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: RST test
+
+
+time: Thu, 06 Apr 2006 16:52:46 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/0922b8f7-b5ce-4572-8e55-c4b34dafe6cf/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/0922b8f7-b5ce-4572-8e55-c4b34dafe6cf/body
new file mode 100644
index 0000000..991501f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/0922b8f7-b5ce-4572-8e55-c4b34dafe6cf/body
@@ -0,0 +1,9 @@
+Hello!
+
+--- 8< ---
+$ be comment b35
+ERROR:
+No comment supplied, and EDITOR not specified.
+--- >8 ---
+
+When EDITOR enviroment variable not set try to use vi editor as default editor
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/0922b8f7-b5ce-4572-8e55-c4b34dafe6cf/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/0922b8f7-b5ce-4572-8e55-c4b34dafe6cf/values
new file mode 100644
index 0000000..d6047ac
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/0922b8f7-b5ce-4572-8e55-c4b34dafe6cf/values
@@ -0,0 +1,11 @@
+Alt-id: <201003161610.10674.antonbatenev@yandex.ru>
+
+
+Author: Anton Batenev <antonbatenev@yandex.ru>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 16 Mar 2010 20:10:10 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/212570d2-8116-462e-9664-9ea8d3976b99/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/212570d2-8116-462e-9664-9ea8d3976b99/body
new file mode 100644
index 0000000..efebc29
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/212570d2-8116-462e-9664-9ea8d3976b99/body
@@ -0,0 +1 @@
+I agree with Ben. Setting status to "wontfix".
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/212570d2-8116-462e-9664-9ea8d3976b99/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/212570d2-8116-462e-9664-9ea8d3976b99/values
new file mode 100644
index 0000000..6abf782
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/212570d2-8116-462e-9664-9ea8d3976b99/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 19 Mar 2010 06:17:38 +0000
+
+
+In-reply-to: 5e8d490e-ee06-4403-96dd-ad8eac66b21c
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/5e8d490e-ee06-4403-96dd-ad8eac66b21c/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/5e8d490e-ee06-4403-96dd-ad8eac66b21c/body
new file mode 100644
index 0000000..92264a7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/5e8d490e-ee06-4403-96dd-ad8eac66b21c/body
@@ -0,0 +1,17 @@
+Anton Batenev <antonbatenev@yandex.ru> writes:
+
+> --- 8< ---
+> $ be comment b35
+> ERROR:
+> No comment supplied, and EDITOR not specified.
+> --- >8 ---
+>
+> When EDITOR enviroment variable not set try to use vi editor as
+> default editor
+
+-1. It's up to the local operating system configuration to set a default
+EDITOR value, and up to the user to over-ride that if they want to. An
+application shouldn't be guessing in the absence of those conventions.
+
+The behaviour is clear and the message provides a way for the user to
+rectify the problem in a standard way. It doesn't need to change.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/5e8d490e-ee06-4403-96dd-ad8eac66b21c/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/5e8d490e-ee06-4403-96dd-ad8eac66b21c/values
new file mode 100644
index 0000000..17ed58f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/comments/5e8d490e-ee06-4403-96dd-ad8eac66b21c/values
@@ -0,0 +1,14 @@
+Alt-id: <877hp9w0zy.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 19 Mar 2010 16:31:29 +0000
+
+
+In-reply-to: <201003161610.10674.antonbatenev@yandex.ru>
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/values
new file mode 100644
index 0000000..79ca1b1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597b03f5-76cb-4951-b370-a01573ad2f75/values
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: Anton Batenev <abbat@abbat>
+
+
+severity: minor
+
+
+status: wontfix
+
+
+summary: Set a default EDITOR incase the user has not
+
+
+time: Fri, 19 Mar 2010 06:11:12 +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
index 0000000..40d9e29
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/628a050a-f969-4290-8468-f5e991528f40/body
@@ -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
index 0000000..decd72f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/628a050a-f969-4290-8468-f5e991528f40/values
@@ -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
index 0000000..80133bf
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/f3e90a7e-b8c4-4a7c-8609-6a783ae59762/body
@@ -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
index 0000000..4cd8b7a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/comments/f3e90a7e-b8c4-4a7c-8609-6a783ae59762/values
@@ -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
index 0000000..3e88ad1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/5fb11e65-68a0-4015-b404-737238299cdc/values
@@ -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/62a74b85-0d4b-49f5-8794-74bafd871cd4/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/62a74b85-0d4b-49f5-8794-74bafd871cd4/values
new file mode 100644
index 0000000..9f7616b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/62a74b85-0d4b-49f5-8794-74bafd871cd4/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Indicate presence of Comments
+
+
+time: Wed, 25 Jan 2006 15:18:58 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/65776f00-34d8-4b58-874d-333196a5e245/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/65776f00-34d8-4b58-874d-333196a5e245/values
new file mode 100644
index 0000000..435c733
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/65776f00-34d8-4b58-874d-333196a5e245/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Select severity filter
+
+
+time: Wed, 04 Jan 2006 21:07:08 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6622c06a-ed84-4d45-8011-a082fca219b6/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6622c06a-ed84-4d45-8011-a082fca219b6/values
new file mode 100644
index 0000000..ba3304f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6622c06a-ed84-4d45-8011-a082fca219b6/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Better word wrapping in comments. (kill <pre>.)
+
+
+time: Mon, 30 Jan 2006 20:03:25 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body
new file mode 100644
index 0000000..dd464bf
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body
@@ -0,0 +1,3 @@
+Per-tree severity and target are now supported.
+
+I'm not sure what Aaron meant be "BE ids".
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values
new file mode 100644
index 0000000..9f4fd8c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:29:30 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values
new file mode 100644
index 0000000..b8e8291
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: wishlist
+
+
+status: closed
+
+
+summary: Support per-tree settings for severity, target, BE ids
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/body
new file mode 100644
index 0000000..30f02d7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/body
@@ -0,0 +1 @@
+ On a new bug, if I add comment then click "Update", the Summary goes missing! \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values
new file mode 100644
index 0000000..a1e25ea
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Fri, 27 Jan 2006 14:30:26 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/values
new file mode 100644
index 0000000..fcc0b22
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/values
@@ -0,0 +1,15 @@
+creator: abentley
+
+
+severity: serious
+
+
+status: closed
+
+
+summary: On a new bug, if I add or edit a comment, then click "Update", the Summary
+ goes missing.
+
+
+time: Fri, 27 Jan 2006 14:29:51 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values
new file mode 100644
index 0000000..9bd84b0
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: serious
+
+
+status: fixed
+
+
+summary: implement comments
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values
new file mode 100644
index 0000000..473f9a7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Can't close bugs
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/764b812f-a0bb-4f4d-8e2f-c255c9474a0e/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/764b812f-a0bb-4f4d-8e2f-c255c9474a0e/values
new file mode 100644
index 0000000..4406356
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/764b812f-a0bb-4f4d-8e2f-c255c9474a0e/values
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Add docstrings explaining role of the libbe submodules.
+
+
+time: Mon, 31 Aug 2009 13:57:54 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/body
new file mode 100644
index 0000000..c006a1a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/body
@@ -0,0 +1 @@
+The commands that can easily be tested are now being tested
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values
new file mode 100644
index 0000000..ead68e1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Thu, 24 Mar 2005 17:04:47 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/body
new file mode 100644
index 0000000..e0b86a5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/body
@@ -0,0 +1,4 @@
+We've got coverage of set_root and new. This leaves
+close, comment, list, open, severity, show, target, upgrade
+
+It's quite nice, though, that doctest captures stdout.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values
new file mode 100644
index 0000000..90beac0
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Thu, 24 Mar 2005 13:05:13 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/values
new file mode 100644
index 0000000..e2a930c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: serious
+
+
+status: fixed
+
+
+summary: Add test cases
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/body
new file mode 100644
index 0000000..c602969
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/body
@@ -0,0 +1 @@
+Fixed at least by commit 273, probably way before.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values
new file mode 100644
index 0000000..2d5059c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 24 Nov 2008 13:08:07 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/body
new file mode 100644
index 0000000..89160a2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/body
@@ -0,0 +1 @@
+It's hard to report bugs if you can't even get it working. \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values
new file mode 100644
index 0000000..dcdd529
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Wed, 21 Dec 2005 21:53:47 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/values
new file mode 100644
index 0000000..d05eed6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: no tests for --help, -h, help, etc.
+
+
+time: Tue, 17 May 2005 13:27:18 +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
index 0000000..c45d2c7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/comments/a555d577-7f8c-49f2-96f6-263ce5fdff8e/body
@@ -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
index 0000000..3b8ba33
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/comments/a555d577-7f8c-49f2-96f6-263ce5fdff8e/values
@@ -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
index 0000000..cb9a372
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7cb42a60-c977-40db-b2a1-19917c10cace/values
@@ -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/7d182ab9-9c0c-4b4f-885e-c5762d7a2437/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7d182ab9-9c0c-4b4f-885e-c5762d7a2437/values
new file mode 100644
index 0000000..ae215bc
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7d182ab9-9c0c-4b4f-885e-c5762d7a2437/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: BEweb fails to set bug creation date
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/body
new file mode 100644
index 0000000..6ad8230
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/body
@@ -0,0 +1,39 @@
+In my Tue, 25 Nov 2008 08:30:19 -0500 email:
+
+Implemented as a free-form value field similar to target? A
+comma-seperated list of tags? Perhaps once we have per-bug/comment
+attribute searching it would be easier to have a 'create-attribute'
+becommand, e.g.
+ be create-attribute [-valid=X,Y,Z] [bugdir|bug|comment] [NAME] [DEFAULT]
+
+We could ship some suggested configuration scripts to set people up,
+and keep the core code more general/flexible.
+
+
+Plan:
+
+Extend and make more consitent the settings_property() attributes.
+Create becommand/(create/remove)-attribute for logic-less attributes.
+Create a few mix-ins for logic-ed attributes
+
+Usage example:
+ Goal:
+ set up for `be depends BUGA BUGB`, `be depends --tree BUGA`, etc
+ Procedure:
+ be set --apend mixins bug:dependency
+ Where we've defined
+ becommands/depends.py, but it is hidden until the mixin is activated
+ libbe/mixins/bug/dependency.Mixin (inheriting from BugMixin)
+ to
+ parse/generate comma seperated dependency uuids for saving/loading
+ pretty-print the dependency list (e.g. uuid->shortname)
+ walk the dependency tree and check target bug status.
+
+With more complicated mixins, there could be inter-mixin dependencies,
+e.g. a dependency tracker that searches depends based on bug.status
+might depend on the base dependency mixin. This way people who need
+it could make rich interfaces without confusing the people who don't.
+
+How does that sound?
+
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/values
new file mode 100644
index 0000000..c054670
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 20:39:39 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/body
new file mode 100644
index 0000000..33638ab
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/body
@@ -0,0 +1,5 @@
+It's tricky to say whether we should have dependencies or reverse dependencies
+or both.
+
+In the case where a bug is removed, normal dependencies mean that its
+dependencies are erased from this system.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/values
new file mode 100644
index 0000000..32e49e7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/6010e186-0260-44e5-8442-8df2269910ce/values
@@ -0,0 +1,11 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Mon, 17 Apr 2006 20:59:15 +0000
+
+
+In-reply-to: f87fd684-6af1-498d-98d5-f915bcee76a9
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/body
new file mode 100644
index 0000000..f1ce046
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/body
@@ -0,0 +1,2 @@
+Arbitrary tagging now supported via `be tag'.
+Dependencies supported via `be depend'.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/values
new file mode 100644
index 0000000..d2f0f5c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 25 Jun 2009 12:39:26 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/body
new file mode 100644
index 0000000..57439b7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/body
@@ -0,0 +1,47 @@
+From my Tue, 25 Nov 2008 13:27:12 -0500 email:
+
+> >> 7ec:om: Arbitrary tags
+> >> Sensible
+> >
+> > Implemented as a free-form value field similar to target? A
+> > comma-seperated list of tags?
+>
+
+That is a much better format than my unmergable one ;).
+
+> "append" usually has two "p"s. Is the omission deliberate?
+
+Nope, sorry :p
+
+> It sounds pretty complicated. I would probably use a type system rather
+> than "mixins", and define types as "scalar", "set" and maybe "list" and
+> "map". Dependencies would be a set, and their special behaviour would
+> be hardcoded according to their name, not a property of their type.
+
+Ok. I'm just worried about bloat. It's pretty easy to move things
+around at the moment, but I'm worried that adding lots of attributes
+with special code will start a slippery slope of trying to satisfy
+everybody internally. Then things start looking more like Arch, with
+newbies scared off by the confusion. I know the Arch people like the
+power, but it took me several hours to figure out how to create a
+repository ;). Some people like bug dependencies, and some do not
+ e.g.
+ https://bugs.launchpad.net/malone/+bug/95419
+ http://trac.edgewall.org/ticket/31
+
+From the *long* Trac post, you can see that this is divisive issue.
+
+I would be in favor of emulating TracCrossReferences
+(http://trac.edgewall.org/wiki/TracCrossReferences) in our core. We
+could have references and backlinks fields for bugs (and comments?).
+But I'd rather not add blocking, etc. However, having a seperate
+plugin obviously doesn't work for some people ;). We'd like to bundle
+lots of functionality, but keep the core fairly clean and flexible.
+
+Therefore, I'd like a way to put non-core implememtation code in a
+seperate submod. We already call our libbe code "plugins", and we're
+extending the builtin BugDir, Bug, etc code, so I thought we'd call
+the non-core submods mixins (see http://en.wikipedia.org/wiki/Mixin).
+
+Anyhow, just my 2c.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/values
new file mode 100644
index 0000000..8874446
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 20:42:12 +0000
+
+
+In-reply-to: ec133a4e-c9ff-4499-b469-cb0a2ca9a685
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body
new file mode 100644
index 0000000..4ac7a33
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/body
@@ -0,0 +1,3 @@
+This could be implemented with an external frontend storing the
+dependency data in arbitrary tags.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values
new file mode 100644
index 0000000..fe86bd4
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 21:29:13 +0000
+
+
+In-reply-to: f87fd684-6af1-498d-98d5-f915bcee76a9
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/body
new file mode 100644
index 0000000..a06e236
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/body
@@ -0,0 +1,55 @@
+In Aaron's Tue, 25 Nov 2008 09:32:29 -0500 email:
+
+>> 7ec:om: Arbitrary tags
+>> Sensible
+>
+> Implemented as a free-form value field similar to target? A
+> comma-seperated list of tags?
+
+I believe I planned to store it as an alpha-sorted, one-entry-per-line
+list, so it would support merging easily.
+
+> Perhaps once we have per-bug/comment
+> attribute searching it would be easier to have a 'create-attribute'
+> becommand, e.g.
+> be create-attribute [-valid=X,Y,Z] [bugdir|bug|comment] [NAME] [DEFAULT]
+
+Well, it really depends how much semantics you want to embed in the data
+format. Some values are scalars, some may be sets (i.e. tags), some may
+be ordered lists or even mappings. How much you want to reflect that in
+the data format is up to you.
+
+> Extend and make more consitent the settings_property() attributes.
+> Create becommand/(create/remove)-attribute for logic-less attributes.
+> Create a few mix-ins for logic-ed attributes
+
+I don't find the term mix-in very intuitive here.
+
+> Usage example:
+> Goal:
+> set up for `be depends BUGA BUGB`, `be depends --tree BUGA`, etc
+> Procedure:
+> be set --apend mixins bug:dependency
+
+"append" usually has two "p"s. Is the omission deliberate?
+
+> Where we've defined
+> becommands/depends.py, but it is hidden until the mixin is activated
+> libbe/mixins/bug/dependency.Mixin (inheriting from BugMixin)
+> to
+> parse/generate comma seperated dependency uuids for saving/loading
+> pretty-print the dependency list (e.g. uuid->shortname)
+> walk the dependency tree and check target bug status.
+>
+> With more complicated mixins, there could be inter-mixin dependencies,
+> e.g. a dependency tracker that searches depends based on bug.status
+> might depend on the base dependency mixin. This way people who need
+> it could make rich interfaces without confusing the people who don't.
+>
+> How does that sound?
+
+It sounds pretty complicated. I would probably use a type system rather
+than "mixins", and define types as "scalar", "set" and maybe "list" and
+"map". Dependencies would be a set, and their special behaviour would
+be hardcoded according to their name, not a property of their type.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/values
new file mode 100644
index 0000000..c85b16f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 20:40:54 +0000
+
+
+In-reply-to: 401950a0-a5ff-46f3-afac-a9cfb300f94b
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/body
new file mode 100644
index 0000000..a28cfe4
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/body
@@ -0,0 +1 @@
+Merged from bug 17921fbc-e7f0-4f31-8cdd-598e5ba7237b \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/values
new file mode 100644
index 0000000..2b6307e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 21:29:32 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/values
new file mode 100644
index 0000000..1059e1b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Arbitrary tags
+
+
+time: Wed, 04 Jan 2006 21:06:38 +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
index 0000000..6e3f1e7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/17d045d1-3b21-4d3d-8f81-29a5bbc5e6c1/body
@@ -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
index 0000000..5bc6769
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/17d045d1-3b21-4d3d-8f81-29a5bbc5e6c1/values
@@ -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
index 0000000..b953536
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/d463e2d9-6dcc-41a4-a6b2-647fb3bddf88/body
@@ -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
index 0000000..cb7278c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/comments/d463e2d9-6dcc-41a4-a6b2-647fb3bddf88/values
@@ -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
index 0000000..5feb832
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/814e39c0-68ee-4165-9166-19e2aee9c07d/values
@@ -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/8385a1fb-63df-4ca6-81cd-28ede83bb0c2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8385a1fb-63df-4ca6-81cd-28ede83bb0c2/values
new file mode 100644
index 0000000..5d80e70
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8385a1fb-63df-4ca6-81cd-28ede83bb0c2/values
@@ -0,0 +1,17 @@
+creator: gianluca <gian@galactica>
+
+
+reporter: gianluca <gian@galactica>
+
+
+severity: minor
+
+
+status: wontfix
+
+
+summary: Add the html files for the status detail to "be html" output
+
+
+time: Fri, 03 Jul 2009 22:56:09 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body
new file mode 100644
index 0000000..8d1ec26
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body
@@ -0,0 +1 @@
+A rough implemention is now sketched out in becommands/list.py
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values
new file mode 100644
index 0000000..924ca86
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 27 Nov 2008 14:26:18 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body
new file mode 100644
index 0000000..bb443b8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body
@@ -0,0 +1,15 @@
+For example:
+ $ be list --status --options
+ File "/home/wking/bin/be", line 35, in <module>
+ sys.exit(cmdutil.execute(sys.argv[1], sys.argv[2:]))
+ File "/home/wking/lib/python2.5/site-packages/libbe/cmdutil.py", line 67, in execute
+ get_command(cmd).execute([a.decode(enc) for a in args])
+ File "/home/wking/lib/python2.5/site-packages/becommands/list.py", line 36, in execute
+ raise Exception, "parsed options"
+ Exception: parsed options
+
+The reason for this is that --status takes an argument, so 'be list'
+thinks it should list all the bugs with status == "--options".
+Ideally what should happen is that an argument-taking option would
+check for argument --options, and if so, would raise an exception
+returning a list of appropriate completions *for that argument*.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values
new file mode 100644
index 0000000..fb952c2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 27 Nov 2008 13:43:47 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values
new file mode 100644
index 0000000..73915d5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values
@@ -0,0 +1,14 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+severity: serious
+
+
+status: fixed
+
+
+summary: be <cmmd> <argopt> --options doesn't raise GetOptions
+
+
+time: Thu, 27 Nov 2008 13:39:25 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/body
new file mode 100644
index 0000000..d10b444
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/body
@@ -0,0 +1,38 @@
+File "/home/wking/src/fun/be-bugfix/becommands/status.py", line 25, in becommands.status.execute
+Failed example:
+ bd = bugdir.simple_bug_dir()
+Exception raised:
+ Traceback (most recent call last):
+ File "/usr/lib/python2.5/doctest.py", line 1228, in __run
+ compileflags, 1) in test.globs
+ File "<doctest becommands.status.execute[1]>", line 1, in <module>
+ bd = bugdir.simple_bug_dir()
+ File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 293, in simple_bug_dir
+ bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True)
+ File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 99, in __init__
+ rcs = self.guess_rcs(allow_rcs_init)
+ File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 165, in guess_rcs
+ rcs = installed_rcs()
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 53, in installed_rcs
+ return _get_matching_rcs(lambda rcs: rcs.installed())
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 37, in _get_matching_rcs
+ if matchfn(rcs) == True:
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 53, in <lambda>
+ return _get_matching_rcs(lambda rcs: rcs.installed())
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 180, in installed
+ self._rcs_help()
+ File "/home/wking/src/fun/be-bugfix/libbe/bzr.py", line 32, in _rcs_help
+ status,output,error = self._u_invoke_client("--help")
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 362, in _u_invoke_client
+ return self._u_invoke(cl_args, expect, cwd=directory)
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 355, in _u_invoke
+ raise CommandError(error, status)
+ CommandError: Command failed (1): 'import site' failed; use -v for traceback
+ bzr: ERROR: Couldn't import bzrlib and dependencies.
+ Please check bzrlib is on your PYTHONPATH.
+
+ Traceback (most recent call last):
+ File "/usr/bin/bzr", line 64, in <module>
+ import bzrlib
+ ImportError: No module named bzrlib
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values
new file mode 100644
index 0000000..3453fd9
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 21 Nov 2008 18:41:47 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/body
new file mode 100644
index 0000000..3d7d3aa
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/body
@@ -0,0 +1,2 @@
+Aha, a final os.chdir('/') line is required to clean up after the
+set_root.py doctest.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values
new file mode 100644
index 0000000..2a76a4e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 21 Nov 2008 19:12:42 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/body
new file mode 100644
index 0000000..1fe5ce3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/body
@@ -0,0 +1,170 @@
+Hysteretic! test.py severity passes, then fails.
+
+Problem caused somewhere in set_root? Doctest? Bzr?
+
+libbe/plugin.py adds the BE-path to sys.path, but it is done by the
+time the TestRunner fires up... Wierd.
+
+$ python test.py severity set_root severity
+Doctest: becommands.severity.execute ... ok
+Doctest: becommands.set_root.execute ... FAIL
+Doctest: becommands.severity.execute ... FAIL
+
+======================================================================
+FAIL: Doctest: becommands.set_root.execute
+----------------------------------------------------------------------
+Traceback (most recent call last):
+ File "/usr/lib/python2.5/doctest.py", line 2128, in runTest
+ raise self.failureException(self.format_failure(new.getvalue()))
+AssertionError: Failed doctest test for becommands.set_root.execute
+ File "/home/wking/src/fun/be-bugfix/becommands/set_root.py", line 22, in execute
+
+----------------------------------------------------------------------
+File "/home/wking/src/fun/be-bugfix/becommands/set_root.py", line 41, in becommands.set_root.execute
+Failed example:
+ print rcs.name
+Expected:
+ Arch
+Got:
+ bzr
+----------------------------------------------------------------------
+File "/home/wking/src/fun/be-bugfix/becommands/set_root.py", line 43, in becommands.set_root.execute
+Failed example:
+ execute([])
+Expected:
+ Using Arch for revision control.
+ Directory initialized.
+Got:
+ Using bzr for revision control.
+ Directory initialized.
+
+
+======================================================================
+FAIL: Doctest: becommands.severity.execute
+----------------------------------------------------------------------
+Traceback (most recent call last):
+ File "/usr/lib/python2.5/doctest.py", line 2128, in runTest
+ raise self.failureException(self.format_failure(new.getvalue()))
+AssertionError: Failed doctest test for becommands.severity.execute
+ File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 22, in execute
+
+----------------------------------------------------------------------
+File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 25, in becommands.severity.execute
+Failed example:
+ bd = bugdir.simple_bug_dir()
+Exception raised:
+ Traceback (most recent call last):
+ File "/usr/lib/python2.5/doctest.py", line 1228, in __run
+ compileflags, 1) in test.globs
+ File "<doctest becommands.severity.execute[1]>", line 1, in <module>
+ bd = bugdir.simple_bug_dir()
+ File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 293, in simple_bug_dir
+ bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True)
+ File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 99, in __init__
+ rcs = self.guess_rcs(allow_rcs_init)
+ File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 165, in guess_rcs
+ rcs = installed_rcs()
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 53, in installed_rcs
+ return _get_matching_rcs(lambda rcs: rcs.installed())
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 37, in _get_matching_rcs
+ if matchfn(rcs) == True:
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 53, in <lambda>
+ return _get_matching_rcs(lambda rcs: rcs.installed())
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 180, in installed
+ self._rcs_help()
+ File "/home/wking/src/fun/be-bugfix/libbe/bzr.py", line 32, in _rcs_help
+ status,output,error = self._u_invoke_client("--help")
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 362, in _u_invoke_client
+ return self._u_invoke(cl_args, expect, cwd=directory)
+ File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 355, in _u_invoke
+ raise CommandError(error, status)
+ CommandError: Command failed (1): 'import site' failed; use -v for traceback
+ bzr: ERROR: Couldn't import bzrlib and dependencies.
+ Please check bzrlib is on your PYTHONPATH.
+
+ Traceback (most recent call last):
+ File "/usr/bin/bzr", line 64, in <module>
+ import bzrlib
+ ImportError: No module named bzrlib
+
+----------------------------------------------------------------------
+File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 26, in becommands.severity.execute
+Failed example:
+ os.chdir(bd.root)
+Exception raised:
+ Traceback (most recent call last):
+ File "/usr/lib/python2.5/doctest.py", line 1228, in __run
+ compileflags, 1) in test.globs
+ File "<doctest becommands.severity.execute[2]>", line 1, in <module>
+ os.chdir(bd.root)
+ NameError: name 'bd' is not defined
+----------------------------------------------------------------------
+File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 27, in becommands.severity.execute
+Failed example:
+ execute(["a"])
+Exception raised:
+ Traceback (most recent call last):
+ File "/usr/lib/python2.5/doctest.py", line 1228, in __run
+ compileflags, 1) in test.globs
+ File "<doctest becommands.severity.execute[3]>", line 1, in <module>
+ execute(["a"])
+ File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 40, in execute
+ bd = bugdir.BugDir(loadNow=True)
+ File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 85, in __init__
+ root = os.getcwd()
+ OSError: [Errno 2] No such file or directory
+----------------------------------------------------------------------
+File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 29, in becommands.severity.execute
+Failed example:
+ execute(["a", "wishlist"])
+Exception raised:
+ Traceback (most recent call last):
+ File "/usr/lib/python2.5/doctest.py", line 1228, in __run
+ compileflags, 1) in test.globs
+ File "<doctest becommands.severity.execute[4]>", line 1, in <module>
+ execute(["a", "wishlist"])
+ File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 40, in execute
+ bd = bugdir.BugDir(loadNow=True)
+ File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 85, in __init__
+ root = os.getcwd()
+ OSError: [Errno 2] No such file or directory
+----------------------------------------------------------------------
+File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 30, in becommands.severity.execute
+Failed example:
+ execute(["a"])
+Exception raised:
+ Traceback (most recent call last):
+ File "/usr/lib/python2.5/doctest.py", line 1228, in __run
+ compileflags, 1) in test.globs
+ File "<doctest becommands.severity.execute[5]>", line 1, in <module>
+ execute(["a"])
+ File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 40, in execute
+ bd = bugdir.BugDir(loadNow=True)
+ File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 85, in __init__
+ root = os.getcwd()
+ OSError: [Errno 2] No such file or directory
+----------------------------------------------------------------------
+File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 32, in becommands.severity.execute
+Failed example:
+ execute(["a", "none"])
+Expected:
+ Traceback (most recent call last):
+ UserError: Invalid severity level: none
+Got:
+ Traceback (most recent call last):
+ File "/usr/lib/python2.5/doctest.py", line 1228, in __run
+ compileflags, 1) in test.globs
+ File "<doctest becommands.severity.execute[6]>", line 1, in <module>
+ execute(["a", "none"])
+ File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 40, in execute
+ bd = bugdir.BugDir(loadNow=True)
+ File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 85, in __init__
+ root = os.getcwd()
+ OSError: [Errno 2] No such file or directory
+
+
+----------------------------------------------------------------------
+Ran 3 tests in 8.719s
+
+FAILED (failures=2)
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values
new file mode 100644
index 0000000..d63f4e1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 21 Nov 2008 19:01:19 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/values
new file mode 100644
index 0000000..93c746c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/values
@@ -0,0 +1,14 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: test.py removes path to bzrlib
+
+
+time: Fri, 21 Nov 2008 18:41:03 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/body
new file mode 100644
index 0000000..635893b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/body
@@ -0,0 +1,4 @@
+From Aaron's Mon, 24 Nov 2008 19:15:09 -0500 email:
+
+8e9:om: list X most recent entries
+Closeable. (And yes, I would do it instead of 'be diff')
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/values
new file mode 100644
index 0000000..dea0808
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 19:46:45 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/body
new file mode 100644
index 0000000..6d75610
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/body
@@ -0,0 +1 @@
+Would you do this instead of `be diff`?
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values
new file mode 100644
index 0000000..ca6c353
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 24 Nov 2008 13:10:38 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/values
new file mode 100644
index 0000000..391d092
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: list X most recent entries
+
+
+time: Wed, 25 Jan 2006 15:44:18 +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
index 0000000..359c90f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/432e994f-3759-42bf-a80d-7cd626c7ce7c/body
@@ -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
index 0000000..993dce1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/432e994f-3759-42bf-a80d-7cd626c7ce7c/values
@@ -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
index 0000000..450a208
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/e3d802cf-1fff-4a48-a61c-a07578969333/body
@@ -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
index 0000000..1c44d10
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/comments/e3d802cf-1fff-4a48-a61c-a07578969333/values
@@ -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
index 0000000..28975af
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8fc5d6fa-cae1-451f-9817-3e4da6d0aac1/values
@@ -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/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body
new file mode 100644
index 0000000..dd9b459
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body
@@ -0,0 +1,3 @@
+From the command line,
+ $ be show `be list --status all --uuids` | grep -A5 -B5 XYZ
+works pretty well...
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values
new file mode 100644
index 0000000..901c32f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 18:05:38 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/body
new file mode 100644
index 0000000..a27ff59
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/body
@@ -0,0 +1 @@
+Hmm. This is already done...
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values
new file mode 100644
index 0000000..4031ab2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Fri, 31 Mar 2006 22:15:09 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values
new file mode 100644
index 0000000..5d35985
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Provide search
+
+
+time: Wed, 25 Jan 2006 15:43:59 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9b1a0e71-4f7d-40b1-ab32-18496bf19a3f/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9b1a0e71-4f7d-40b1-ab32-18496bf19a3f/values
new file mode 100644
index 0000000..4b6bb4f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9b1a0e71-4f7d-40b1-ab32-18496bf19a3f/values
@@ -0,0 +1,17 @@
+creator: gianluca <gian@galactica>
+
+
+reporter: gianluca <gian@galactica>
+
+
+severity: minor
+
+
+status: wontfix
+
+
+summary: Add the html files for the severity detail to "be html" output
+
+
+time: Fri, 03 Jul 2009 22:56:19 +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
index 0000000..73ce487
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/comments/7cd2d475-676f-4d60-b431-c7635468e9bd/body
@@ -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
index 0000000..a774eb2
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/comments/7cd2d475-676f-4d60-b431-c7635468e9bd/values
@@ -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
index 0000000..e6a7689
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9c25fd46-5e2b-478f-8beb-01b89e27c1f2/values
@@ -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/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/body
new file mode 100644
index 0000000..bfb1037
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/body
@@ -0,0 +1,19 @@
+Presumably this would be to allow sorting of bugs by last-modified
+date instead of by creation date. With the xml output, this is no
+longer needed. For example, I view bugs in mutt with
+ $ be list | xml/be-xml-to-mbox | xml/catmutt
+and use mutt to sort the threads by last-modified, e.g. by adding
+ set sort=threads
+ set sort_aux=last-date
+to my ~/.muttrc.
+
+That being said, I could go for a user-specified sort command in
+becommands/list.py, rather than the current bug.cmp_full, since other
+mail readers may suck more than mutt ;), and even mutt might not have
+that perfect sort you desire coded into it :p. The problem is that
+while the cmp_* functions in bug are short, they are not really the
+sort of thing you'd want to type in on the command line. Perhaps we
+can just slowly accumulate a rich array of bug.cmp_* functions as
+they are requested, and allow the user to prepend their favorites to
+the default cmp_full list...
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/values
new file mode 100644
index 0000000..98f869b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 18:40:43 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/body
new file mode 100644
index 0000000..777975d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/body
@@ -0,0 +1,17 @@
+No need for RCS-expansion for the history. If the user is versioning
+their code with some RCS, they presumably know how to use the RCS to
+investigate the history already. The .be/ directory structure is not
+so complicated that it's worth much work to avoid their having to peer
+inside it by hand.
+
+In rare cases where people really do want to peer into history using
+only BE or sort by e.g. bug closing time, they could add those
+comments by hand, e.g.
+ $ echo 'bug closed' | be comment <bug> -
+ $ be close <bug>
+So the already-implemented cmp_last_modified would handle it.
+
+If you want, you could add (optional) comment-generation to the
+becommands themselves. For example becommand/merge.py already does
+this.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/values
new file mode 100644
index 0000000..cebbded
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 21:12:00 +0000
+
+
+In-reply-to: d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/body
new file mode 100644
index 0000000..5e3ef6b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/body
@@ -0,0 +1,9 @@
+User specfied sort added, along with bug.cmp_last_modified.
+
+Hmm, perhaps you don't want the last comment date, but e.g. the last
+time one of the bug attributes are changed. In that case, I suggest
+ bzr log .be/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/
+
+Maybe log(file) functionality should be incorperated into libbe/rcs...
+Perhaps accessed through a --history. I'm not sure I remember enough
+Arch to do that ;).
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/values
new file mode 100644
index 0000000..c2f0da8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 19:43:21 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/values
new file mode 100644
index 0000000..f124ccb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Add last-modified field to bugs
+
+
+time: Thu, 14 Sep 2006 18:08:53 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9daa72ee-0721-4f68-99ee-f06fec0b340e/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9daa72ee-0721-4f68-99ee-f06fec0b340e/values
new file mode 100644
index 0000000..f110a26
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9daa72ee-0721-4f68-99ee-f06fec0b340e/values
@@ -0,0 +1,14 @@
+assigned: abentley
+
+
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Organize list by whether it's assigned to the current target
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9f910ee0-ff0f-4fa3-b1e3-79a4118e48e9/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9f910ee0-ff0f-4fa3-b1e3-79a4118e48e9/values
new file mode 100644
index 0000000..d305d6c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9f910ee0-ff0f-4fa3-b1e3-79a4118e48e9/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: prevent collisions in different branches
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body
new file mode 100644
index 0000000..073f0b8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body
@@ -0,0 +1 @@
+Merged from bug c894f10f-197d-4b22-9c5b-19f394df40d4 \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values
new file mode 100644
index 0000000..1bd47bf
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 25 Nov 2008 02:24:04 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body
new file mode 100644
index 0000000..7f46872
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body
@@ -0,0 +1,17 @@
+Example:
+
+We're working happily in a versioned bugdir, and our RCS knows who we
+are. We create a temporary repository copy from a previous revision
+for diff generation. We set the RCS for the copy to "None", since we
+didn't bother initializing our normal RCS in the snapshot copy. But
+now the BugDir instantized on the copy doesn't know who we are!
+
+Solution:
+
+Track user id in the bugdir settings file. If you
+bugdir.settings["user_id"], it will be saved and loaded. When loaded,
+it will also set bugdir.user_id. If you set rcs.user_id, it will be
+returned by rcs.get_user_id(), instead of returing the output of
+rcs._rcs_get_user_id(). We should be caching the output of
+_rcs_get_user_id() anyway.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values
new file mode 100644
index 0000000..7a04fc3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 22 Nov 2008 21:43:29 +0000
+
+
+In-reply-to: 0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body
new file mode 100644
index 0000000..62c14e6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body
@@ -0,0 +1 @@
+This bug duplicates a403de79-8f39-41f2-b9ec-15053b175ee2
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values
new file mode 100644
index 0000000..0c87273
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 23 Nov 2008 12:37:57 +0000
+
+
+In-reply-to: 0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/values
new file mode 100644
index 0000000..db04837
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Beweb should support declaring username
+
+
+time: Wed, 04 Jan 2006 21:07:25 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/body
new file mode 100644
index 0000000..05022e8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/body
@@ -0,0 +1 @@
+Fixed by 273. Probably around 253.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values
new file mode 100644
index 0000000..8086b48
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 24 Nov 2008 13:05:07 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/body
new file mode 100644
index 0000000..0a004ac
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/body
@@ -0,0 +1,21 @@
+Err, perhaps we should use revision ids. Or status. Or something...
+
+
+$ be diff
+Traceback (most recent call last):
+ File "/usr/bin/be", line 55, in ?
+ sys.exit(execute(sys.argv[1], sys.argv[2:]))
+ File "/usr/lib/python2.4/site-packages/libbe/cmdutil.py", line 105, in execute return get_command(cmd).execute([a.decode(encoding) for a in args])
+ File "/usr/lib/python2.4/site-packages/becommands/diff.py", line 33, in execute
+ diff.diff_report(diff.reference_diff(tree, spec), tree)
+ File "/usr/lib/python2.4/site-packages/libbe/diff.py", line 41, in reference_diff
+ return diff(bugdir.get_reference_bugdir(spec), bugdir)
+ File "/usr/lib/python2.4/site-packages/libbe/diff.py", line 22, in diff
+ old_bug_map = old_tree.bug_map()
+ File "/usr/lib/python2.4/site-packages/libbe/bugdir.py", line 169, in bug_map
+ for bug in self.list():
+ File "/usr/lib/python2.4/site-packages/libbe/bugdir.py", line 164, in list
+ for uuid in self.list_uuids():
+ File "/usr/lib/python2.4/site-packages/libbe/bugdir.py", line 177, in list_uuids
+ for uuid in os.listdir(self.bugs_path):
+OSError: [Errno 2] No such file or directory: '/home/abentley/.bzrrevs/None/.be/bugs'
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/values
new file mode 100644
index 0000000..e94fece
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Mon, 10 Apr 2006 23:23:25 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/values
new file mode 100644
index 0000000..5de8e08
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: be diff doesn't work with bzr in directories that have no commits
+
+
+time: Mon, 10 Apr 2006 23:22:17 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a63bd76a-cd43-4f97-88ba-2323546d4572/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a63bd76a-cd43-4f97-88ba-2323546d4572/values
new file mode 100644
index 0000000..e5e6b0b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a63bd76a-cd43-4f97-88ba-2323546d4572/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: 'Beweb: New comment button should save any changes'
+
+
+time: Wed, 04 Jan 2006 21:05:20 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body
new file mode 100644
index 0000000..6c46db0
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body
@@ -0,0 +1 @@
+Merged from bug 4a4609c8-1882-47de-9d30-fee410b8a802 \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values
new file mode 100644
index 0000000..8fcd9d4
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:05:49 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body
new file mode 100644
index 0000000..d0b8404
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body
@@ -0,0 +1 @@
+Implemented.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values
new file mode 100644
index 0000000..3033bc1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 15:42:07 +0000
+
+
+In-reply-to: 2628eeca-96c6-4933-8484-d55bb1dbf985
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body
new file mode 100644
index 0000000..f7659c3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body
@@ -0,0 +1 @@
+Per-tree severity and status levels are now supported.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values
new file mode 100644
index 0000000..4a24d7e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:07:25 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values
new file mode 100644
index 0000000..2f65fbc
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Customizable severity levels?
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/body
new file mode 100644
index 0000000..0dedcdd
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/body
@@ -0,0 +1,3 @@
+The 'be' command should have a Unix manpage, describing it like any
+other command on the system.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/values
new file mode 100644
index 0000000..251453e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352/values
@@ -0,0 +1,8 @@
+Author: benf
+
+
+Content-type: text/plain
+
+
+Date: Mon, 21 Apr 2008 03:24:11 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values
new file mode 100644
index 0000000..4dc6a1e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values
@@ -0,0 +1,17 @@
+assigned: benf
+
+
+creator: benf
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Manual page for 'be' command
+
+
+time: Mon, 21 Apr 2008 03:21:35 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b1bc6f39-8166-46c5-a724-4c4a3e1e7d74/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b1bc6f39-8166-46c5-a724-4c4a3e1e7d74/values
new file mode 100644
index 0000000..eda617b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b1bc6f39-8166-46c5-a724-4c4a3e1e7d74/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Comments do not appear in web UI
+
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
index 0000000..3195bea
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/2a51d90a-d47e-4a67-abe7-cce19c1eafad/body
@@ -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
index 0000000..c79c578
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/2a51d90a-d47e-4a67-abe7-cce19c1eafad/values
@@ -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
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/body
new file mode 100644
index 0000000..2a05cbd
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/body
@@ -0,0 +1,40 @@
+When I try to create bug with utf8 string I get error:
+
+$ be new 'utf8 string'
+Traceback (most recent call last):
+ File "/usr/bin/be", line 64, in <module>
+ sys.exit(cmdutil.execute(args[0], args[1:]))
+ File "/usr/lib/python2.6/site-packages/libbe/cmdutil.py", line 82, in execute
+ manipulate_encodings=manipulate_encodings)
+ File "/usr/lib/python2.6/site-packages/becommands/new.py", line 54, in execute
+ bug = bd.new_bug(summary=summary.strip())
+ File "/usr/lib/python2.6/site-packages/libbe/bugdir.py", line 584, in new_bug
+ bg.save()
+ File "/usr/lib/python2.6/site-packages/libbe/bug.py", line 388, in save
+ self.save_settings()
+ File "/usr/lib/python2.6/site-packages/libbe/bug.py", line 373, in save_settings
+ mapfile.map_save(self.vcs, path, self._get_saved_settings())
+ File "/usr/lib/python2.6/site-packages/libbe/mapfile.py", line 110, in map_save
+ vcs.set_file_contents(path, contents, allow_no_vcs)
+ File "/usr/lib/python2.6/site-packages/libbe/vcs.py", line 354, in set_file_contents
+ f.write(contents)
+ File "/usr/lib/python2.6/codecs.py", line 686, in write
+ return self.writer.write(data)
+ File "/usr/lib/python2.6/codecs.py", line 351, in write
+ data, consumed = self.encode(object, self.errors)
+UnicodeDecodeError: 'ascii' codec can't decode byte 0xd0 in position 95: ordinal not in range(128)
+---
+
+$ python
+Python 2.6.1 (r261:67515, Jan 8 2010, 16:07:38)
+[GCC 4.3.2] on linux2
+Type "help", "copyright", "credits" or "license" for more information.
+>>> import sys
+>>> import locale
+>>> sys.getdefaultencoding()
+'ascii'
+>>> locale.getdefaultlocale()
+('ru_RU', 'UTF-8')
+
+I think it is more correct to use UTF-8 everywhere or use locale.getdefaultlocale() instead sys.getdefaultencoding().
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/values
new file mode 100644
index 0000000..68e1d55
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/comments/854eec21-2eeb-4ed4-af35-7a4a2e1f2e98/values
@@ -0,0 +1,8 @@
+Author: Anton Batenev <abbat@abbat>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 16 Mar 2010 12:53:45 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/values
new file mode 100644
index 0000000..5dda50d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3562f08-ad27-4b9f-8d21-8b58ba6d9eac/values
@@ -0,0 +1,17 @@
+creator: Anton Batenev <abbat@abbat>
+
+
+reporter: Anton Batenev <abbat@abbat>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: UTF-8 problems
+
+
+time: Tue, 16 Mar 2010 12:40:01 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3c6da51-3a30-42c9-8c75-587c7a1705c5/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3c6da51-3a30-42c9-8c75-587c7a1705c5/values
new file mode 100644
index 0000000..ab12d24
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3c6da51-3a30-42c9-8c75-587c7a1705c5/values
@@ -0,0 +1,14 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+severity: critical
+
+
+status: fixed
+
+
+summary: Slow be commands due to bugdir loading, go back to lazy bug loading.
+
+
+time: Sun, 23 Nov 2008 13:48:01 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body
new file mode 100644
index 0000000..dd40bfa
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body
@@ -0,0 +1 @@
+Aaron said this was closeable in Nov. 24th email to the BE list.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values
new file mode 100644
index 0000000..549a346
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 13:48:47 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values
new file mode 100644
index 0000000..594433d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: support multi-rcs configurations
+
+
+time: Thu, 07 Apr 2005 16:09:10 +0000
+
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
index 0000000..e42beab
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values
@@ -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
index 0000000..f245ea4
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/27a5a4cc-1782-4509-a3d2-db00c190f97d/body
@@ -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
index 0000000..2169b75
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/27a5a4cc-1782-4509-a3d2-db00c190f97d/values
@@ -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
index 0000000..f890566
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/76d54016-755b-42ca-ad07-eb9a1c77c33d/body
@@ -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
index 0000000..1566702
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/comments/76d54016-755b-42ca-ad07-eb9a1c77c33d/values
@@ -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
index 0000000..364629d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c1b76442-eab6-4796-9517-8454425d7757/values
@@ -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
index 0000000..c5f1b4f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097/body
@@ -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
index 0000000..f7b7498
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097/values
@@ -0,0 +1,8 @@
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 08 Oct 2009 20:16:46 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/values
new file mode 100644
index 0000000..27cfc2f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/values
@@ -0,0 +1,17 @@
+creator: gianluca <gian@galactica>
+
+
+reporter: gianluca <gian@galactica>
+
+
+severity: wishlist
+
+
+status: fixed
+
+
+summary: Add a verbose option to "be html"?
+
+
+time: Fri, 03 Jul 2009 21:17:41 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/body
new file mode 100644
index 0000000..832cfd3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/body
@@ -0,0 +1,3 @@
+Bugs-Everywhere-Web/start-beweb.py should call python not python2.4
+it works great with python2.5
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/values
new file mode 100644
index 0000000..cdba2a5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671/values
@@ -0,0 +1,8 @@
+Author: j
+
+
+Content-type: text/plain
+
+
+Date: Mon, 14 Apr 2008 16:43:07 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/body
new file mode 100644
index 0000000..a490992
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/body
@@ -0,0 +1 @@
+Looks like j@oil21.org fixed this in 211.3.1.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values
new file mode 100644
index 0000000..6a0464f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 24 Nov 2008 13:23:43 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/values
new file mode 100644
index 0000000..90fedbc
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/values
@@ -0,0 +1,14 @@
+creator: j
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: use python instead of python2.4 in Bugs-Everywhere-Web/start-beweb.py
+
+
+time: Mon, 14 Apr 2008 16:42:37 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/body
new file mode 100644
index 0000000..d589f18
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/body
@@ -0,0 +1,7 @@
+When running `python test.py` I recieved lots of errors due to 'tla'
+(the GNU Arch revision control system binary) not being installed.
+I had expected test.py to only test the backends for installed VCSs.
+
+I've added a note saying that `python test.py` tests *all* the
+backends, but someone who understands the usage better can probably
+write a nicer version.
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
index 0000000..4255708
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values
@@ -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/acbecd72-988c-4899-a340-fea370ce15a8/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/body
new file mode 100644
index 0000000..cf9af30
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/body
@@ -0,0 +1,5 @@
+I rewrote test.py, so I suppose I'm the person who understands it
+better now ;). The usage is now documented in the test.py lead
+comment. The becommand tests now attempt to run with the first
+*installed* versioning system, which should reduce cryptic errors.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values
new file mode 100644
index 0000000..cf27de6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Wed, 19 Nov 2008 17:11:51 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/body
new file mode 100644
index 0000000..77d75fb
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/body
@@ -0,0 +1,3 @@
+Ideally the tests would fail gracefully with some simple message like
+"tla version control system not found", and we could skip the message
+in the test.py docstring.
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
index 0000000..f38cb7f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values
@@ -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/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/values
new file mode 100644
index 0000000..354de99
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/values
@@ -0,0 +1,14 @@
+creator: wking
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Usage of be/test.py is unclear
+
+
+time: Thu, 13 Nov 2008 16:31:41 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values
new file mode 100644
index 0000000..e8ba31c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Needs ability to create comments
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/body
new file mode 100644
index 0000000..2767d69
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/body
@@ -0,0 +1,6 @@
+In my Tue, 25 Nov 2008 08:30:19 -0500 email:
+
+I thought feature requests would just have "wishlist" severity. What
+would be an example of a to-do item that is not a feature request or a
+bug?
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/values
new file mode 100644
index 0000000..6bc9258
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 20:12:35 +0000
+
+
+In-reply-to: 354dcfc6-5997-4ffe-b7a0-baa852213539
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/body
new file mode 100644
index 0000000..7fcd766
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/body
@@ -0,0 +1,7 @@
+In Aaron's Mon, 24 Nov 2008 19:15:08 -0500 email, he adds:
+
+Issue trackers should provide tracking of
+1. bugs
+2. feature requests
+3. to-do items.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/values
new file mode 100644
index 0000000..d000e42
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 20:11:02 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/body
new file mode 100644
index 0000000..04a97cd
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/body
@@ -0,0 +1,14 @@
+If you want more granularity than just `wishlist' what about the
+`severities':
+ todo-critical
+ todo-minor
+ todo-...
+Then get a list of available severities with
+ $ be list --help | grep -A1 '^severity'
+ severity
+ wishlist,minor,serious,critical,fatal,todo-critical,todo-minor
+And show all the todos:
+ $ be list --severity todo-critical,todo-minor
+
+You can configure all the severities you'd like with
+ $ be set severity wishlist,minor,...
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/values
new file mode 100644
index 0000000..34d6548
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 20:20:39 +0000
+
+
+In-reply-to: f847c981-873e-41ae-b5ce-83dfe60b9afe
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/body
new file mode 100644
index 0000000..cc02836
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/body
@@ -0,0 +1,10 @@
+In Aaron's Tue, 25 Nov 2008 09:32:29 -0500 email:
+
+I think that approach doesn't give features the richness they need.
+Features also have severities-- some features are important, and others
+are just nice-to-have. And there should be a way to list *only* bugs,
+or *only* features.
+
+In a bug tracker, "wishlist" is either an aberration, or it means a very
+low severity.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/values
new file mode 100644
index 0000000..62a3fb7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 20:14:26 +0000
+
+
+In-reply-to: 22348320-40d3-422c-bdf0-0f6a6bde3fab
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/values
new file mode 100644
index 0000000..d000d2e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Support 'issues', like todo, better
+
+
+time: Wed, 04 Jan 2006 21:09:02 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body
new file mode 100644
index 0000000..7f46872
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body
@@ -0,0 +1,17 @@
+Example:
+
+We're working happily in a versioned bugdir, and our RCS knows who we
+are. We create a temporary repository copy from a previous revision
+for diff generation. We set the RCS for the copy to "None", since we
+didn't bother initializing our normal RCS in the snapshot copy. But
+now the BugDir instantized on the copy doesn't know who we are!
+
+Solution:
+
+Track user id in the bugdir settings file. If you
+bugdir.settings["user_id"], it will be saved and loaded. When loaded,
+it will also set bugdir.user_id. If you set rcs.user_id, it will be
+returned by rcs.get_user_id(), instead of returing the output of
+rcs._rcs_get_user_id(). We should be caching the output of
+_rcs_get_user_id() anyway.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values
new file mode 100644
index 0000000..645e8c9
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 22 Nov 2008 21:43:29 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/body
new file mode 100644
index 0000000..62c14e6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/body
@@ -0,0 +1 @@
+This bug duplicates a403de79-8f39-41f2-b9ec-15053b175ee2
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values
new file mode 100644
index 0000000..32491b7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 23 Nov 2008 12:37:57 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body
new file mode 100644
index 0000000..090895e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body
@@ -0,0 +1 @@
+Merged into bug a403de79-8f39-41f2-b9ec-15053b175ee2 \ No newline at end of file
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values
new file mode 100644
index 0000000..a0c3bd7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 25 Nov 2008 02:24:05 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values
new file mode 100644
index 0000000..947a596
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values
@@ -0,0 +1,14 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Allow user id to be cached in settings for duplicate bugdirs
+
+
+time: Sat, 22 Nov 2008 21:36:06 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cb56c990-a757-4aef-9888-a30918a7b3d7/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cb56c990-a757-4aef-9888-a30918a7b3d7/values
new file mode 100644
index 0000000..804f631
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cb56c990-a757-4aef-9888-a30918a7b3d7/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: 'Beweb: Stripey tables'
+
+
+time: Wed, 04 Jan 2006 21:06:10 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/body
new file mode 100644
index 0000000..89d64c5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/body
@@ -0,0 +1,8 @@
+From Aaron's Mon, 24 Nov 2008 19:15:09 -0500 email
+
+cf5:oc: OK, maybe not fatal, but how about a new name that suggests
+process tracking, not just bugs?
+
+If you can come with a better name, that would be great. But naming an
+issue tracker for its bug-tracking features isn't a terrible idea.
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/values
new file mode 100644
index 0000000..2dea024
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 22 Jun 2009 19:48:44 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body
new file mode 100644
index 0000000..d7a57d9
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body
@@ -0,0 +1 @@
+I dunno, bugs everywhere is such a great mental image... ;)
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
index 0000000..06d6017
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 15 Nov 2008 23:56:51 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values
new file mode 100644
index 0000000..29d76c7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values
@@ -0,0 +1,15 @@
+creator: abentley
+
+
+severity: critical
+
+
+status: closed
+
+
+summary: OK, maybe not fatal, but how about a new name that suggests process tracking,
+ not just bugs?
+
+
+time: Fri, 27 Jan 2006 14:37:25 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values
new file mode 100644
index 0000000..4e6607c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values
@@ -0,0 +1,14 @@
+assigned: abentley
+
+
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: date-stamp bugs
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/body
new file mode 100644
index 0000000..1090ace
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/body
@@ -0,0 +1,2 @@
+Available with
+ be -d DIR html
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/values
new file mode 100644
index 0000000..9ac4884
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 07 Aug 2009 17:58:58 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/values
new file mode 100644
index 0000000..ce4bc92
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/values
@@ -0,0 +1,17 @@
+creator: gianluca <gian@galactica>
+
+
+reporter: gianluca <gian@galactica>
+
+
+severity: wishlist
+
+
+status: fixed
+
+
+summary: Add the possibility to specify the repository directory to "be html"?
+
+
+time: Fri, 03 Jul 2009 21:18:13 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/body
new file mode 100644
index 0000000..a6ce192
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/body
@@ -0,0 +1,19 @@
+Hi,
+
+ > http://bitbucket.org/sjl/cherryflavoredbugseverywhere/
+
+Cool! I've set up a copy here:
+
+ http://bugsweb.bugseverywhere.org/
+
+(Since we don't have any open bugs lately, click "Closed" at the top,
+or create some, but don't expect them to persist if you do.)
+
+ > anyone has any feedback (on any aspect of it) I'd appreciate it.
+
+I'm pretty enthusiastic about merging this and then working on it
+further.
+
+Thanks,
+
+- Chris.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/values
new file mode 100644
index 0000000..4a25a25
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c/values
@@ -0,0 +1,14 @@
+Alt-id: <m3eisxtgfx.fsf@pullcord.laptop.org>
+
+
+Author: Chris Ball <cjb@laptop.org>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 03 Jul 2009 20:55:30 -0400
+
+
+In-reply-to: 2496ccca-130b-4459-bfae-9d9ef0138177
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/body
new file mode 100644
index 0000000..5a3508c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/body
@@ -0,0 +1,26 @@
+Hi everyone. I found Bugs Everywhere and really like the idea of
+distributed bug tracking. I felt like practicing building a CherryPy
+site so I put together a quick web interface to BE. I know there's
+already a TurboGears one in the works, but I needed an excuse to try
+out CherryPy again after working with Django for a while.
+
+Would any of you be willing to take a look at what I've got so far and
+tell me what you think I could improve?
+
+To install and use it:
+
+* Install CherryPy from http://cherrypy.org/ if you don't have it.
+* Install Jinja2 from http://jinja.pocoo.org/2/ if you don't have it.
+* Install BugsEverywhere - you probably know how to do this :)
+* Download a zip/tar of my project (or hg clone) from http://bitbucket.org/sjl/cherryflavoredbugseverywhere/
+* Unzip my project and put the folder in your Python site-packages
+directory.
+* Symlink site-packages/cherryflavoredbugseverywhere/cfbe.py to /usr/
+local/bin/cfbe
+* Use the "cfbe [project_root]" command to start up the web interface
+for that project.
+* Visit http://localhost:8080/ in a browser.
+
+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!
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/values
new file mode 100644
index 0000000..bb88284
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d/values
@@ -0,0 +1,11 @@
+Alt-id: <272FECFE-D16B-47B7-B39A-E2C8A718CCC5@stevelosh.com>
+
+
+Author: Steve Losh <steve@stevelosh.com>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 07 Feb 2009 16:30:33 -0500
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/body
new file mode 100644
index 0000000..d2ef28c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/body
@@ -0,0 +1,77 @@
+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/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/values
new file mode 100644
index 0000000..ca3efd0
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8/values
@@ -0,0 +1,14 @@
+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/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/body
new file mode 100644
index 0000000..5fc4981
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/body
@@ -0,0 +1,52 @@
+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!
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/values
new file mode 100644
index 0000000..fbe6c0e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799/values
@@ -0,0 +1,14 @@
+Alt-id: <D765386C-4D43-4AE0-83E3-986A1CB4008C@stevelosh.com>
+
+
+Author: Steve Losh <steve@stevelosh.com>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 07 Feb 2009 17:48:06 -0500
+
+
+In-reply-to: 42d57a41-219f-46db-9fda-21b42351da63
+
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
index 0000000..ed4f97d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/body
@@ -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.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/values
new file mode 100644
index 0000000..e602a56
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177/values
@@ -0,0 +1,14 @@
+Alt-id: <4701D71B-A14D-4C63-ABCC-E7E5FFE4E4BA@stevelosh.com>
+
+
+Author: Steve Losh <steve@stevelosh.com>
+
+
+Content-type: text/plain
+
+
+Date: Fri, 03 Jul 2009 20:34:51 -0400
+
+
+In-reply-to: 16357f68-19c0-4bf9-8220-b88b52b3456d
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/body
new file mode 100644
index 0000000..a280d8c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/body
@@ -0,0 +1,25 @@
+Hi Steve,
+
+ > Hi everyone. I found Bugs Everywhere and really like the idea of
+ > distributed bug tracking. I felt like practicing building a
+ > CherryPy site so I put together a quick web interface to BE. I
+ > know there's already a TurboGears one in the works, but I needed an
+ > excuse to try out CherryPy again after working with Django for a
+ > while.
+
+This looks awesome, thanks! I've taken some screenshots for others to
+see:
+
+http://chris.printf.net/cfbe-main.png
+http://chris.printf.net/cfbe-detail.png
+
+My initial impression is that this looks good enough already to merge as
+a replacement for the turbogears site. What does everyone else think?
+
+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.)
+
+Great work, thanks!
+
+- Chris.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/values
new file mode 100644
index 0000000..e05f99d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63/values
@@ -0,0 +1,14 @@
+Alt-id: <m3zlgxyjo5.fsf@pullcord.laptop.org>
+
+
+Author: Chris Ball <cjb@laptop.org>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 07 Feb 2009 17:19:22 -0500
+
+
+In-reply-to: 16357f68-19c0-4bf9-8220-b88b52b3456d
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/body
new file mode 100644
index 0000000..593fa83
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/body
@@ -0,0 +1,28 @@
+Hi,
+
+ > Works for me. I've done this now, which closes the last open bug
+ > in my repo :D.
+
+Wow. Congrats! I've merged your branch.
+
+ > All the new functionality comes from bug.extra_strings, which
+ > provides a list for storing arbitrary strings in the bug's
+ > permanent state.
+
+That's going to be really useful.
+
+ > Next up: regexp searching for list --extra-strings! ;).
+
+Awesome.
+
+ > Oh, and obviously there must still be bugs in BE. Please submit
+ > more ;).
+
+Perhaps it's a good time to merge Steve Losh's CherryPy web interface?
+
+http://void.printf.net/pipermail/be-devel/2009-February/000095.html
+http://bitbucket.org/sjl/cherryflavoredbugseverywhere/
+
+Thanks,
+
+- Chris.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/values
new file mode 100644
index 0000000..a4063be
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b/values
@@ -0,0 +1,14 @@
+Alt-id: <m31vp82yyj.fsf@pullcord.laptop.org>
+
+
+Author: Chris Ball <cjb@laptop.org>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 25 Jun 2009 10:02:44 -0400
+
+
+In-reply-to: 16357f68-19c0-4bf9-8220-b88b52b3456d
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/body
new file mode 100644
index 0000000..d52f4b9
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/body
@@ -0,0 +1,22 @@
+On Jun 25, 2009, at 10:02 AM, Chris Ball <cjb@laptop.org> wrote:
+>
+>> Oh, and obviously there must still be bugs in BE. Please submit
+>> more ;).
+>
+> Perhaps it's a good time to merge Steve Losh's CherryPy web interface?
+>
+> http://void.printf.net/pipermail/be-devel/2009-February/000095.html
+> http://bitbucket.org/sjl/cherryflavoredbugseverywhere/
+>
+
+Hey, I haven't touched the web interface in a while, but I should have
+some time to fix some stuff up tonight and tomorrow. Hold off on
+merging it in until then.
+
+I'm still curious as to what people think the role of a web interface
+like this should be. When I wrote it I meant it as a single-user
+interface like the command line one. It could definitely work as a
+public, read-only interface too.
+
+If the goal is to allow more than one person to add issues, how should
+commits go? One commit per change? Commit every X minutes if necessary?
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/values
new file mode 100644
index 0000000..eea816a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b/values
@@ -0,0 +1,14 @@
+Alt-id: <26FBD153-39C5-4641-AF5E-749731964086@stevelosh.com>
+
+
+Author: Steve Losh <steve@stevelosh.com>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 25 Jun 2009 10:23:04 -0400
+
+
+In-reply-to: 5e339bac-f4f3-407b-974a-b88795d3573b
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/body
new file mode 100644
index 0000000..a6dd2f1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/body
@@ -0,0 +1,26 @@
+On Sat, Feb 07, 2009 at 05:48:06PM -0500, Steve Losh wrote:
+>> 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. :)
+
+Steve's also versioning it with Mercurial. Will he mind changing to
+Bazaar?
+
+Steve, I've touched up CFBE to work with my current BE branch. Some
+of the changes apply to the trunk BE, and hopefully the rest will
+soon. I think the version-naming issue is what's currently blocking
+their adoption ;). I've put up my CFBE branch at
+ static-http://www.physics.drexel.edu/~wking/code/hg/cfbe
+for Mercurial.
+
+Everyone, should CFBE-specific discussions move off-list? More
+generally, I've been sending in lots of what-I'm-working on messages,
+but not hearing much back, so let me know if I'm being too obnoxious,
+and I'll shut up (or at least move more off-list) ;).
+
+Cheers,
+Trevor
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/values
new file mode 100644
index 0000000..3847736
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/values
@@ -0,0 +1,14 @@
+Alt-id: <20090721135907.GB4469@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 21 Jul 2009 09:59:07 -0400
+
+
+In-reply-to: 21c90231-d7f2-49bb-97d9-99e16459d799
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/body
new file mode 100644
index 0000000..b1db016
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/body
@@ -0,0 +1,62 @@
+On Thu, Jun 25, 2009 at 10:23:04AM -0400, Steve Losh wrote:
+> I'm still curious as to what people think the role of a web interface like
+> this should be. When I wrote it I meant it as a single-user interface like
+> the command line one. It could definitely work as a public, read-only
+> interface too.
+
+I think the multi-user/public is the way to go. I'd also like to see
+a procmail-able script to handle multi-user/public access via email,
+which would have all the same issues we're worrying about here.
+
+On Sat, Feb 07, 2009 at 05:48:06PM -0500, Steve Losh wrote:
+> 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.
+
+This sounds good to me. Yay spam reduction ;).
+
+> 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?
+
+On Thu, Jun 25, 2009 at 10:23:04AM -0400, Steve Losh wrote:
+> One commit per change? Commit every X minutes if necessary?
+
+On Sat, Feb 07, 2009 at 05:48:06PM -0500, Steve Losh wrote:
+> 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)?
+
+There are interesting discussions about the role of distributed
+bugtracking here (I'm sure there are others):
+ http://lwn.net/Articles/281849/
+ http://community.livejournal.com/evan_tech/248736.html
+
+Personally, I've never done much cherry-picking or anything that would
+require me to determine exactly which parts of someone's work I want
+and which I don't want. I just merge someone's head and edit out the
+bits I don't like, a process that also works well for our current
+"commit however you want" BE development model ;). Maybe that just
+shows that I only work on minor branches of small projects :p. In the
+absence of big-project advice, I think we just limit the web front end
+to our current model, and let the web interface commit however it
+wants as well ;). +1 for adding a <commit> button to the web
+interface ;).
+
+One caveat about a multi-user interface would be that it would allow
+the casual users to commit bugs about whatever version they had
+installed onto the head of the public-bug branch. In order to figure
+out what version of the project they were talking about, the project
+should have a way for the user to get a unique version string, ideally
+be the bzr-revision-id/git-commit/etc. of the commit for the version
+they were using. This would allow developers to determine what branch
+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.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/values
new file mode 100644
index 0000000..721f9fd
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/values
@@ -0,0 +1,14 @@
+Alt-id: <20090625154734.GA19441@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 25 Jun 2009 11:47:34 -0400
+
+
+In-reply-to: 16357f68-19c0-4bf9-8220-b88b52b3456d
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/values
new file mode 100644
index 0000000..8cf85c9
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/values
@@ -0,0 +1,20 @@
+assigned: Steve Losh <steve@stevelosh.com>
+
+
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: Steve Losh <steve@stevelosh.com>
+
+
+severity: wishlist
+
+
+status: assigned
+
+
+summary: CherryPy interface "Cherry-flavored BE"
+
+
+time: Tue, 21 Jul 2009 18:52:44 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/da2b09ff-af24-40f3-9b8d-6ffaa5f41164/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/da2b09ff-af24-40f3-9b8d-6ffaa5f41164/values
new file mode 100644
index 0000000..2832bb3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/da2b09ff-af24-40f3-9b8d-6ffaa5f41164/values
@@ -0,0 +1,17 @@
+creator: Gianluca Montecchi <gian@grys.it>
+
+
+reporter: Gianluca Montecchi <gian@grys.it>
+
+
+severity: wishlist
+
+
+status: open
+
+
+summary: Add an icon near the status string in "be html" output
+
+
+time: Tue, 04 Aug 2009 21:15:52 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/body
new file mode 100644
index 0000000..2887d2b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/body
@@ -0,0 +1,10 @@
+Problem was due to
+ open-value-file
+ write-value-file
+ add/update-value-file
+which should be (and now is)
+ open-value-file
+ write-value-file
+ close-value-file
+ add/update-value-file
+since it was getting added before the changes we'd written were flushed out.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values
new file mode 100644
index 0000000..5972d7a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@example.com>
+
+
+Content-type: text/plain
+
+
+Date: Wed, 19 Nov 2008 01:12:37 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/body
new file mode 100644
index 0000000..2c49b6b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/body
@@ -0,0 +1,26 @@
+It looks like the mapfiles are not being 'git add'ed after changes.
+
+$ mkdir BEtest
+$ cd BEtest
+$ git init
+$ be set-root .
+$ be new 'new'
+$ git status
+# On branch master
+#
+# Initial commit
+#
+# Changes to be committed:
+# (use "git rm --cached <file>..." to unstage)
+#
+# new file: .be/bugs/8f021d79-44f5-479f-af12-c37e2caf3ce1/values
+# new file: .be/settings
+# new file: .be/version
+#
+# Changed but not updated:
+# (use "git add <file>..." to update what will be committed)
+#
+# modified: .be/bugs/8f021d79-44f5-479f-af12-c37e2caf3ce1/values
+# modified: .be/settings
+#
+
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
index 0000000..bb26755
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values
@@ -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/dac91856-cb6a-4f69-8c03-38ff0b29aab2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/values
new file mode 100644
index 0000000..17288dc
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/values
@@ -0,0 +1,14 @@
+creator: wking
+
+
+severity: serious
+
+
+status: fixed
+
+
+summary: BE not notifying git of some changed files
+
+
+time: Mon, 17 Nov 2008 15:02:15 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c/values
new file mode 100644
index 0000000..9271b50
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c/values
@@ -0,0 +1,15 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: there's a tyop in the wx example gui (in the line that appends ../../ to
+ sys.path)
+
+
+time: Tue, 27 Dec 2005 16:59:49 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/body
new file mode 100644
index 0000000..d29c749
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/body
@@ -0,0 +1,43 @@
+BE should not crash when be list|show is used on a git repository that
+have not the config variables user.name and user.email defined in the
+.git/config file.
+
+To view the bug, in my opinion shold not be mandatory to have these two options
+defined
+
+
+Traceroute:
+
+galactica:~/Devel/dumb> be show 996
+Traceback (most recent call last):
+ File "/usr/bin/be", line 62, in <module>
+ sys.exit(cmdutil.execute(args[0], args[1:]))
+ File "/usr/lib/python2.5/site-packages/libbe/cmdutil.py", line 76, in execute
+ ret = cmd.execute([a.decode(enc) for a in args])
+ File "/usr/lib/python2.5/site-packages/becommands/show.py", line 60, in execute
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
+ File "/usr/lib/python2.5/site-packages/libbe/bugdir.py", line 302, in __init__
+ self.load()
+ File "/usr/lib/python2.5/site-packages/libbe/bugdir.py", line 382, in load
+ self.load_settings()
+ File "/usr/lib/python2.5/site-packages/libbe/bugdir.py", line 411, in load_settings
+ self._setup_user_id(self.user_id)
+ File "/usr/lib/python2.5/site-packages/libbe/properties.py", line 293, in _fget
+ value = generator(self)
+ File "/usr/lib/python2.5/site-packages/libbe/bugdir.py", line 177, in _guess_user_id
+ return self.rcs.get_user_id()
+ File "/usr/lib/python2.5/site-packages/libbe/rcs.py", line 258, in get_user_id
+ id = self._rcs_get_user_id()
+ File "/usr/lib/python2.5/site-packages/libbe/git.py", line 56, in _rcs_get_user_id
+ status,output,error = self._u_invoke_client("config", "user.name")
+ File "/usr/lib/python2.5/site-packages/libbe/rcs.py", line 458, in _u_invoke_client
+ return self._u_invoke(cl_args, stdin=stdin,expect=expect,cwd=directory)
+ File "/usr/lib/python2.5/site-packages/libbe/rcs.py", line 450, in _u_invoke
+ raise CommandError(args, status, error)
+libbe.rcs.CommandError: Command failed (1):
+
+
+while executing
+ ['git', 'config', 'user.name']
+galactica:~/Devel/dumb>
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/values
new file mode 100644
index 0000000..324b732
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6/values
@@ -0,0 +1,8 @@
+Author: Gianluca Montecchi <gian@grys.it>
+
+
+Content-type: text/plain
+
+
+Date: Mon, 03 Aug 2009 20:33:30 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/values
new file mode 100644
index 0000000..375e44d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/values
@@ -0,0 +1,17 @@
+creator: Gianluca Montecchi <gian@grys.it>
+
+
+reporter: Gianluca Montecchi <gian@grys.it>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: BE should not crash if user.email and user.name are not defined
+
+
+time: Mon, 03 Aug 2009 20:30:43 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0155831-499f-421a-ad02-cd15fc3fecf1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0155831-499f-421a-ad02-cd15fc3fecf1/values
new file mode 100644
index 0000000..bffaec4
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0155831-499f-421a-ad02-cd15fc3fecf1/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: No way to commit/update from beweb
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/body
new file mode 100644
index 0000000..770af86
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/body
@@ -0,0 +1,19 @@
+On Thu, Jul 16, 2009 at 09:39:30AM -0400, W. Trevor King wrote:
+> Disclaimer: I imaging the current implementation will choke on
+> non-text/plain content types. Also possibly on non-ascii encodings.
+
+Non-ascii encodings are now handled (although now the output is
+base64-encoded). This is limited by poor unicode handling in the
+email module for current pythons, see the log for more details.
+
+> I should probably allow the "help" command ... ;).
+
+Added, but it currently shows _all_ commands, not just allowed
+commands.
+
+--
+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/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/values
new file mode 100644
index 0000000..78bc87b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/values
@@ -0,0 +1,14 @@
+Alt-id: <20090718131220.GA31832@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 18 Jul 2009 09:12:20 -0400
+
+
+In-reply-to: f1cde826-0506-4b4a-92ab-8499e953fa49
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/body
new file mode 100644
index 0000000..e008923
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/body
@@ -0,0 +1,27 @@
+On Sat, Jul 18, 2009 at 06:05:51PM -0400, W. Trevor King wrote:
+> My email interface now supports bug creation/comments that look
+> like:
+>
+> $ cat | mail -s "[be-bug] new" "whatever-dev@fancyprojects.com"
+> The demuxulizer is broken
+>
+> <describe bug>
+> ^D
+
+The move towards the DBT interface means this example should now look
+like
+
+ $ cat | mail -s "[be-bug:submit] The demuxulizer is broken" whatever-dev@fancyprojects.com
+ Version: XYZ
+
+ <describe bug>
+ --
+ Ignored text
+ ^D
+
+--
+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/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/values
new file mode 100644
index 0000000..b640d0b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/values
@@ -0,0 +1,14 @@
+Alt-id: <20090719130649.GA4164@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 19 Jul 2009 09:06:49 -0400
+
+
+In-reply-to: 7b904395-86e9-4eb1-8534-69cec63801d4
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/body
new file mode 100644
index 0000000..800609e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/body
@@ -0,0 +1,27 @@
+Ah, it's been a good day :). My email interface now supports bug
+creation/comments that look like:
+
+ $ cat | mail -s "[be-bug] new" "whatever-dev@fancyprojects.com"
+ The demuxulizer is broken
+
+ <describe bug>
+ ^D
+
+Which is probably easy enough for just about anybody ;). Also easy
+for other projects to wrap into one of their tools:
+
+ $ cat | projectAexecutable --report-bug
+ The demuxulizer is broken
+
+ <describe bug>
+ ^D
+
+Which could do things like automatically add the version string, OS
+name, etc.
+
+--
+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/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/values
new file mode 100644
index 0000000..b70c6e3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/values
@@ -0,0 +1,14 @@
+Alt-id: <20090718220551.GB32230@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 18 Jul 2009 18:05:51 -0400
+
+
+In-reply-to: 09f950d4-9366-4e7b-98b3-9057999f8f38
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/body
new file mode 100644
index 0000000..087d67a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/body
@@ -0,0 +1,58 @@
+On Sun, Jul 19, 2009 at 09:09:05AM +1000, Ben Finney wrote:
+> > The interface is basically "place your be command in the subject line"
+>
+> I would far prefer an interface of “place as many BE commands as you
+> like at the top of the message body, ending with an optional terminator
+> command, and they will each be executed in turn”.
+> ...
+
+I think the idea behind my first approach was "you guys have
+experience with the command line BE interface, so it will be easier to
+test if you don't have to learn the DBT interface." The Debian people
+have been doing this for a while though, so I imagine their email
+interface is pretty good ;).
+
+Here's a short primer on my take on the DBT interface.
+
+The DBT has three main email addresses, each with it's own parsing style.
+ 1) creating bugs (submit@bugs.debian.org)
+ 2) commenting on bugs (<bug-number>@bugs.debian.org)
+ 3) controlling/managing bugs (control@bugs.debian.org)
+I'm trying to squeeze these down into a single email address to avoid
+having to tinker with the mail delivery system, so I've got everything
+at (wking at tremily dot us) with subject tags:
+ 1) creating bugs
+ Subject: [be-bug:submit] new bug summary ...
+ 2) commenting on bugs
+ Subject: [be-bug:<bug-number>] human-specific subject
+ 3) control
+ Subject: [be-bug] human-specific subject
+
+The parsing styles each follow their DBT counterparts, but currently
+have a much restricted breadth.
+
+The control-style consists of a list of allowed be commands, with one
+command per line. Blank lines and lines beginning with '#' are
+ignored, as well anything following a line starting with '--'. All the
+listed commands are executed in order and their output returned.
+
+The comment-style 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.
+
+The create-style interface creates a bug whose summary is given by the
+email's post-tag subject. The body of the email must begin with a
+psuedo-header containing at least the "Version" field. Anything after
+the pseudo-header and before a line starting with '--' is, if present,
+attached as the bugs first comment.
+
+Take a look at my interfaces/email/interactive/examples for some
+examples.
+
+--
+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/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/values
new file mode 100644
index 0000000..5f323c6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/values
@@ -0,0 +1,14 @@
+Alt-id: <20090719130153.GA4036@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 19 Jul 2009 09:01:53 -0400
+
+
+In-reply-to: cfd7cbc7-27ad-4618-8530-cb4d7323514a
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/body
new file mode 100644
index 0000000..3db2a91
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/body
@@ -0,0 +1,26 @@
+"W. Trevor King" <wking@drexel.edu> writes:
+
+> The interface is basically "place your be command in the subject line"
+
+I would far prefer an interface of “place as many BE commands as you
+like at the top of the message body, ending with an optional terminator
+command, and they will each be executed in turn”.
+
+This would allow a single message to perform a batch of BE commands that
+are related, instead of requiring to send each command in a separate
+message.
+
+It would also leave the subject field free for something more
+descriptive. The subject field could also be used as the summary field
+of newly-created bug reports. With a terminator command, this would
+allow the message to be sent both to BE and to some other recipient
+(e.g. a mailing list) explaining the change.
+
+Have a look at the email interface of the Debian BTS for an example
+<URL:http://www.debian.org/Bugs/server-request>.
+
+--
+ \ “Pinky, are you pondering what I'm pondering?” “Wuh, I think |
+ `\ so, Brain, but will they let the Cranberry Duchess stay in the |
+_o__) Lincoln Bedroom?” —_Pinky and The Brain_ |
+Ben Finney
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/values
new file mode 100644
index 0000000..057b7fa
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a/values
@@ -0,0 +1,14 @@
+Alt-id: <87fxctbnce.fsf@benfinney.id.au>
+
+
+Author: Ben Finney <bignose+hates-spam@benfinney.id.au>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 19 Jul 2009 09:09:05 +1000
+
+
+In-reply-to: f1cde826-0506-4b4a-92ab-8499e953fa49
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/body
new file mode 100644
index 0000000..37b9936
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/body
@@ -0,0 +1,38 @@
+I finally did something towards a useful interactive email interface
+;). As per our new guidelines, I'll develop this feature in it's own
+branch:
+ http://www.physics.drexel.edu/~wking/code/bzr/be-email
+
+The interface is basically "place your be command in the subject line"
+with a few exceptions. Some examples:
+ Subject: [be-bug] list --status=all
+ Subject: [be-bug] show --xml ID
+ Subject: [be-bug] new
+ Subject: [be-bug] comment ID
+In the case of "new", the bug description is extracted from the first
+non-blank body line. In the case of "comment", the email body is used
+as the comment. Currently only "list", "show", "new", and "comment"
+are allowed.
+
+You should get a reply email with exit status, stdout, and stderr from
+your command.
+
+Send some mail to [wking (at) tremily (dot) us] to try it out! Depending
+on spam attraction, this might be a limited time offer ;).
+
+Hopefully this lowers the entry barrier for bug reporting :).
+
+Disclaimer: I imaging the current implementation will choke on
+non-text/plain content types. Also possibly on non-ascii encodings.
+Probably lots of other bugs too... ;). For example, I should probably
+allow the "help" command ... ;).
+
+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
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/values
new file mode 100644
index 0000000..727c4ee
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/values
@@ -0,0 +1,11 @@
+Alt-id: <20090716133930.GC12213@mjolnir.home.net>
+
+
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 16 Jul 2009 09:39:30 -0400
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/body
new file mode 100644
index 0000000..167cfe5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/body
@@ -0,0 +1,10 @@
+Hi Trevor,
+
+ > I finally did something towards a useful interactive email
+ > interface ;).
+
+Wow, nice! That'll be really useful.
+
+- Chris.
+--
+Chris Ball <cjb@laptop.org>
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/values
new file mode 100644
index 0000000..3cac90e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54/values
@@ -0,0 +1,14 @@
+Alt-id: <m3fxct5vl6.fsf@pullcord.laptop.org>
+
+
+Author: Chris Ball <cjb@laptop.org>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 18 Jul 2009 21:07:33 -0400
+
+
+In-reply-to: f1cde826-0506-4b4a-92ab-8499e953fa49
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/values
new file mode 100644
index 0000000..da43639
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/values
@@ -0,0 +1,20 @@
+assigned: W. Trevor King <wking@drexel.edu>
+
+
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: wishlist
+
+
+status: fixed
+
+
+summary: Interactive email interface
+
+
+time: Tue, 21 Jul 2009 18:53:50 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/body
new file mode 100644
index 0000000..c3b0f20
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/body
@@ -0,0 +1,5 @@
+There is definitely a difference between the person who reports a bug and the
+person who enters it in the system. For example, you are reporting bugs to me,
+and I am entering them in the Bugs Everywhere bug list.
+
+Perhaps there should be two fields.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/values
new file mode 100644
index 0000000..ae0ca8f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077/values
@@ -0,0 +1,11 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Thu, 14 Sep 2006 18:05:48 +0000
+
+
+In-reply-to: e5decfc6-050b-4283-8776-977bf85b2c99
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/body
new file mode 100644
index 0000000..23cb999
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/body
@@ -0,0 +1,3 @@
+Jens Mueller:
+Referring to the fields describing a bug, I suggest the following:
+The field 'Creator' should be named 'Reporter' (minor issue).
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/values
new file mode 100644
index 0000000..72d84b7
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99/values
@@ -0,0 +1,8 @@
+Author: abentley
+
+
+Content-type: text/plain
+
+
+Date: Thu, 14 Sep 2006 18:03:41 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values
new file mode 100644
index 0000000..7b5e6e6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Add a reporter field
+
+
+time: Thu, 14 Sep 2006 16:47:57 +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
index 0000000..30286d3
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/2cd562f5-fcb9-4cc5-bf8c-ad5c9d960761/body
@@ -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
index 0000000..a4a84af
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/2cd562f5-fcb9-4cc5-bf8c-ad5c9d960761/values
@@ -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
index 0000000..fafa132
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/68ec74b9-d2c7-421f-ac70-602b43bbd263/body
@@ -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
index 0000000..4bb296a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/comments/68ec74b9-d2c7-421f-ac70-602b43bbd263/values
@@ -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
index 0000000..4d48446
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e30e2b6b-acc9-4b93-88c6-b63b6e30b593/values
@@ -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/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/body
new file mode 100644
index 0000000..0598d70
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/body
@@ -0,0 +1,8 @@
+<type 'unicode'> <body>�</body>
+Traceback (most recent call last):
+ File "<string>", line 1, in <module>
+ File "/usr/lib/python2.5/xml/etree/ElementTree.py", line 963, in XML
+ parser.feed(text)
+ File "/usr/lib/python2.5/xml/etree/ElementTree.py", line 1245, in feed
+ self._parser.Parse(data, 0)
+UnicodeEncodeError: 'ascii' codec can't encode character u'\u1234' in position 6: ordinal not in range(128)
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/values
new file mode 100644
index 0000000..8a5060e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 12 Jul 2009 11:34:22 +0000
+
+
+In-reply-to: faa686bf-c0eb-48bf-8a0b-d9a2e02bd132
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/body
new file mode 100644
index 0000000..397d4b6
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/body
@@ -0,0 +1 @@
+It looks like etree wants a byte string, not unicode input
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/values
new file mode 100644
index 0000000..55642ec
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 12 Jul 2009 11:42:16 +0000
+
+
+In-reply-to: faa686bf-c0eb-48bf-8a0b-d9a2e02bd132
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/body
new file mode 100644
index 0000000..ce2bb8d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/body
@@ -0,0 +1,5 @@
+For example, this works:
+
+python -c 'from xml.etree import ElementTree; a=u"<body>\u1234</body>"; print type(a), a; b=ElementTree.XML(a.encode("unicode_escape")); print type(b.text), unicode(b.text).decode("unicode_escape");'
+
+Ugly though :p. Ah well.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/values
new file mode 100644
index 0000000..705ce8d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 12 Jul 2009 11:46:57 +0000
+
+
+In-reply-to: 520a9829-8d90-43ce-be64-868b8321e5b0
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/body
new file mode 100644
index 0000000..89a8f8d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/body
@@ -0,0 +1 @@
+That's with Python 2.5.2 and ElementTree "2326 2005-03-17 07:45:21Z fredrik"
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/values
new file mode 100644
index 0000000..3a0c6ff
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075/values
@@ -0,0 +1,11 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 12 Jul 2009 11:37:55 +0000
+
+
+In-reply-to: 07fc448f-c42e-4846-929a-8924de485766
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/body
new file mode 100644
index 0000000..57e050d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/body
@@ -0,0 +1,5 @@
+Isolated problem to:
+
+python -c 'from xml.etree import ElementTree; a=u"<body>\u1234</body>"; print type(a), a; b=ElementTree.XML(a);'
+
+Output attached below
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/values
new file mode 100644
index 0000000..8591aa5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sun, 12 Jul 2009 11:31:13 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/values
new file mode 100644
index 0000000..4bc81f5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/values
@@ -0,0 +1,17 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+reporter: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: utf8 problems in xml parsing
+
+
+time: Sat, 11 Jul 2009 15:48:32 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ecc91b94-7f3f-44a7-af58-03191d327a7f/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ecc91b94-7f3f-44a7-af58-03191d327a7f/values
new file mode 100644
index 0000000..a82beb8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ecc91b94-7f3f-44a7-af58-03191d327a7f/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: no tests for missing $EDITOR
+
+
+time: Tue, 17 May 2005 13:27:33 +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
index 0000000..ae7a57f
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9525e3f3-a044-4fa9-b311-56336267b8b5/body
@@ -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
index 0000000..3dfe992
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9525e3f3-a044-4fa9-b311-56336267b8b5/values
@@ -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
index 0000000..e43a951
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9c4b8921-7b43-4bb6-b650-34144b414dc0/body
@@ -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
index 0000000..a02a32b
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/9c4b8921-7b43-4bb6-b650-34144b414dc0/values
@@ -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
index 0000000..b1b9b1a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/c664b7be-ded5-42dd-a16a-82b2bdb52e36/body
@@ -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
index 0000000..1d01491
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/comments/c664b7be-ded5-42dd-a16a-82b2bdb52e36/values
@@ -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
index 0000000..7b2813d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ed5eac05-80ed-411d-88a4-d2261b879713/values
@@ -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
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values
new file mode 100644
index 0000000..2c8543e
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values
@@ -0,0 +1,15 @@
+creator: abentley
+
+
+extra_strings:
+- BLOCKS:4fc71206-4285-417f-8a3c-ed6fb31bbbda
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Support rcs configuration
+
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
index 0000000..bb6b222
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values
@@ -0,0 +1,15 @@
+creator: Aaron Bentley
+
+
+extra_strings:
+- BLOCKS:47c8fd5f-1f5a-4048-bef7-bb4c9a37c411
+
+
+severity: fatal
+
+
+status: fixed
+
+
+summary: Can't create bugs
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values
new file mode 100644
index 0000000..3c7b8d1
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values
@@ -0,0 +1,15 @@
+creator: abentley
+
+
+extra_strings:
+- BLOCKS:bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Implement bug tree diff
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f65b680b-4309-43a2-ae2d-e65811c9d107/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f65b680b-4309-43a2-ae2d-e65811c9d107/values
new file mode 100644
index 0000000..a2b042c
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f65b680b-4309-43a2-ae2d-e65811c9d107/values
@@ -0,0 +1,11 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: friendly name is created, but not used
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body
new file mode 100644
index 0000000..dd40bfa
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body
@@ -0,0 +1 @@
+Aaron said this was closeable in Nov. 24th email to the BE list.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values
new file mode 100644
index 0000000..9825ae8
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:20:20 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values
new file mode 100644
index 0000000..dde51b9
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values
@@ -0,0 +1,14 @@
+creator: abentley
+
+
+severity: minor
+
+
+status: closed
+
+
+summary: Allow different sorts
+
+
+time: Wed, 25 Jan 2006 15:43:19 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f77fc673-c852-4c81-bfa2-1d59de2661c8/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f77fc673-c852-4c81-bfa2-1d59de2661c8/values
new file mode 100644
index 0000000..72c2839
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f77fc673-c852-4c81-bfa2-1d59de2661c8/values
@@ -0,0 +1,17 @@
+creator: Gianluca Montecchi <gian@grys.it>
+
+
+reporter: Gianluca Montecchi <gian@grys.it>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: Comment should be threaded in the "be html" output
+
+
+time: Tue, 21 Jul 2009 21:39:52 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body
new file mode 100644
index 0000000..02bbe3a
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body
@@ -0,0 +1,5 @@
+Wrote/borrowed libbe/encoding.py.
+Now the following works:
+
+python -c 'import libbe.encoding as e; print e.get_encoding(); e.set_IO_stream_encodings(e.get_encoding()) ;print u"\u2019"' | cat
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values
new file mode 100644
index 0000000..1e12a53
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 25 Nov 2008 19:41:02 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body
new file mode 100644
index 0000000..d97791d
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body
@@ -0,0 +1,14 @@
+$ be show 31cd490d-a1c2-4ab3-8284-d80395e34dd2
+
+works as expected, but
+
+$ be show 31cd490d-a1c2-4ab3-8284-d80395e34dd2 | grep something
+Traceback (most recent call last):
+ File "/home/wking/bin/be", line 30, in <module>
+ sys.exit(cmdutil.execute(sys.argv[1], sys.argv[2:]))
+ File "/home/wking/src/fun/be-bugfix/libbe/cmdutil.py", line 57, in execute
+ File "/home/wking/src/fun/be/be.wtk/becommands/show.py", line 44, in execute
+ print bug.string(show_comments=True)
+UnicodeEncodeError: 'ascii' codec can't encode character u'\u2019' in position 2100: ordinal not in range(128)
+
+By the way, u2019 is a fancy apostrophe.
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values
new file mode 100644
index 0000000..95751fd
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 25 Nov 2008 02:36:16 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body
new file mode 100644
index 0000000..7bb09ff
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body
@@ -0,0 +1,9 @@
+Solution here
+http://www.amk.ca/python/howto/unicode
+
+You need to encode before printing.
+
+This is unfortunate, because we're currently very glib about just
+printing info to the terminal. This makes it much more important to
+have a single bugdir-wide encoding specification...
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values
new file mode 100644
index 0000000..1e4f9c5
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Tue, 25 Nov 2008 03:02:59 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/body
new file mode 100644
index 0000000..b441da9
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/body
@@ -0,0 +1 @@
+Test unicode �quotes�
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/values
new file mode 100644
index 0000000..86cfb90
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7/values
@@ -0,0 +1,8 @@
+Author: W. Trevor King <wking@drexel.edu>
+
+
+Content-type: text/plain
+
+
+Date: Sat, 11 Jul 2009 18:28:57 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values
new file mode 100644
index 0000000..93ad759
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values
@@ -0,0 +1,14 @@
+creator: W. Trevor King <wking@drexel.edu>
+
+
+severity: minor
+
+
+status: fixed
+
+
+summary: UTF-8 encoding trouble with pipes in becommands/show
+
+
+time: Tue, 25 Nov 2008 02:30:35 +0000
+
diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/settings b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/settings
new file mode 100644
index 0000000..8ecc0cd
--- /dev/null
+++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/settings
@@ -0,0 +1,14 @@
+extra_strings:
+- "SUBSCRIBE:W. Trevor King <wking@drexel.edu>\tall\t*"
+
+
+inactive_status:
+- - closed
+ - The bug is no longer relevant.
+- - fixed
+ - The bug should no longer occur.
+- - wontfix
+ - It's not a bug, it's a feature.
+- - disabled
+ - Unknown meaning. For backwards compatibility with old BE bugs.
+
diff --git a/.be/version b/.be/version
index 990837e..e7aade4 100644
--- a/.be/version
+++ b/.be/version
@@ -1 +1 @@
-Bugs Everywhere Tree 1 0
+Bugs Everywhere Directory v1.4
diff --git a/.bzrignore b/.bzrignore
new file mode 100644
index 0000000..7f19077
--- /dev/null
+++ b/.bzrignore
@@ -0,0 +1,12 @@
+./build
+libbe/_version.py
+./doc/*.1
+*.pyc
+*.sw[pon]
+fte.dsk
+*~
+./.shelf
+Bugs-Everywhere-Web/devdata.sqlite
+Bugs-Everywhere-Web/beweb/config.py
+Bugs-Everywhere-Web/beweb/database.sqlite
+Bugs-Everywhere-Web/beweb/catwalk-session
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..5445afc
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,15 @@
+Bugs Everywhere was written by:
+Aaron Bentley
+Alex Miller
+Alexander Belchenko
+Ben Finney
+Chris Ball
+Gianluca Montecchi
+James Rowe
+Jelmer Vernooij
+Marien Zwart
+Moritz Barsnick
+Oleg Romanyshyn
+Thomas Gerigk
+Thomas Habets
+W. Trevor King
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..d511905
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e9e1748
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,81 @@
+#! /usr/bin/make -f
+# :vim: filetype=make : -*- makefile; coding: utf-8; -*-
+
+# Makefile
+# Part of Bugs Everywhere, a distributed bug tracking system.
+#
+# 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.
+
+SHELL = /bin/bash
+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
+
+MANPAGES = be.1
+GENERATED_FILES := build libbe/_version.py
+
+MANPAGE_FILES = $(patsubst %,${MAN_DIR}/%,${MANPAGES})
+GENERATED_FILES += ${MANPAGE_FILES}
+
+
+.PHONY: all
+all: build
+
+
+.PHONY: build
+build: libbe/_version.py
+ python setup.py build
+
+.PHONY: doc
+doc: sphinx man
+
+.PHONY: install
+install: build doc
+ python setup.py install ${INSTALL_OPTIONS}
+
+test: build
+ python test.py
+
+.PHONY: clean
+clean:
+ $(RM) -rf ${GENERATED_FILES}
+ $(MAKE) -C ${DOC_DIR} clean
+
+
+.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
new file mode 100644
index 0000000..2defeb0
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,152 @@
+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
+
+April 6, 2006
+ * Better diagnostics from Marien Zwart
+ * Fixed installation from Marien Zwart
+ * Support ReST comments
+
+April 3, 2006
+ * Handle replying to comments
+ * Better help handling (Thomas Gerigk)
+
+March 31, 2006
+ * Changes to comments are shown in bzr diff
+
+March 3, 2006
+ * Better bzr compatibility
+ * Auto-commit support
+
+Feb 3, 2006
+ * BeWeb can merge, commit, etc.
+
+Jan 30, 2006
+ * Creator support (Alexander Belchenko)
+
+Jan 26, 2006
+ * Unicode support
+
+December 3, 2005
+* Added new "beweb" web interface
diff --git a/README b/README
index 6bd04e5..ef597bb 100644
--- a/README
+++ b/README
@@ -1,20 +1,71 @@
--*- markdown -*-
+Bugs Everywhere
+===============
-Cherry Flavored Bugs Everywhere
-===============================
+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 VCS at
+all.
-CFBE is a quick web interface to [BugsEverywhere](http://bugseverywhere.org/). It's still very much a work-in-progress.
+The idea is to package the bug information with the source code, so that
+bugs can be marked "fixed" in the branches that fix them. So, instead of
+numbers, bugs have globally unique ids.
-Installing
-----------
-I intend to streamline the installation once I'm satisfied with the interface itself. For now, the install process goes something like this:
+Getting BE
+==========
-* Install [CherryPy](http://cherrypy.org/) if you don't have it.
-* Install [Jinja2](http://jinja.pocoo.org/2/) if you don't have it.
-* Install [BugsEverywhere](http://bugseverywhere.org/) if you don't have it.
-* Download a zip/tar of CFBE (or hg clone) from the [Mercurial repository](http://bitbucket.org/sjl/cherryflavoredbugseverywhere/).
-* Unzip (if you grabbed a zip) and put the folder in your Python site-packages directory (or put it anywhere and symlink it to site-packages).
-* Symlink `site-packages/cherryflavoredbugseverywhere/cfbe.py` to `/usr/local/bin/cfbe`
-* Use `cfbe [project_root]` to start up the web interface for that project.
-* Visit http://localhost:8080/ in a browser.
+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 -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``.
+
+
+Documentation
+=============
+
+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.
+
+.. _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/be b/be
new file mode 100755
index 0000000..8c7f41c
--- /dev/null
+++ b/be
@@ -0,0 +1,21 @@
+#!/usr/bin/env 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 sys
+import libbe.ui.command_line
+
+sys.exit(libbe.ui.command_line.main())
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..6d7bbc5
--- /dev/null
+++ b/doc/Makefile
@@ -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
index 0000000..1090a28
--- /dev/null
+++ b/doc/conf.py
@@ -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
index 0000000..5ca42a6
--- /dev/null
+++ b/doc/distributed_bugtracking.txt
@@ -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
index 0000000..e1b7a3a
--- /dev/null
+++ b/doc/doc.txt
@@ -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
index 0000000..b25875f
--- /dev/null
+++ b/doc/email.txt
@@ -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
index 0000000..35eb5c4
--- /dev/null
+++ b/doc/generate-libbe-txt.py
@@ -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
index 0000000..5b075f9
--- /dev/null
+++ b/doc/hacking.txt
@@ -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
index 0000000..9e7f114
--- /dev/null
+++ b/doc/html.txt
@@ -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
index 0000000..30b0318
--- /dev/null
+++ b/doc/index.txt
@@ -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
index 0000000..b1d153e
--- /dev/null
+++ b/doc/install.txt
@@ -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
diff --git a/doc/man/be.1.sgml b/doc/man/be.1.sgml
new file mode 100644
index 0000000..a2df21f
--- /dev/null
+++ b/doc/man/be.1.sgml
@@ -0,0 +1,134 @@
+<!doctype refentry PUBLIC "-//OASIS//DTD DocBook V4.1//EN" [
+
+<!-- Process this file with docbook-to-man to generate an nroff manual
+ page: `docbook-to-man manpage.sgml > manpage.1'. You may view
+ the manual page with: `docbook-to-man manpage.sgml | nroff -man |
+ less'. A typical entry in a Makefile or Makefile.am is:
+
+be.1: be.1.sgml
+ docbook-to-man $< > $@
+
+ The docbook-to-man binary is found in the docbook-to-man package.
+ Please remember that if you create the nroff version in one of the
+ debian/rules file targets (such as build), you will need to include
+ docbook-to-man in your Build-Depends control field.
+
+ -->
+
+ <!ENTITY dhfirstname "<firstname>Ben</firstname>">
+ <!ENTITY dhsurname "<surname>Finney</surname>">
+ <!-- Please adjust the date whenever revising the manpage. -->
+ <!ENTITY dhdate "<date>2009-06-25</date>">
+ <!-- SECTION should be 1-8, maybe w/ subsection other parameters are
+ allowed: see man(7), man(1). -->
+ <!ENTITY dhsection "<manvolnum>1</manvolnum>">
+ <!ENTITY dhemail "<email>ben+debian@benfinney.id.au</email>">
+ <!ENTITY dhusername "Ben Finney">
+ <!ENTITY dhucpackage "<refentrytitle>BUGS-EVERYWHERE</refentrytitle>">
+ <!ENTITY dhpackage "bugs-everywhere">
+ <!ENTITY pkgfullname "Bugs Everywhere">
+ <!ENTITY uccmdname "<refentrytitle>BE</refentrytitle>">
+ <!ENTITY cmdname "be">
+
+ <!ENTITY debian "<productname>Debian</productname>">
+ <!ENTITY gnu "<acronym>GNU</acronym>">
+ <!ENTITY gpl "&gnu; <acronym>GPL</acronym>">
+]>
+
+<refentry>
+ <refentryinfo>
+ <address>
+ &dhemail;
+ </address>
+ <author>
+ &dhfirstname;
+ &dhsurname;
+ </author>
+ <copyright>
+ <year>2009</year>
+ <holder>&dhusername;</holder>
+ </copyright>
+ &dhdate;
+ </refentryinfo>
+ <refmeta>
+ &uccmdname;
+
+ &dhsection;
+ </refmeta>
+ <refnamediv>
+ <refname>&cmdname;</refname>
+
+ <refpurpose>distributed bug tracker</refpurpose>
+ </refnamediv>
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>&cmdname;</command>
+ <arg><replaceable>command</replaceable></arg>
+ <arg><replaceable>command_options ...</replaceable></arg>
+ <arg><replaceable>command_args ...</replaceable></arg>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>&cmdname; help</command>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>&cmdname; help</command>
+ <arg><replaceable>command</replaceable></arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+ <refsect1>
+ <title>DESCRIPTION</title>
+
+ <para>This manual page documents briefly the
+ <command>&cmdname;</command> command, part of the
+ &pkgfullname; package.</para>
+
+ <para><command>&cmdname;</command> allows commandline interaction
+ with the &pkgfullname; database in a project tree.</para>
+
+ </refsect1>
+ <refsect1>
+ <title>COMMANDS</title>
+ <variablelist>
+ <varlistentry>
+ <term><command>help</command>
+ </term>
+ <listitem>
+ <para>Print help for be and a list of all available commands.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+ <refsect1>
+ <title>AUTHOR</title>
+
+ <para>This manual page was written by &dhusername; <&dhemail;> for
+ the &debian; system (but may be used by others). Permission is
+ granted to copy, distribute and/or modify this document under
+ the terms of the &gnu; General Public License, Version 2 or any
+ later version published by the Free Software Foundation.
+ </para>
+ <para>
+ On Debian systems, the complete text of the GNU General Public
+ License can be found in /usr/share/common-licenses/GPL.
+ </para>
+
+ </refsect1>
+</refentry>
+
+<!-- Keep this comment at the end of the file
+Local variables:
+mode: sgml
+sgml-omittag:t
+sgml-shorttag:t
+sgml-minimize-attributes:nil
+sgml-always-quote-attributes:t
+sgml-indent-step:2
+sgml-indent-data:t
+sgml-parent-document:nil
+sgml-default-dtd-file:nil
+sgml-exposed-tags:nil
+sgml-local-catalogs:nil
+sgml-local-ecat-files:nil
+End:
+-->
diff --git a/doc/spam.txt b/doc/spam.txt
new file mode 100644
index 0000000..39e7a86
--- /dev/null
+++ b/doc/spam.txt
@@ -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
index 0000000..7932c9c
--- /dev/null
+++ b/doc/tutorial.txt
@@ -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/email/interactive/README b/interfaces/email/interactive/README
new file mode 100644
index 0000000..48bccdd
--- /dev/null
+++ b/interfaces/email/interactive/README
@@ -0,0 +1,160 @@
+***************
+Email Interface
+***************
+
+Overview
+========
+
+The interactive email interface to Bugs Everywhere (BE) attempts to
+provide a `Debian-bug-tracking-system-style`_ interface to a BE
+repository. Users can mail in bug reports, comments, or control
+requests, which will be committed to the served repository.
+Developers can then pull the changes they approve of from the served
+repository into their other repositories and push updates back onto
+the served repository.
+
+.. _Debian-bug-tracking-system-style: http://www.debian.org/Bugs
+
+Architecture
+============
+
+In order to reduce setup costs, the entire interface can piggyback on
+an existing email address, although from a security standpoint it's
+probably best to create a dedicated user. Incoming email is filtered
+by procmail, with matching emails being piped into ``be-handle-mail``
+for execution.
+
+Once ``be-handle-mail`` receives the email, the parsing method is
+selected according to the subject tag that procmail used grab the
+email in the first place. There are 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.::
+
+ From jdoe@example.com Fri Apr 18 12:00:00 2008
+ From: John Doe <jdoe@example.com>
+ Date: Fri, 18 Apr 2008 12:00:00 +0000
+ Content-Type: text/plain; charset=UTF-8
+ Content-Transfer-Encoding: 8bit
+ Subject: [be-bug:submit] Need tests for the email interface.
+
+ Version: XYZ
+ Severity: minor
+
+ Someone should write up a series of test emails to send into
+ be-handle-mail so we can test changes quickly without having to
+ use procmail.
+
+ --
+ Goofy tagline not included.
+
+Available pseudo-headers are ``Version``, ``Reporter``, ``Assign``,
+``Depend``, ``Severity``, ``Status``, ``Tag``, and ``Target``.
+
+Commenting on bugs
+==================
+
+This interface appends a comment to the bug specified in the subject
+tag. The the first non-multipart body is attached with the
+appropriate content-type. In the case of ``text/plain`` contents,
+anything following a line starting with ``--`` is stripped.::
+
+ From jdoe@example.com Fri Apr 18 12:00:00 2008
+ From: John Doe <jdoe@example.com>
+ Date: Fri, 18 Apr 2008 12:00:00 +0000
+ Content-Type: text/plain; charset=UTF-8
+ Content-Transfer-Encoding: 8bit
+ Subject: [be-bug:XYZ] Isolated problem in baz()
+
+ Finally tracked it down to the bar() call. Some sort of
+ string<->unicode conversion problem. Solution ideas?
+
+ --
+ Goofy tagline not included.
+
+Controlling bugs
+================
+
+This interface consists of a list of allowed be commands, with one
+command per line. Blank lines and lines beginning with ``#`` are
+ignored, as well anything following a line starting with ``--``. All
+the listed commands are executed in order and their output returned.
+The commands are split into arguments with the POSIX-compliant
+shlex.split().::
+
+ From jdoe@example.com Fri Apr 18 12:00:00 2008
+ From: John Doe <jdoe@example.com>
+ Date: Fri, 18 Apr 2008 12:00:00 +0000
+ Content-Type: text/plain; charset=UTF-8
+ Content-Transfer-Encoding: 8bit
+ Subject: [be-bug] I'll handle XYZ by release 1.2.3
+
+ assign XYZ "John Doe <jdoe@example.com>"
+ status XYZ assigned
+ severity XYZ critical
+ target XYZ 1.2.3
+
+ --
+ Goofy tagline ignored.
+
+Example emails
+==============
+
+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``.
+
+If you're piggybacking the interface on top of an existing account,
+you probably only need to add the ``be-handle-mail`` stanza to your
+existing ``~/.procmailrc``, since you will still want to receive
+non-bug emails.
+
+Note that you will probably have to add a::
+
+ --repo /path/to/served/repository
+
+option to the ``be-handle-mail`` invocation so it knows what repository to
+serve.
+
+Multiple repositories may be served by the same email address by adding
+multiple ``be-handle-mail`` stanzas, each matching a different tag, for
+example the ``[be-bug`` portion of the stanza could be ``[projectX-bug``,
+``[projectY-bug``, etc. If you change the base tag, be sure to add a::
+
+ --tag-base "projectX-bug"
+
+or equivalent to your ``be-handle-mail`` invocation.
+
+Testing
+=======
+
+Send test emails in to ``be-handle-mail`` with something like::
+
+ cat examples/blank | ./be-handle-mail -o -l - -a
diff --git a/interfaces/email/interactive/_procmailrc b/interfaces/email/interactive/_procmailrc
new file mode 100644
index 0000000..d42c0cf
--- /dev/null
+++ b/interfaces/email/interactive/_procmailrc
@@ -0,0 +1,22 @@
+# .procmailrc
+#
+# see man procmail, procmailrc, and procmailex
+#
+# If you already have a ~/.procmailrc file, you probably only need to
+# insert the bug-email grabbing stanza in your ~/.procmailrc.
+#
+# This file is released to the Public Domain.
+
+MAILDIR=$HOME/be-mail
+LOGFILE=$MAILDIR/procmail.log
+
+# Grab all incoming bug emails (but not replies). This rule eats
+# matching emails (i.e. no further procmail processing).
+:0
+* ^Subject: \[be-bug
+* !^Subject:.*\[be-bug].*Re:
+| be-handle-mail
+
+# Drop everything else
+:0
+/dev/null
diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail
new file mode 100755
index 0000000..c8343fc
--- /dev/null
+++ b/interfaces/email/interactive/be-handle-mail
@@ -0,0 +1,909 @@
+#!/usr/bin/env 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.
+"""
+Provide and email interface to the distributed bugtracker Bugs
+Everywhere. Recieves incoming email via procmail. Provides an
+interface similar to the Debian Bug Tracker. There are currently
+three distinct email types: submits, comments, and controls. The
+email types are differentiated by tags in the email subject. See
+SUBJECT_TAG* for the current values.
+
+Submit emails create a bug (and optionally add some intitial
+comments). The post-tag subject is used as the bug summary, and the
+email body is parsed for a pseudo-header. Any text after the
+psuedo-header but before a possible line starting with BREAK is added
+as the initial bug comment.
+
+Comment emails add comments to a bug. The first non-multipart portion
+of the email is used as the comment body. If that portion has a
+"text/plain" type, any text after and including a possible line
+starting with BREAK is stripped to avoid lots of taglines cluttering
+up the repository.
+
+Control emails preform any allowed BE commands. The first
+non-multipart portion of the email is used as the comment body. If
+that portion has a "text/plain" type, any text after and including a
+possible line starting with BREAK is stripped. Each pre-BREAK line of
+the portion should be a valid BE command, with the initial "be"
+omitted, e.g. "be status XYZ fixed" --> "status XYZ fixed".
+
+Any changes made to the repository are commited after the email is
+executed, with the email's post-tag subject as the commit message.
+"""
+
+import codecs
+import StringIO as StringIO
+import email
+from email.mime.multipart import MIMEMultipart
+import email.utils
+import os
+import os.path
+import re
+import shlex
+import sys
+import time
+import traceback
+import types
+import doctest
+import unittest
+
+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>'
+UI = None
+_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
+LOGPATH = os.path.join(_THIS_DIR, u'be-handle-mail.log')
+LOGFILE = None
+
+# Tag strings generated by generate_global_tags()
+SUBJECT_TAG_BASE = u'be-bug'
+SUBJECT_TAG_RESPONSE = None
+SUBJECT_TAG_START = None
+SUBJECT_TAG_NEW = None
+SUBJECT_TAG_COMMENT = None
+SUBJECT_TAG_CONTROL = None
+
+BREAK = u'--'
+NEW_REQUIRED_PSEUDOHEADERS = [u'Version']
+NEW_OPTIONAL_PSEUDOHEADERS = [u'Reporter', u'Assign', u'Depend', u'Severity',
+ u'Status', u'Tag', u'Target',
+ u'Confirm', u'Subscribe']
+CONTROL_COMMENT = u'#'
+ALLOWED_COMMANDS = [u'assign', u'comment', u'commit', u'depend', u'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
+
+ENCODING = u'utf-8'
+libbe.util.encoding.ENCODING = ENCODING # force default encoding
+
+class InvalidEmail (ValueError):
+ def __init__(self, msg, message):
+ ValueError.__init__(self, message)
+ self.msg = msg
+ def response(self):
+ header = self.msg.response_header
+ body = [u'Error processing email:\n',
+ self.response_body(), u'']
+ response_generator = \
+ send_pgp_mime.PGPMimeMessageFactory(u'\n'.join(body))
+ response = MIMEMultipart()
+ response.attach(response_generator.plain())
+ response.attach(self.msg.msg)
+ ret = send_pgp_mime.attach_root(header, response)
+ return ret
+ def response_body(self):
+ err_text = [unicode(self)]
+ return u'\n'.join(err_text)
+
+class InvalidSubject (InvalidEmail):
+ def __init__(self, msg, message=None):
+ if message == None:
+ message = u'Invalid subject'
+ InvalidEmail.__init__(self, msg, message)
+ def response_body(self):
+ err_text = u'\n'.join([unicode(self), u'',
+ u'full subject was:',
+ self.msg.subject()])
+ return err_text
+
+class InvalidPseudoHeader (InvalidEmail):
+ def response_body(self):
+ err_text = [u'Invalid pseudo-header:\n',
+ unicode(self)]
+ return u'\n'.join(err_text)
+
+class InvalidCommand (InvalidEmail):
+ def __init__(self, msg, command, message=None):
+ bigmessage = u'Invalid execution command "%s"' % command
+ if message != None:
+ bigmessage += u'\n%s' % message
+ InvalidEmail.__init__(self, msg, bigmessage)
+ self.command = command
+
+class InvalidOption (InvalidCommand):
+ def __init__(self, msg, option, message=None):
+ bigmessage = u'Invalid option "%s"' % (option)
+ if message != None:
+ bigmessage += u'\n%s' % message
+ InvalidCommand.__init__(self, msg, info, command, bigmessage)
+ self.option = option
+
+class NotificationFailed (Exception):
+ def __init__(self, msg):
+ bigmessage = 'Notification failed: %s' % msg
+ Exception.__init__(self, bigmessage)
+ self.short_msg = msg
+
+class ID (object):
+ """
+ Sometimes you want to reference the output of a command that
+ hasn't been executed yet. ID is there for situations like
+ > a = Command(msg, "new", ["create a bug"])
+ > b = Command(msg, "comment", [ID(a), "and comment on it"])
+ """
+ def __init__(self, command):
+ self.command = command
+ def extract_id(self):
+ if hasattr(self, 'cached_id'):
+ return self._cached_id
+ assert self.command.ret == 0, self.command.ret
+ if self.command.command.name == u'new':
+ regexp = re.compile(u'Created bug with ID (.*)')
+ else:
+ raise NotImplementedError, self.command.command
+ match = regexp.match(self.command.stdout)
+ assert len(match.groups()) == 1, str(match.groups())
+ self._cached_id = match.group(1)
+ return self._cached_id
+ def __str__(self):
+ if self.command.ret != 0:
+ return '<id for %s>' % repr(self.command)
+ return '<id %s>' % self.extract_id()
+
+class Command (object):
+ """
+ A libbe.command.Command handler.
+
+ Initialize with
+ Command(msg, command, args=None, stdin=None)
+ where
+ msg: the Message instance prompting this command
+ command: name of becommand to execute, e.g. "new"
+ args: list of arguments to pass to the command
+ stdin: if non-null, a string to pipe into the command's stdin
+ """
+ def __init__(self, msg, command, args=None, stdin=None):
+ self.msg = msg
+ if args == None:
+ self.args = []
+ else:
+ self.args = args
+ 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
+ def __str__(self):
+ 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.
+ """
+ for i,arg in enumerate(self.args):
+ if isinstance(arg, ID):
+ self.args[i] = arg.extract_id()
+ def run(self):
+ """
+ Attempt to execute the command whose info is given in the dictionary
+ info. Returns the exit code, stdout, and stderr produced by the
+ command.
+ """
+ if self.command.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()
+ 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.name,u' '.join(self.args))]
+ if self.stdout != None and len(self.stdout) > 0:
+ 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))
+ return response_generator.plain()
+
+class DiffTree (libbe.diff.DiffTree):
+ """
+ In order to avoid tons of tiny MIMEText attachments, bug-level
+ nodes set .add_child_text=True (in .join()), which is propogated
+ on to their descendents. Instead of creating their own
+ attachement, each of these descendents appends his data_part to
+ the end of the bug-level MIMEText attachment.
+
+ For the example tree in the libbe.diff.Diff unittests:
+ bugdir
+ bugdir/settings
+ bugdir/bugs
+ bugdir/bugs/new
+ bugdir/bugs/new/c <- sets .add_child_text
+ bugdir/bugs/rem
+ bugdir/bugs/rem/b <- sets .add_child_text
+ bugdir/bugs/mod
+ bugdir/bugs/mod/a <- sets .add_child_text
+ bugdir/bugs/mod/a/settings
+ bugdir/bugs/mod/a/comments
+ bugdir/bugs/mod/a/comments/new
+ bugdir/bugs/mod/a/comments/new/acom
+ bugdir/bugs/mod/a/comments/rem
+ bugdir/bugs/mod/a/comments/mod
+ """
+ def report_or_none(self):
+ report = self.report()
+ if report == None:
+ return None
+ payload = report.get_payload()
+ if payload == None or len(payload) == 0:
+ return None
+ return report
+ def report_string(self):
+ report = self.report_or_none()
+ if report == None:
+ return 'No changes'
+ else:
+ return send_pgp_mime.flatten(report, to_unicode=True)
+ def make_root(self):
+ return MIMEMultipart()
+ def join(self, root, parent, data_part):
+ if hasattr(parent, 'attach_child_text'):
+ self.attach_child_text = True
+ if data_part != None:
+ send_pgp_mime.append_text(parent.data_mime_part, u'\n\n%s' % (data_part))
+ self.data_mime_part = parent.data_mime_part
+ else:
+ self.data_mime_part = None
+ if data_part != None:
+ self.data_mime_part = send_pgp_mime.encodedMIMEText(data_part)
+ if parent != None and parent.name in [u'new', u'rem', u'mod']:
+ self.attach_child_text = True
+ if data_part == None: # make blank data_mime_part for children's appends
+ self.data_mime_part = send_pgp_mime.encodedMIMEText(u'')
+ if self.data_mime_part != None:
+ self.data_mime_part[u'Content-Description'] = self.name
+ root.attach(self.data_mime_part)
+ def data_part(self, depth, indent=False):
+ return libbe.diff.DiffTree.data_part(self, depth, indent=indent)
+
+class Diff (libbe.diff.Diff):
+ def bug_add_string(self, bug):
+ return bug.string(show_comments=True)
+ def _comment_summary_string(self, comment):
+ return comment.string()
+ def comment_add_string(self, comment):
+ return self._comment_summary_string(comment)
+ def comment_rem_string(self, comment):
+ return self._comment_summary_string(comment)
+
+class Message (object):
+ def __init__(self, email_text=None, disable_parsing=False):
+ if disable_parsing == False:
+ self.text = email_text
+ p=email.Parser.Parser()
+ self.msg=p.parsestr(self.text)
+ if LOGFILE != None:
+ LOGFILE.write(u'handling %s\n' % self.author_addr())
+ LOGFILE.write(u'\n%s\n\n' % self.text)
+ self.confirm = True # enable/disable confirmation email
+ def _yes_no(self, boolean):
+ if boolean == True:
+ return 'yes'
+ return 'no'
+ def author_tuple(self):
+ """
+ Extract and normalize the sender's email address. Returns a
+ (name, email) tuple.
+ """
+ if not hasattr(self, 'author_tuple_cache'):
+ self._author_tuple_cache = \
+ send_pgp_mime.source_email(self.msg, return_realname=True)
+ return self._author_tuple_cache
+ def author_addr(self):
+ return email.utils.formataddr(self.author_tuple())
+ def author_name(self):
+ return self.author_tuple()[0]
+ def author_email(self):
+ return self.author_tuple()[1]
+ def default_msg_attribute_access(self, attr_name, default=None):
+ if attr_name in self.msg:
+ return self.msg[attr_name]
+ return default
+ def message_id(self, default=None):
+ return self.default_msg_attribute_access('message-id', default=default)
+ def subject(self):
+ if 'subject' not in self.msg:
+ raise InvalidSubject(self, u'Email must contain a subject')
+ return self.msg['subject']
+ def _split_subject(self):
+ """
+ Returns (tag, subject), with missing values replaced by None.
+ """
+ if hasattr(self, '_split_subject_cache'):
+ return self._split_subject_cache
+ args = self.subject().split(u']',1)
+ if len(args) < 1:
+ self._split_subject_cache = (None, None)
+ elif len(args) < 2:
+ self._split_subject_cache = (args[0]+u']', None)
+ else:
+ self._split_subject_cache = (args[0]+u']', args[1].strip())
+ return self._split_subject_cache
+ def _subject_tag_type(self):
+ """
+ Parse subject tag, return (type, value), where type is one of
+ None, "new", "comment", or "control"; and value is None except
+ in the case of "comment", in which case it's the bug
+ ID/shortname.
+ """
+ tag,subject = self._split_subject()
+ type = None
+ value = None
+ if tag == SUBJECT_TAG_NEW:
+ type = u'new'
+ elif tag == SUBJECT_TAG_CONTROL:
+ type = u'control'
+ else:
+ match = SUBJECT_TAG_COMMENT.match(tag)
+ if len(match.groups()) == 1:
+ type = u'comment'
+ value = match.group(1)
+ return (type, value)
+ def validate_subject(self):
+ """
+ Validate the subject line.
+ """
+ tag,subject = self._split_subject()
+ if not tag.startswith(SUBJECT_TAG_START):
+ raise InvalidSubject(
+ self, u'Subject must start with "%s"' % SUBJECT_TAG_START)
+ tag_type,value = self._subject_tag_type()
+ if tag_type == None:
+ raise InvalidSubject(self, u'Invalid tag "%s"' % tag)
+ elif tag_type == u'new' and len(subject) == 0:
+ raise InvalidSubject(self, u'Cannot create a bug with blank title')
+ elif tag_type == u'comment' and len(value) == 0:
+ raise InvalidSubject(self, u'Must specify a bug ID to comment')
+ def _get_bodies_and_mime_types(self):
+ """
+ Traverse the email message returning (body, mime_type) for
+ each non-mulitpart portion of the message.
+ """
+ msg_charset = self.msg.get_content_charset(ENCODING).lower()
+ for part in self.msg.walk():
+ if part.is_multipart():
+ continue
+ body,mime_type=(part.get_payload(decode=True),part.get_content_type())
+ charset = part.get_content_charset(msg_charset).lower()
+ if mime_type.startswith('text/'):
+ body = unicode(body, charset) # convert text types to unicode
+ yield (body, mime_type)
+ def _parse_body_pseudoheaders(self, body, required, optional,
+ dictionary=None):
+ """
+ Grab any pseudo-headers from the beginning of body. Raise
+ InvalidPseudoHeader on errors. Returns the body text after
+ the pseudo-header and a dictionary of set options. If you
+ like, you can initialize the dictionary with some defaults
+ and pass your initialized dict in as dictionary.
+ """
+ if dictionary == None:
+ dictionary = {}
+ body_lines = body.splitlines()
+ all = required+optional
+ for i,line in enumerate(body_lines):
+ line = line.strip()
+ if len(line) == 0:
+ break
+ if ':' not in line:
+ raise InvalidPseudoheader(self, line)
+ key,value = line.split(':', 1)
+ value = value.strip()
+ if key not in all:
+ raise InvalidPseudoHeader(self, key)
+ if len(value) == 0:
+ raise InvalidEmail(
+ self, u'Blank value for: %s' % key)
+ dictionary[key] = value
+ missing = []
+ for key in required:
+ if key not in dictionary:
+ missing.append(key)
+ if len(missing) > 0:
+ raise InvalidPseudoHeader(self,
+ u'Missing required pseudo-headers:\n%s'
+ % u', '.join(missing))
+ remaining_body = u'\n'.join(body_lines[i:]).strip()
+ return (remaining_body, dictionary)
+ def _strip_footer(self, body):
+ body_lines = body.splitlines()
+ for i,line in enumerate(body_lines):
+ if line.startswith(BREAK):
+ break
+ i += 1 # increment past the current valid line.
+ return u'\n'.join(body_lines[:i]).strip()
+ def parse(self):
+ """
+ Parse the commands given in the email. Raises assorted
+ subclasses of InvalidEmail in the case of invalid messages,
+ otherwise returns a list of suggested commands to run.
+ """
+ self.validate_subject()
+ tag_type,value = self._subject_tag_type()
+ if tag_type == u'new':
+ commands = self.parse_new()
+ elif tag_type == u'comment':
+ commands = self.parse_comment(value)
+ elif tag_type == u'control':
+ commands = self.parse_control()
+ else:
+ raise Exception, u'Unrecognized tag type "%s"' % tag_type
+ return commands
+ def parse_new(self):
+ command = u'new'
+ tag,subject = self._split_subject()
+ summary = subject
+ options = {u'Reporter': self.author_addr(),
+ u'Confirm': self._yes_no(self.confirm),
+ u'Subscribe': 'no',
+ }
+ body,mime_type = list(self._get_bodies_and_mime_types())[0]
+ comment_body,options = \
+ self._parse_body_pseudoheaders(body,
+ NEW_REQUIRED_PSEUDOHEADERS,
+ NEW_OPTIONAL_PSEUDOHEADERS,
+ options)
+ if options[u'Confirm'].lower() == 'no':
+ self.confirm = False
+ if options[u'Subscribe'].lower() == 'yes' and self.confirm == True:
+ # respond with the subscription format rather than the
+ # normal command-output format, because the subscription
+ # format is more user-friendly.
+ self.confirm = False
+ args = [u'--reporter', options[u'Reporter']]
+ args.append(summary)
+ commands = [Command(self, command, args)]
+ id = ID(commands[0])
+ comment_body = self._strip_footer(comment_body)
+ if len(comment_body) > 0:
+ command = u'comment'
+ comment = u'Version: %s\n\n'%options[u'Version'] + comment_body
+ args = [u'--author', self.author_addr(),
+ u'--alt-id', self.message_id(),
+ u'--content-type', mime_type]
+ args.append(id)
+ args.append(u'-')
+ commands.append(Command(self, u'comment', args, stdin=comment))
+ for key,value in options.items():
+ if key in [u'Version', u'Reporter', u'Confirm']:
+ continue # we've already handled these options
+ command = key.lower()
+ 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]
+ commands.append(Command(self, command, args))
+ return commands
+ def parse_comment(self, bug_uuid):
+ command = u'comment'
+ bug_id = bug_uuid
+ author = self.author_addr()
+ alt_id = self.message_id()
+ body,mime_type = list(self._get_bodies_and_mime_types())[0]
+ if mime_type == 'text/plain':
+ body = self._strip_footer(body)
+ content_type = mime_type
+ args = [u'--author', author]
+ 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):
+ body,mime_type = list(self._get_bodies_and_mime_types())[0]
+ commands = []
+ for line in body.splitlines():
+ line = line.strip()
+ if line.startswith(CONTROL_COMMENT) or len(line) == 0:
+ continue
+ if line.startswith(BREAK):
+ break
+ 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.')
+ return commands
+ def run(self, repo='.'):
+ self._begin_response()
+ commands = self.parse()
+ try:
+ 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.run()
+ if LOGFILE != None:
+ LOGFILE.write(u'Autocommit:\n%s\n\n' %
+ send_pgp_mime.flatten(self.commit_command.response_msg(),
+ to_unicode=True))
+ def _begin_response(self):
+ tag,subject = self._split_subject()
+ response_header = [u'From: %s' % THIS_ADDRESS,
+ u'To: %s' % self.author_addr(),
+ u'Date: %s' % libbe.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())
+ self.response_header = \
+ send_pgp_mime.header_from_text(text=u'\n'.join(response_header))
+ self._response_messages = []
+ def _add_response(self, response_message):
+ self._response_messages.append(response_message)
+ def response_email(self):
+ assert len(self._response_messages) > 0
+ if len(self._response_messages) == 1:
+ response_body = self._response_messages[0]
+ else:
+ response_body = MIMEMultipart()
+ for message in self._response_messages:
+ response_body.attach(message)
+ return send_pgp_mime.attach_root(self.response_header, response_body)
+ def subscriber_emails(self, previous_revision=None):
+ if previous_revision == None:
+ if AUTOCOMMIT != True: # no way to tell what's changed
+ raise NotificationFailed('Autocommit dissabled')
+ if len(self._response_messages) == 0:
+ raise NotificationFailed('Initial email failed.')
+ if self.commit_command.ret != 0:
+ # commit failed. Error already logged.
+ raise NotificationFailed('Commit failed')
+
+ 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:
+ 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.full_report(diff_tree=DiffTree)
+ header = self._subscriber_header(bd, previous_revision)
+
+ emails = []
+ for subscriber,subscriptions in subscribers.items():
+ 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.storage.revision_id(-2)
+ else:
+ before_revision = previous_revision
+ if before_revision == None:
+ # this commit was the initial commit
+ before_bd = libbe.bugdir.BugDir(from_disk=False,
+ manipulate_encodings=False)
+ else:
+ before_bd = 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.storage.repo)
+ if previous_revision == None:
+ subject = 'Changes to %s on %s by %s' \
+ % (root_dir, THIS_SERVER, self.author_addr())
+ else:
+ subject = 'Changes to %s on %s since revision %s' \
+ % (root_dir, THIS_SERVER, previous_revision)
+ header = [u'From: %s' % THIS_ADDRESS,
+ u'To: %s' % u'DUMMY-AUTHOR',
+ u'Date: %s' % libbe.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))
+
+def generate_global_tags(tag_base=u'be-bug'):
+ """
+ Generate a series of tags from a base tag string.
+ """
+ global SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \
+ SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL
+ SUBJECT_TAG_BASE = tag_base
+ SUBJECT_TAG_START = u'[%s' % tag_base
+ SUBJECT_TAG_RESPONSE = u'[%s]' % tag_base
+ SUBJECT_TAG_NEW = u'[%s:submit]' % tag_base
+ SUBJECT_TAG_COMMENT = re.compile(u'\[%s:([\-0-9a-z/]*)]' % tag_base)
+ SUBJECT_TAG_CONTROL = SUBJECT_TAG_RESPONSE
+
+def open_logfile(logpath=None):
+ """
+ If logpath=None, default to global LOGPATH.
+ Special logpath strings:
+ "-" set LOGFILE to sys.stderr
+ "none" disable logging
+ Relative logpaths are expanded relative to _THIS_DIR
+ """
+ global LOGPATH, LOGFILE
+ if logpath != None:
+ if logpath == u'-':
+ LOGPATH = u'stderr'
+ LOGFILE = sys.stderr
+ elif logpath == u'none':
+ LOGPATH = u'none'
+ LOGFILE = None
+ elif os.path.isabs(logpath):
+ LOGPATH = logpath
+ else:
+ LOGPATH = os.path.join(_THIS_DIR, logpath)
+ if LOGFILE == None and LOGPATH != u'none':
+ LOGFILE = codecs.open(LOGPATH, u'a+',
+ libbe.util.encoding.get_filesystem_encoding())
+
+def close_logfile():
+ if LOGFILE != None and LOGPATH not in [u'stderr', u'none']:
+ LOGFILE.close()
+
+def test():
+ result = unittest.TextTestRunner(verbosity=2).run(suite)
+ num_errors = len(result.errors)
+ num_failures = len(result.failures)
+ num_bad = num_errors + num_failures
+ return num_bad
+
+def main(args):
+ from optparse import OptionParser
+ global AUTOCOMMIT, UI
+
+ usage='be-handle-mail [options]\n\n%s' % (__doc__)
+ parser = OptionParser(usage=usage)
+ 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',
+ help='Set the subject tag base (%default).')
+ parser.add_option('-o', '--output', dest='output', action='store_true',
+ help="Don't mail the generated message, print it to stdout instead. Useful for testing be-handle-mail functionality without the whole mail transfer agent and procmail setup.")
+ parser.add_option('-l', '--logfile', dest='logfile', metavar='LOGFILE',
+ help='Set the logfile to LOGFILE. Relative paths are relative to the location of this be-handle-mail file (%s). The special value of "-" directs the log output to stderr, and "none" disables logging.' % _THIS_DIR)
+ parser.add_option('-a', '--disable-autocommit', dest='autocommit',
+ default=True, action='store_false',
+ help='Disable the autocommit after parsing the email.')
+ parser.add_option('-s', '--disable-subscribers', dest='subscribers',
+ default=True, action='store_false',
+ help='Disable subscriber notification emails.')
+ parser.add_option('--notify-since', dest='notify_since', metavar='REVISION',
+ help='Notify subscribers of all changes since REVISION. When this option is set, no input email parsing is done.')
+ parser.add_option('--test', dest='test', action='store_true',
+ help='Run internal unit-tests and exit.')
+
+ pargs = args
+ options,args = parser.parse_args(args[1:])
+
+ if options.test == True:
+ num_bad = test()
+ if num_bad > 126:
+ num_bad = 1
+ sys.exit(num_bad)
+
+ AUTOCOMMIT = options.autocommit
+
+ if options.notify_since == None:
+ msg_text = sys.stdin.read()
+
+ 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'
+ % options.notify_since)
+ try:
+ m = Message(disable_parsing=True)
+ emails = m.subscriber_emails(options.notify_since)
+ except NotificationFailed, e:
+ if LOGFILE != None:
+ LOGFILE.write(unicode(e) + u'\n')
+ else:
+ for msg in emails:
+ if options.output == True:
+ print send_pgp_mime.flatten(msg, to_unicode=True)
+ else:
+ send_pgp_mime.mail(msg, send_pgp_mime.sendmail)
+ close_logfile()
+ UI.cleanup()
+ sys.exit(0)
+
+ if len(msg_text.strip()) == 0: # blank email!?
+ if LOGFILE != None:
+ LOGFILE.write(u'Blank email!\n')
+ close_logfile()
+ UI.cleanup()
+ sys.exit(1)
+ try:
+ m = Message(msg_text)
+ m.run()
+ except InvalidEmail, e:
+ response = e.response()
+ except Exception, e:
+ if LOGFILE != None:
+ LOGFILE.write(u'Uncaught exception:\n%s\n' % (e,))
+ traceback.print_tb(sys.exc_traceback, file=LOGFILE)
+ close_logfile()
+ m.commit_command.cleanup()
+ UI.cleanup()
+ sys.exit(1)
+ else:
+ response = m.response_email()
+ if options.output == True:
+ print send_pgp_mime.flatten(response, to_unicode=True)
+ elif m.confirm == True:
+ if LOGFILE != None:
+ LOGFILE.write(u'Sending response to %s\n' % m.author_addr())
+ LOGFILE.write(u'\n%s\n\n' % send_pgp_mime.flatten(response,
+ to_unicode=True))
+ send_pgp_mime.mail(response, send_pgp_mime.sendmail)
+ else:
+ if LOGFILE != None:
+ LOGFILE.write(u'Response declined by %s\n' % m.author_addr())
+ if options.subscribers == True:
+ if LOGFILE != None:
+ LOGFILE.write(u'Checking for subscribers\n')
+ try:
+ emails = m.subscriber_emails()
+ except NotificationFailed, e:
+ if LOGFILE != None:
+ LOGFILE.write(unicode(e) + u'\n')
+ else:
+ for msg in emails:
+ if options.output == True:
+ print send_pgp_mime.flatten(msg, to_unicode=True)
+ else:
+ send_pgp_mime.mail(msg, send_pgp_mime.sendmail)
+
+ close_logfile()
+ m.commit_command.cleanup()
+ UI.cleanup()
+
+class GenerateGlobalTagsTestCase (unittest.TestCase):
+ def setUp(self):
+ super(GenerateGlobalTagsTestCase, self).setUp()
+ self.save_global_tags()
+ def tearDown(self):
+ self.restore_global_tags()
+ super(GenerateGlobalTagsTestCase, self).tearDown()
+ def save_global_tags(self):
+ self.saved_globals = [SUBJECT_TAG_BASE, SUBJECT_TAG_START,
+ SUBJECT_TAG_RESPONSE, SUBJECT_TAG_NEW,
+ SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL]
+ def restore_global_tags(self):
+ global SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \
+ SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL
+ SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \
+ SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL = \
+ self.saved_globals
+ def test_restore_global_tags(self):
+ "Test global tag restoration by teardown function."
+ global SUBJECT_TAG_BASE
+ self.failUnlessEqual(SUBJECT_TAG_BASE, u'be-bug')
+ SUBJECT_TAG_BASE = 'projectX-bug'
+ self.failUnlessEqual(SUBJECT_TAG_BASE, u'projectX-bug')
+ self.restore_global_tags()
+ self.failUnlessEqual(SUBJECT_TAG_BASE, u'be-bug')
+ def test_subject_tag_base(self):
+ "Should set SUBJECT_TAG_BASE global correctly"
+ generate_global_tags(u'projectX-bug')
+ self.failUnlessEqual(SUBJECT_TAG_BASE, u'projectX-bug')
+ def test_subject_tag_start(self):
+ "Should set SUBJECT_TAG_START global correctly"
+ generate_global_tags(u'projectX-bug')
+ self.failUnlessEqual(SUBJECT_TAG_START, u'[projectX-bug')
+ def test_subject_tag_response(self):
+ "Should set SUBJECT_TAG_RESPONSE global correctly"
+ generate_global_tags(u'projectX-bug')
+ self.failUnlessEqual(SUBJECT_TAG_RESPONSE, u'[projectX-bug]')
+ def test_subject_tag_new(self):
+ "Should set SUBJECT_TAG_NEW global correctly"
+ generate_global_tags(u'projectX-bug')
+ self.failUnlessEqual(SUBJECT_TAG_NEW, u'[projectX-bug:submit]')
+ def test_subject_tag_control(self):
+ "Should set SUBJECT_TAG_CONTROL global correctly"
+ generate_global_tags(u'projectX-bug')
+ self.failUnlessEqual(SUBJECT_TAG_CONTROL, u'[projectX-bug]')
+ def test_subject_tag_comment(self):
+ "Should set SUBJECT_TAG_COMMENT global correctly"
+ generate_global_tags(u'projectX-bug')
+ m = SUBJECT_TAG_COMMENT.match('[projectX-bug:abc/xyz-123]')
+ self.failUnlessEqual(len(m.groups()), 1)
+ 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/__init__.py b/interfaces/email/interactive/examples/blank
index e69de29..e69de29 100644
--- a/__init__.py
+++ b/interfaces/email/interactive/examples/blank
diff --git a/interfaces/email/interactive/examples/comment b/interfaces/email/interactive/examples/comment
new file mode 100644
index 0000000..f22e4b2
--- /dev/null
+++ b/interfaces/email/interactive/examples/comment
@@ -0,0 +1,11 @@
+From jdoe@example.com Fri Apr 18 11:18:58 2008
+Message-ID: <xyz@example.com>
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+From: John Doe <jdoe@example.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Subject: [be-bug:a1d] Subject ignored
+
+We sure do.
+--
+Goofy tagline ignored
diff --git a/interfaces/email/interactive/examples/email_bugs b/interfaces/email/interactive/examples/email_bugs
new file mode 100644
index 0000000..949e1c1
--- /dev/null
+++ b/interfaces/email/interactive/examples/email_bugs
@@ -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>
+
diff --git a/interfaces/email/interactive/examples/failing_multiples b/interfaces/email/interactive/examples/failing_multiples
new file mode 100644
index 0000000..cf50211
--- /dev/null
+++ b/interfaces/email/interactive/examples/failing_multiples
@@ -0,0 +1,16 @@
+From jdoe@example.com Fri Apr 18 12:00:00 2008
+Message-ID: <abcd@example.com>
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+From: John Doe <jdoe@example.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Subject: [be-bug] Commit message...
+
+new "test bug"
+new "test bug 2"
+failing-command
+new "test bug 3"
+
+--
+This message fails partway through, but the partial changes should be
+recorded in a commit...
diff --git a/interfaces/email/interactive/examples/invalid_command b/interfaces/email/interactive/examples/invalid_command
new file mode 100644
index 0000000..f2963c7
--- /dev/null
+++ b/interfaces/email/interactive/examples/invalid_command
@@ -0,0 +1,11 @@
+From jdoe@example.com Fri Apr 18 11:18:58 2008
+Message-ID: <abcd@example.com>
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+From: John Doe <jdoe@example.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Subject: [be-bug]
+
+close
+--
+Close is currently disabled for the email interface.
diff --git a/interfaces/email/interactive/examples/invalid_subject b/interfaces/email/interactive/examples/invalid_subject
new file mode 100644
index 0000000..1e2eb88
--- /dev/null
+++ b/interfaces/email/interactive/examples/invalid_subject
@@ -0,0 +1,9 @@
+From jdoe@example.com Fri Apr 18 11:18:58 2008
+Message-ID: <abcd@example.com>
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+From: John Doe <jdoe@example.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Subject: Spam!
+
+This should elicit an "invalid subject" response email.
diff --git a/interfaces/email/interactive/examples/list b/interfaces/email/interactive/examples/list
new file mode 100644
index 0000000..acba424
--- /dev/null
+++ b/interfaces/email/interactive/examples/list
@@ -0,0 +1,11 @@
+From jdoe@example.com Fri Apr 18 11:18:58 2008
+Message-ID: <abcd@example.com>
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+From: John Doe <jdoe@example.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Subject: [be-bug] Subject ignored
+
+list --status all
+--
+Dummy content
diff --git a/interfaces/email/interactive/examples/missing_command b/interfaces/email/interactive/examples/missing_command
new file mode 100644
index 0000000..bb390fc
--- /dev/null
+++ b/interfaces/email/interactive/examples/missing_command
@@ -0,0 +1,11 @@
+From jdoe@example.com Fri Apr 18 11:18:58 2008
+Message-ID: <abcd@example.com>
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+From: John Doe <jdoe@example.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Subject: [be-bug] Subject ignored
+
+abcde
+--
+This should elicit a "invalid command 'abcde'" response email.
diff --git a/interfaces/email/interactive/examples/multiple_commands b/interfaces/email/interactive/examples/multiple_commands
new file mode 100644
index 0000000..41ef730
--- /dev/null
+++ b/interfaces/email/interactive/examples/multiple_commands
@@ -0,0 +1,14 @@
+From jdoe@example.com Fri Apr 18 11:18:58 2008
+Message-ID: <abcd@example.com>
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+From: John Doe <jdoe@example.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Subject: [be-bug] Subject ignored
+
+help
+list --status=all
+list --status=fixed
+show --xml 361
+--
+Goofy tagline ignored.
diff --git a/interfaces/email/interactive/examples/new b/interfaces/email/interactive/examples/new
new file mode 100644
index 0000000..c64db93
--- /dev/null
+++ b/interfaces/email/interactive/examples/new
@@ -0,0 +1,19 @@
+From jdoe@example.com Fri Apr 18 12:00:00 2008
+Message-ID: <abcd@example.com>
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+From: John Doe <jdoe@example.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Subject: [be-bug:submit] Need tests for the email interface.
+
+Version: XYZ
+Reporter: Jane Doe
+Assign: Dick Tracy
+Depend: 00f
+Severity: critical
+Status: assigned
+Tag: topsecret
+Target: Law&Order
+
+--
+Goofy tagline not included, and no comment added.
diff --git a/interfaces/email/interactive/examples/new_with_comment b/interfaces/email/interactive/examples/new_with_comment
new file mode 100644
index 0000000..1077f0f
--- /dev/null
+++ b/interfaces/email/interactive/examples/new_with_comment
@@ -0,0 +1,13 @@
+From jdoe@example.com Fri Apr 18 11:18:58 2008
+Message-ID: <abcd@example.com>
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+From: John Doe <jdoe@example.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Subject: [be-bug:submit] Need tests for the email interface.
+
+Version: XYZ
+
+I think so anyway.
+--
+Goofy tagline not included.
diff --git a/interfaces/email/interactive/examples/show b/interfaces/email/interactive/examples/show
new file mode 100644
index 0000000..c5f8a4d
--- /dev/null
+++ b/interfaces/email/interactive/examples/show
@@ -0,0 +1,11 @@
+From jdoe@example.com Fri Apr 18 11:18:58 2008
+Message-ID: <abcd@example.com>
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+From: John Doe <jdoe@example.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Subject: [be-bug] Subject ignored
+
+show --xml 361
+--
+Can we show a bug?
diff --git a/interfaces/email/interactive/examples/unicode b/interfaces/email/interactive/examples/unicode
new file mode 100644
index 0000000..f0e8001
--- /dev/null
+++ b/interfaces/email/interactive/examples/unicode
@@ -0,0 +1,11 @@
+From jdoe@example.com Fri Apr 18 11:18:58 2008
+Message-ID: <abcd@example.com>
+Date: Fri, 18 Apr 2008 12:00:00 +0000
+From: John Doe <jdoe@example.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Subject: [be-bug] Subject ignored
+
+show --xml f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a
+--
+Can we handle unicode output?
diff --git a/interfaces/email/interactive/libbe b/interfaces/email/interactive/libbe
new file mode 120000
index 0000000..7d18612
--- /dev/null
+++ b/interfaces/email/interactive/libbe
@@ -0,0 +1 @@
+../../../libbe \ No newline at end of file
diff --git a/interfaces/email/interactive/send_pgp_mime.py b/interfaces/email/interactive/send_pgp_mime.py
new file mode 100644
index 0000000..517b1f0
--- /dev/null
+++ b/interfaces/email/interactive/send_pgp_mime.py
@@ -0,0 +1,611 @@
+#!/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.
+"""
+Python module and command line tool for sending pgp/mime email.
+
+Mostly uses subprocess to call gpg and a sendmail-compatible mailer.
+If you lack gpg, either don't use the encryption functions or adjust
+the pgp_* commands. You may need to adjust the sendmail command to
+point to whichever sendmail-compatible mailer you have on your system.
+"""
+
+from cStringIO import StringIO
+import os
+import re
+#import GnuPGInterface # Maybe should use this instead of subprocess
+import smtplib
+import subprocess
+import sys
+import tempfile
+import types
+
+try:
+ from email import Message
+ from email.mime.text import MIMEText
+ from email.mime.multipart import MIMEMultipart
+ from email.mime.application import MIMEApplication
+ from email.encoders import encode_7or8bit
+ from email.generator import Generator
+ from email.parser import Parser
+ from email.utils import getaddress
+except ImportError:
+ # adjust to old python 2.4
+ from email import Message
+ from email.MIMEText import MIMEText
+ from email.MIMEMultipart import MIMEMultipart
+ from email.MIMENonMultipart import MIMENonMultipart
+ from email.Encoders import encode_7or8bit
+ from email.Generator import Generator
+ from email.Parser import Parser
+ from email.Utils import getaddresses
+
+ getaddress = getaddresses
+ class MIMEApplication (MIMENonMultipart):
+ def __init__(self, _data, _subtype, _encoder, **params):
+ MIMENonMultipart.__init__(self, 'application', _subtype, **params)
+ self.set_payload(_data)
+ _encoder(self)
+
+usage="""usage: %prog [options]
+
+Scriptable PGP MIME email using gpg.
+
+You can use gpg-agent for passphrase caching if your key requires a
+passphrase (it better!). Example usage would be to install gpg-agent,
+and then run
+ export GPG_TTY=`tty`
+ eval $(gpg-agent --daemon)
+in your shell before invoking this script. See gpg-agent(1) for more
+details. Alternatively, you can send your passphrase in on stdin
+ echo 'passphrase' | %prog [options]
+or use the --passphrase-file option
+ %prog [options] --passphrase-file FILE [more options]
+Both of these alternatives are much less secure than gpg-agent. You
+have been warned.
+"""
+
+verboseInvoke = False
+PGP_SIGN_AS = None
+PASSPHRASE = None
+
+# The following commands are adapted from my .mutt/pgp configuration
+#
+# Printf-like sequences:
+# %a The value of PGP_SIGN_AS.
+# %f Expands to the name of a file with text to be signed/encrypted.
+# %p Expands to the passphrase argument.
+# %R A string with some number (0 on up) of pgp_reciepient_arg
+# strings.
+# %r One key ID (e.g. recipient email address) to build a
+# pgp_reciepient_arg string.
+#
+# The above sequences can be used to optionally print a string if
+# their length is nonzero. For example, you may only want to pass the
+# -u/--local-user argument to gpg if PGP_SIGN_AS is defined. To
+# optionally print a string based upon one of the above sequences, the
+# following construct is used
+# %?<sequence_char>?<optional_string>?
+# where sequence_char is a character from the table above, and
+# optional_string is the string you would like printed if status_char
+# is nonzero. optional_string may contain other sequence as well as
+# normal text, but it may not contain any question marks.
+#
+# see http://codesorcery.net/old/mutt/mutt-gnupg-howto
+# http://www.mutt.org/doc/manual/manual-6.html#pgp_autosign
+# http://tldp.org/HOWTO/Mutt-GnuPG-PGP-HOWTO-8.html
+# for more details
+
+pgp_recipient_arg='-r "%r"'
+pgp_stdin_passphrase_arg='--passphrase-fd 0'
+pgp_sign_command='/usr/bin/gpg --no-verbose --quiet --batch %p --output - --detach-sign --armor --textmode %?a?-u "%a"? %f'
+pgp_encrypt_only_command='/usr/bin/gpg --no-verbose --quiet --batch --output - --encrypt --armor --textmode --always-trust --encrypt-to "%a" %R -- %f'
+pgp_encrypt_sign_command='/usr/bin/gpg --no-verbose --quiet --batch %p --output - --encrypt --sign %?a?-u "%a"? --armor --textmode --always-trust --encrypt-to "%a" %R -- %f'
+sendmail='/usr/sbin/sendmail -t'
+
+def mail(msg, sendmail=None):
+ """
+ Send an email Message instance on its merry way.
+
+ We can shell out to the user specified sendmail in case
+ the local host doesn't have an SMTP server set up
+ for easy smtplib usage.
+ """
+ if sendmail != None:
+ execute(sendmail, stdin=flatten(msg))
+ return None
+ s = smtplib.SMTP()
+ s.connect()
+ s.sendmail(from_addr=source_email(msg),
+ to_addrs=target_emails(msg),
+ msg=flatten(msg))
+ s.close()
+
+def header_from_text(text, encoding="us-ascii"):
+ """
+ Simple wrapper for instantiating an email.Message from text.
+ >>> header = header_from_text('\\n'.join(['From: me@big.edu','To: you@big.edu','Subject: testing']))
+ >>> print flatten(header)
+ From: me@big.edu
+ To: you@big.edu
+ Subject: testing
+ <BLANKLINE>
+ <BLANKLINE>
+ """
+ text = text.strip()
+ if type(text) == types.UnicodeType:
+ text = text.encode(encoding)
+ # assume StringType arguments are already encoded
+ p = Parser()
+ return p.parsestr(text, headersonly=True)
+
+def guess_encoding(text):
+ if type(text) == types.StringType:
+ encoding = "us-ascii"
+ elif type(text) == types.UnicodeType:
+ for encoding in ["us-ascii", "iso-8859-1", "utf-8"]:
+ try:
+ text.encode(encoding)
+ except UnicodeError:
+ pass
+ else:
+ break
+ assert encoding != None
+ return encoding
+
+def encodedMIMEText(body, encoding=None):
+ if encoding == None:
+ encoding = guess_encoding(body)
+ if encoding == "us-ascii":
+ return MIMEText(body)
+ else:
+ # Create the message ('plain' stands for Content-Type: text/plain)
+ return MIMEText(body.encode(encoding), 'plain', encoding)
+
+def append_text(text_part, new_text):
+ original_payload = text_part.get_payload(decode=True)
+ new_payload = u"%s%s" % (original_payload, new_text)
+ new_encoding = guess_encoding(new_payload)
+ text_part.set_payload(new_payload.encode(new_encoding), new_encoding)
+
+def attach_root(header, root_part):
+ """
+ Attach the email.Message root_part to the email.Message header
+ without generating a multi-part message.
+ """
+ for k,v in header.items():
+ root_part[k] = v
+ return root_part
+
+def execute(args, stdin=None, expect=(0,)):
+ """
+ Execute a command (allows us to drive gpg).
+ """
+ if verboseInvoke == True:
+ print >> sys.stderr, '$ '+args
+ try:
+ p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, close_fds=True)
+ except OSError, e:
+ strerror = '%s\nwhile executing %s' % (e.args[1], args)
+ raise Exception, strerror
+ output, error = p.communicate(input=stdin)
+ status = p.wait()
+ if verboseInvoke == True:
+ print >> sys.stderr, '(status: %d)\n%s%s' % (status, output, error)
+ if status not in expect:
+ strerror = '%s\nwhile executing %s\n%s\n%d' % (args[1], args, error, status)
+ raise Exception, strerror
+ return status, output, error
+
+def replace(template, format_char, replacement_text):
+ """
+ >>> replace('--textmode %?a?-u %a? %f', 'f', 'file.in')
+ '--textmode %?a?-u %a? file.in'
+ >>> replace('--textmode %?a?-u %a? %f', 'a', '0xHEXKEY')
+ '--textmode -u 0xHEXKEY %f'
+ >>> replace('--textmode %?a?-u %a? %f', 'a', '')
+ '--textmode %f'
+ """
+ if replacement_text == None:
+ replacement_text = ""
+ regexp = re.compile('%[?]'+format_char+'[?]([^?]*)[?]')
+ if len(replacement_text) > 0:
+ str = regexp.sub('\g<1>', template)
+ else:
+ str = regexp.sub('', template)
+ regexp = re.compile('%'+format_char)
+ str = regexp.sub(replacement_text, str)
+ return str
+
+def flatten(msg, to_unicode=False):
+ """
+ Produce flat text output from an email Message instance.
+ """
+ assert msg != None
+ fp = StringIO()
+ g = Generator(fp, mangle_from_=False)
+ g.flatten(msg)
+ text = fp.getvalue()
+ if to_unicode == True:
+ encoding = msg.get_content_charset() or "utf-8"
+ text = unicode(text, encoding=encoding)
+ return text
+
+def source_email(msg, return_realname=False):
+ """
+ Search the header of an email Message instance to find the
+ sender's email address.
+ """
+ froms = msg.get_all('from', [])
+ from_tuples = getaddresses(froms) # [(realname, email_address), ...]
+ assert len(from_tuples) == 1
+ if return_realname == True:
+ return from_tuples[0] # (realname, email_address)
+ return from_tuples[0][1] # email_address
+
+def target_emails(msg):
+ """
+ Search the header of an email Message instance to find a
+ list of recipient's email addresses.
+ """
+ tos = msg.get_all('to', [])
+ ccs = msg.get_all('cc', [])
+ bccs = msg.get_all('bcc', [])
+ resent_tos = msg.get_all('resent-to', [])
+ resent_ccs = msg.get_all('resent-cc', [])
+ resent_bccs = msg.get_all('resent-bcc', [])
+ all_recipients = getaddresses(tos + ccs + bccs + resent_tos
+ + resent_ccs + resent_bccs)
+ return [addr[1] for addr in all_recipients]
+
+class PGPMimeMessageFactory (object):
+ """
+ See http://www.ietf.org/rfc/rfc3156.txt for specification details.
+ >>> from_addr = "me@big.edu"
+ >>> to_addr = "you@you.edu"
+ >>> header = header_from_text('\\n'.join(['From: %s'%from_addr,'To: %s'%to_addr,'Subject: testing']))
+ >>> source_email(header) == from_addr
+ True
+ >>> target_emails(header) == [to_addr]
+ True
+ >>> m = PGPMimeMessageFactory('check 1 2\\ncheck 1 2\\n')
+ >>> print flatten(m.clearBodyPart())
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ <BLANKLINE>
+ check 1 2
+ check 1 2
+ <BLANKLINE>
+ >>> print flatten(m.plain())
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ <BLANKLINE>
+ check 1 2
+ check 1 2
+ <BLANKLINE>
+ >>> signed = m.sign(header)
+ >>> signed.set_boundary('boundsep')
+ >>> print flatten(signed).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+ Content-Type: multipart/signed; protocol="application/pgp-signature";
+ micalg="pgp-sha1"; boundary="boundsep"
+ MIME-Version: 1.0
+ Content-Disposition: inline
+ <BLANKLINE>
+ --boundsep
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ <BLANKLINE>
+ check 1 2
+ check 1 2
+ <BLANKLINE>
+ --boundsep
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Description: signature
+ Content-Type: application/pgp-signature; name="signature.asc";
+ charset="us-ascii"
+ <BLANKLINE>
+ -----BEGIN PGP SIGNATURE-----
+ ...
+ -----END PGP SIGNATURE-----
+ <BLANKLINE>
+ --boundsep--
+ >>> encrypted = m.encrypt(header)
+ >>> encrypted.set_boundary('boundsep')
+ >>> print flatten(encrypted).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+ Content-Type: multipart/encrypted;
+ protocol="application/pgp-encrypted";
+ micalg="pgp-sha1"; boundary="boundsep"
+ MIME-Version: 1.0
+ Content-Disposition: inline
+ <BLANKLINE>
+ --boundsep
+ Content-Type: application/pgp-encrypted
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ <BLANKLINE>
+ Version: 1
+ <BLANKLINE>
+ --boundsep
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Type: application/octet-stream; charset="us-ascii"
+ <BLANKLINE>
+ -----BEGIN PGP MESSAGE-----
+ ...
+ -----END PGP MESSAGE-----
+ <BLANKLINE>
+ --boundsep--
+ >>> signedAndEncrypted = m.signAndEncrypt(header)
+ >>> signedAndEncrypted.set_boundary('boundsep')
+ >>> print flatten(signedAndEncrypted).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+ Content-Type: multipart/encrypted;
+ protocol="application/pgp-encrypted";
+ micalg="pgp-sha1"; boundary="boundsep"
+ MIME-Version: 1.0
+ Content-Disposition: inline
+ <BLANKLINE>
+ --boundsep
+ Content-Type: application/pgp-encrypted
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ <BLANKLINE>
+ Version: 1
+ <BLANKLINE>
+ --boundsep
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Type: application/octet-stream; charset="us-ascii"
+ <BLANKLINE>
+ -----BEGIN PGP MESSAGE-----
+ ...
+ -----END PGP MESSAGE-----
+ <BLANKLINE>
+ --boundsep--
+ """
+ def __init__(self, body):
+ self.body = body
+ def clearBodyPart(self):
+ body = encodedMIMEText(self.body)
+ body.add_header('Content-Disposition', 'inline')
+ return body
+ def passphrase_arg(self, passphrase=None):
+ if passphrase == None and PASSPHRASE != None:
+ passphrase = PASSPHRASE
+ if passphrase == None:
+ return (None,'')
+ return (passphrase, pgp_stdin_passphrase_arg)
+ def plain(self):
+ """
+ text/plain
+ """
+ return encodedMIMEText(self.body)
+ def sign(self, header, passphrase=None):
+ """
+ multipart/signed
+ +-> text/plain (body)
+ +-> application/pgp-signature (signature)
+ """
+ passphrase,pass_arg = self.passphrase_arg(passphrase)
+ body = self.clearBodyPart()
+ bfile = tempfile.NamedTemporaryFile()
+ bfile.write(flatten(body))
+ bfile.flush()
+
+ args = replace(pgp_sign_command, 'f', bfile.name)
+ if PGP_SIGN_AS == None:
+ pgp_sign_as = '<%s>' % source_email(header)
+ else:
+ pgp_sign_as = PGP_SIGN_AS
+ args = replace(args, 'a', pgp_sign_as)
+ args = replace(args, 'p', pass_arg)
+ status,output,error = execute(args, stdin=passphrase)
+ signature = output
+
+ sig = MIMEApplication(_data=signature,
+ _subtype='pgp-signature; name="signature.asc"',
+ _encoder=encode_7or8bit)
+ sig['Content-Description'] = 'signature'
+ sig.set_charset('us-ascii')
+
+ msg = MIMEMultipart('signed', micalg='pgp-sha1',
+ protocol='application/pgp-signature')
+ msg.attach(body)
+ msg.attach(sig)
+
+ msg['Content-Disposition'] = 'inline'
+ return msg
+ def encrypt(self, header, passphrase=None):
+ """
+ multipart/encrypted
+ +-> application/pgp-encrypted (control information)
+ +-> application/octet-stream (body)
+ """
+ body = self.clearBodyPart()
+ bfile = tempfile.NamedTemporaryFile()
+ bfile.write(flatten(body))
+ bfile.flush()
+
+ recipients = [replace(pgp_recipient_arg, 'r', recipient)
+ for recipient in target_emails(header)]
+ recipient_string = ' '.join(recipients)
+ args = replace(pgp_encrypt_only_command, 'R', recipient_string)
+ args = replace(args, 'f', bfile.name)
+ if PGP_SIGN_AS == None:
+ pgp_sign_as = '<%s>' % source_email(header)
+ else:
+ pgp_sign_as = PGP_SIGN_AS
+ args = replace(args, 'a', pgp_sign_as)
+ status,output,error = execute(args)
+ encrypted = output
+
+ enc = MIMEApplication(_data=encrypted, _subtype='octet-stream',
+ _encoder=encode_7or8bit)
+ enc.set_charset('us-ascii')
+
+ control = MIMEApplication(_data='Version: 1\n', _subtype='pgp-encrypted',
+ _encoder=encode_7or8bit)
+
+ msg = MIMEMultipart('encrypted', micalg='pgp-sha1',
+ protocol='application/pgp-encrypted')
+ msg.attach(control)
+ msg.attach(enc)
+
+ msg['Content-Disposition'] = 'inline'
+ return msg
+ def signAndEncrypt(self, header, passphrase=None):
+ """
+ multipart/encrypted
+ +-> application/pgp-encrypted (control information)
+ +-> application/octet-stream (body)
+ """
+ passphrase,pass_arg = self.passphrase_arg(passphrase)
+ body = self.sign(header, passphrase)
+ body.__delitem__('Bcc')
+ bfile = tempfile.NamedTemporaryFile()
+ bfile.write(flatten(body))
+ bfile.flush()
+
+ recipients = [replace(pgp_recipient_arg, 'r', recipient)
+ for recipient in target_emails(header)]
+ recipient_string = ' '.join(recipients)
+ args = replace(pgp_encrypt_only_command, 'R', recipient_string)
+ args = replace(args, 'f', bfile.name)
+ if PGP_SIGN_AS == None:
+ pgp_sign_as = '<%s>' % source_email(header)
+ else:
+ pgp_sign_as = PGP_SIGN_AS
+ args = replace(args, 'a', pgp_sign_as)
+ args = replace(args, 'p', pass_arg)
+ status,output,error = execute(args, stdin=passphrase)
+ encrypted = output
+
+ enc = MIMEApplication(_data=encrypted, _subtype='octet-stream',
+ _encoder=encode_7or8bit)
+ enc.set_charset('us-ascii')
+
+ control = MIMEApplication(_data='Version: 1\n',
+ _subtype='pgp-encrypted',
+ _encoder=encode_7or8bit)
+
+ msg = MIMEMultipart('encrypted', micalg='pgp-sha1',
+ protocol='application/pgp-encrypted')
+ msg.attach(control)
+ msg.attach(enc)
+
+ msg['Content-Disposition'] = 'inline'
+ return msg
+
+def test():
+ import doctest
+ doctest.testmod()
+
+
+if __name__ == '__main__':
+ from optparse import OptionParser
+
+ parser = OptionParser(usage=usage)
+ parser.add_option('-t', '--test', dest='test', action='store_true',
+ help='Run doctests and exit')
+
+ parser.add_option('-H', '--header-file', dest='header_filename',
+ help='file containing email header', metavar='FILE')
+ parser.add_option('-B', '--body-file', dest='body_filename',
+ help='file containing email body', metavar='FILE')
+
+ parser.add_option('-P', '--passphrase-file', dest='passphrase_file',
+ help='file containing gpg passphrase', metavar='FILE')
+ parser.add_option('-p', '--passphrase-fd', dest='passphrase_fd',
+ help='file descriptor from which to read gpg passphrase (0 for stdin)',
+ type="int", metavar='DESCRIPTOR')
+
+ parser.add_option('--mode', dest='mode', default='sign',
+ help="One of 'sign', 'encrypt', 'sign-encrypt', or 'plain'. Defaults to %default.",
+ metavar='MODE')
+
+ parser.add_option('-a', '--sign-as', dest='sign_as',
+ help="The gpg key to sign with (gpg's -u/--local-user)",
+ metavar='KEY')
+
+ parser.add_option('--output', dest='output', action='store_true',
+ help="Don't mail the generated message, print it to stdout instead.")
+
+ (options, args) = parser.parse_args()
+
+ stdin_used = False
+
+ if options.passphrase_file != None:
+ PASSPHRASE = file(options.passphrase_file, 'r').read()
+ elif options.passphrase_fd != None:
+ if options.passphrase_fd == 0:
+ stdin_used = True
+ PASSPHRASE = sys.stdin.read()
+ else:
+ PASSPHRASE = os.read(options.passphrase_fd)
+
+ if options.sign_as:
+ PGP_SIGN_AS = options.sign_as
+
+ if options.test == True:
+ test()
+ sys.exit(0)
+
+ header = None
+ if options.header_filename != None:
+ if options.header_filename == '-':
+ assert stdin_used == False
+ stdin_used = True
+ header = sys.stdin.read()
+ else:
+ header = file(options.header_filename, 'r').read()
+ if header == None:
+ raise Exception, "missing header"
+ headermsg = header_from_text(header)
+ body = None
+ if options.body_filename != None:
+ if options.body_filename == '-':
+ assert stdin_used == False
+ stdin_used = True
+ body = sys.stdin.read()
+ else:
+ body = file(options.body_filename, 'r').read()
+ if body == None:
+ raise Exception, "missing body"
+
+ m = PGPMimeMessageFactory(body)
+ if options.mode == "sign":
+ bodymsg = m.sign(header)
+ elif options.mode == "encrypt":
+ bodymsg = m.encrypt(header)
+ elif options.mode == "sign-encrypt":
+ bodymsg = m.signAndEncrypt(header)
+ elif options.mode == "plain":
+ bodymsg = m.plain()
+ else:
+ print "Unrecognized mode '%s'" % options.mode
+
+ message = attach_root(headermsg, bodymsg)
+ if options.output == True:
+ message = flatten(message)
+ print message
+ else:
+ mail(message, sendmail)
diff --git a/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/body b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/body
index 49a1e50..49a1e50 100644
--- a/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/body
+++ b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/body
diff --git a/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/values b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/values
index bf58725..bf58725 100644
--- a/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/values
+++ b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/values
diff --git a/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/values b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/values
index 256574b..256574b 100644
--- a/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/values
+++ b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/values
diff --git a/.be/bugs/0a234f51-2fdf-4001-a04f-b7e02c2fa47b/values b/interfaces/web/.be/bugs/0a234f51-2fdf-4001-a04f-b7e02c2fa47b/values
index b911874..b911874 100644
--- a/.be/bugs/0a234f51-2fdf-4001-a04f-b7e02c2fa47b/values
+++ b/interfaces/web/.be/bugs/0a234f51-2fdf-4001-a04f-b7e02c2fa47b/values
diff --git a/.be/bugs/0be47243-c172-4de9-b71b-d5dea60f91d5/values b/interfaces/web/.be/bugs/0be47243-c172-4de9-b71b-d5dea60f91d5/values
index 0626932..0626932 100644
--- a/.be/bugs/0be47243-c172-4de9-b71b-d5dea60f91d5/values
+++ b/interfaces/web/.be/bugs/0be47243-c172-4de9-b71b-d5dea60f91d5/values
diff --git a/.be/bugs/171819aa-c092-4ddf-ace3-797635fa2572/values b/interfaces/web/.be/bugs/171819aa-c092-4ddf-ace3-797635fa2572/values
index 361cfa7..361cfa7 100644
--- a/.be/bugs/171819aa-c092-4ddf-ace3-797635fa2572/values
+++ b/interfaces/web/.be/bugs/171819aa-c092-4ddf-ace3-797635fa2572/values
diff --git a/.be/bugs/24555ea1-76b5-40a8-918f-115a28f5f36a/values b/interfaces/web/.be/bugs/24555ea1-76b5-40a8-918f-115a28f5f36a/values
index 72629bc..72629bc 100644
--- a/.be/bugs/24555ea1-76b5-40a8-918f-115a28f5f36a/values
+++ b/interfaces/web/.be/bugs/24555ea1-76b5-40a8-918f-115a28f5f36a/values
diff --git a/.be/bugs/312fb152-0155-45c1-9d4d-f49dd5816fbb/values b/interfaces/web/.be/bugs/312fb152-0155-45c1-9d4d-f49dd5816fbb/values
index 2795a3a..2795a3a 100644
--- a/.be/bugs/312fb152-0155-45c1-9d4d-f49dd5816fbb/values
+++ b/interfaces/web/.be/bugs/312fb152-0155-45c1-9d4d-f49dd5816fbb/values
diff --git a/.be/bugs/35b962a0-a64a-4b5c-82c5-ea740e8a6322/values b/interfaces/web/.be/bugs/35b962a0-a64a-4b5c-82c5-ea740e8a6322/values
index 134df9b..134df9b 100644
--- a/.be/bugs/35b962a0-a64a-4b5c-82c5-ea740e8a6322/values
+++ b/interfaces/web/.be/bugs/35b962a0-a64a-4b5c-82c5-ea740e8a6322/values
diff --git a/.be/bugs/42716dc2-6201-4537-b5fd-e1280812a53d/values b/interfaces/web/.be/bugs/42716dc2-6201-4537-b5fd-e1280812a53d/values
index c193f8f..c193f8f 100644
--- a/.be/bugs/42716dc2-6201-4537-b5fd-e1280812a53d/values
+++ b/interfaces/web/.be/bugs/42716dc2-6201-4537-b5fd-e1280812a53d/values
diff --git a/.be/bugs/4286c0f8-5703-4bc1-b256-414dc408f067/values b/interfaces/web/.be/bugs/4286c0f8-5703-4bc1-b256-414dc408f067/values
index bc901f9..bc901f9 100644
--- a/.be/bugs/4286c0f8-5703-4bc1-b256-414dc408f067/values
+++ b/interfaces/web/.be/bugs/4286c0f8-5703-4bc1-b256-414dc408f067/values
diff --git a/.be/bugs/528b2e84-a944-4628-a18f-cc1def1c7e16/values b/interfaces/web/.be/bugs/528b2e84-a944-4628-a18f-cc1def1c7e16/values
index 19aafd2..19aafd2 100644
--- a/.be/bugs/528b2e84-a944-4628-a18f-cc1def1c7e16/values
+++ b/interfaces/web/.be/bugs/528b2e84-a944-4628-a18f-cc1def1c7e16/values
diff --git a/.be/bugs/52a15454-196c-4990-b55d-be2e37d575c3/values b/interfaces/web/.be/bugs/52a15454-196c-4990-b55d-be2e37d575c3/values
index a3cb0aa..a3cb0aa 100644
--- a/.be/bugs/52a15454-196c-4990-b55d-be2e37d575c3/values
+++ b/interfaces/web/.be/bugs/52a15454-196c-4990-b55d-be2e37d575c3/values
diff --git a/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/body b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/body
index 5becd48..5becd48 100644
--- a/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/body
+++ b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/body
diff --git a/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/values b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/values
index 41f53c6..41f53c6 100644
--- a/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/values
+++ b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/values
diff --git a/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/values b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/values
index 851021e..851021e 100644
--- a/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/values
+++ b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/values
diff --git a/.be/bugs/55e76f74-37fb-4254-8498-54b703ba54f6/values b/interfaces/web/.be/bugs/55e76f74-37fb-4254-8498-54b703ba54f6/values
index cded1dc..cded1dc 100644
--- a/.be/bugs/55e76f74-37fb-4254-8498-54b703ba54f6/values
+++ b/interfaces/web/.be/bugs/55e76f74-37fb-4254-8498-54b703ba54f6/values
diff --git a/.be/bugs/615ad650-9fb9-4026-9779-58d42b4e528e/values b/interfaces/web/.be/bugs/615ad650-9fb9-4026-9779-58d42b4e528e/values
index 56ae9a1..56ae9a1 100644
--- a/.be/bugs/615ad650-9fb9-4026-9779-58d42b4e528e/values
+++ b/interfaces/web/.be/bugs/615ad650-9fb9-4026-9779-58d42b4e528e/values
diff --git a/.be/bugs/63619cf7-89eb-4e64-91e9-b8a73d2a6c72/values b/interfaces/web/.be/bugs/63619cf7-89eb-4e64-91e9-b8a73d2a6c72/values
index cb7a38e..cb7a38e 100644
--- a/.be/bugs/63619cf7-89eb-4e64-91e9-b8a73d2a6c72/values
+++ b/interfaces/web/.be/bugs/63619cf7-89eb-4e64-91e9-b8a73d2a6c72/values
diff --git a/.be/bugs/700cd3f1-70b6-4887-89a2-c1d039732add/values b/interfaces/web/.be/bugs/700cd3f1-70b6-4887-89a2-c1d039732add/values
index 71ab0a3..71ab0a3 100644
--- a/.be/bugs/700cd3f1-70b6-4887-89a2-c1d039732add/values
+++ b/interfaces/web/.be/bugs/700cd3f1-70b6-4887-89a2-c1d039732add/values
diff --git a/.be/bugs/81f69fbd-1ca5-4f89-a6e1-79ea1e6bf4d9/values b/interfaces/web/.be/bugs/81f69fbd-1ca5-4f89-a6e1-79ea1e6bf4d9/values
index dcaa6b3..dcaa6b3 100644
--- a/.be/bugs/81f69fbd-1ca5-4f89-a6e1-79ea1e6bf4d9/values
+++ b/interfaces/web/.be/bugs/81f69fbd-1ca5-4f89-a6e1-79ea1e6bf4d9/values
diff --git a/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/body b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/body
index f75f8b7..f75f8b7 100644
--- a/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/body
+++ b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/body
diff --git a/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/values b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/values
index 7927b05..7927b05 100644
--- a/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/values
+++ b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/values
diff --git a/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/values b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/values
index f9e0a64..f9e0a64 100644
--- a/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/values
+++ b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/values
diff --git a/.be/bugs/870d5dbe-6449-4ec4-ae6f-e84bebadbce0/values b/interfaces/web/.be/bugs/870d5dbe-6449-4ec4-ae6f-e84bebadbce0/values
index e91a4cf..e91a4cf 100644
--- a/.be/bugs/870d5dbe-6449-4ec4-ae6f-e84bebadbce0/values
+++ b/interfaces/web/.be/bugs/870d5dbe-6449-4ec4-ae6f-e84bebadbce0/values
diff --git a/.be/bugs/8cb9045c-7266-4c40-9a76-65f3c5d5bb60/values b/interfaces/web/.be/bugs/8cb9045c-7266-4c40-9a76-65f3c5d5bb60/values
index b8403eb..b8403eb 100644
--- a/.be/bugs/8cb9045c-7266-4c40-9a76-65f3c5d5bb60/values
+++ b/interfaces/web/.be/bugs/8cb9045c-7266-4c40-9a76-65f3c5d5bb60/values
diff --git a/.be/bugs/984472f6-98f5-48fc-b521-70a1e5f60614/values b/interfaces/web/.be/bugs/984472f6-98f5-48fc-b521-70a1e5f60614/values
index 21d3cef..21d3cef 100644
--- a/.be/bugs/984472f6-98f5-48fc-b521-70a1e5f60614/values
+++ b/interfaces/web/.be/bugs/984472f6-98f5-48fc-b521-70a1e5f60614/values
diff --git a/.be/bugs/9bc14860-b2bb-4442-85ea-0b8e7083457b/values b/interfaces/web/.be/bugs/9bc14860-b2bb-4442-85ea-0b8e7083457b/values
index b01cd70..b01cd70 100644
--- a/.be/bugs/9bc14860-b2bb-4442-85ea-0b8e7083457b/values
+++ b/interfaces/web/.be/bugs/9bc14860-b2bb-4442-85ea-0b8e7083457b/values
diff --git a/.be/bugs/ac72991a-72e5-4b14-b53c-0fa38d0f31bb/values b/interfaces/web/.be/bugs/ac72991a-72e5-4b14-b53c-0fa38d0f31bb/values
index b4de064..b4de064 100644
--- a/.be/bugs/ac72991a-72e5-4b14-b53c-0fa38d0f31bb/values
+++ b/interfaces/web/.be/bugs/ac72991a-72e5-4b14-b53c-0fa38d0f31bb/values
diff --git a/.be/bugs/bef126a0-27be-402f-84fa-85f6342c97c0/values b/interfaces/web/.be/bugs/bef126a0-27be-402f-84fa-85f6342c97c0/values
index 94d96d7..94d96d7 100644
--- a/.be/bugs/bef126a0-27be-402f-84fa-85f6342c97c0/values
+++ b/interfaces/web/.be/bugs/bef126a0-27be-402f-84fa-85f6342c97c0/values
diff --git a/.be/bugs/c7251ff9-24e4-402d-8d4e-605a78b9a91d/values b/interfaces/web/.be/bugs/c7251ff9-24e4-402d-8d4e-605a78b9a91d/values
index 4c1c8cb..4c1c8cb 100644
--- a/.be/bugs/c7251ff9-24e4-402d-8d4e-605a78b9a91d/values
+++ b/interfaces/web/.be/bugs/c7251ff9-24e4-402d-8d4e-605a78b9a91d/values
diff --git a/.be/bugs/cfb52b6c-d1a6-4018-a255-27cc1c878193/values b/interfaces/web/.be/bugs/cfb52b6c-d1a6-4018-a255-27cc1c878193/values
index 49fa830..49fa830 100644
--- a/.be/bugs/cfb52b6c-d1a6-4018-a255-27cc1c878193/values
+++ b/interfaces/web/.be/bugs/cfb52b6c-d1a6-4018-a255-27cc1c878193/values
diff --git a/.be/bugs/d63d0bdd-e025-4f7c-9fcf-47a71de6d4d4/values b/interfaces/web/.be/bugs/d63d0bdd-e025-4f7c-9fcf-47a71de6d4d4/values
index c100da5..c100da5 100644
--- a/.be/bugs/d63d0bdd-e025-4f7c-9fcf-47a71de6d4d4/values
+++ b/interfaces/web/.be/bugs/d63d0bdd-e025-4f7c-9fcf-47a71de6d4d4/values
diff --git a/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/body b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/body
index 6447d18..6447d18 100644
--- a/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/body
+++ b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/body
diff --git a/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/values b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/values
index c68151d..c68151d 100644
--- a/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/values
+++ b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/values
diff --git a/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/values b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/values
index 18dd9a3..18dd9a3 100644
--- a/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/values
+++ b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/values
diff --git a/.be/bugs/decc6e78-a3db-4cd3-ad23-2bf8ed77cb0d/values b/interfaces/web/.be/bugs/decc6e78-a3db-4cd3-ad23-2bf8ed77cb0d/values
index 96f52d3..96f52d3 100644
--- a/.be/bugs/decc6e78-a3db-4cd3-ad23-2bf8ed77cb0d/values
+++ b/interfaces/web/.be/bugs/decc6e78-a3db-4cd3-ad23-2bf8ed77cb0d/values
diff --git a/.be/bugs/e22a9048-9a97-41b1-91a2-d4178c674b37/values b/interfaces/web/.be/bugs/e22a9048-9a97-41b1-91a2-d4178c674b37/values
index 63e9e8c..63e9e8c 100644
--- a/.be/bugs/e22a9048-9a97-41b1-91a2-d4178c674b37/values
+++ b/interfaces/web/.be/bugs/e22a9048-9a97-41b1-91a2-d4178c674b37/values
diff --git a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/body b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/body
index d13b1b7..d13b1b7 100644
--- a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/body
+++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/body
diff --git a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/values b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/values
index 4f055dd..4f055dd 100644
--- a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/values
+++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/values
diff --git a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/body b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/body
index 8598e67..8598e67 100644
--- a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/body
+++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/body
diff --git a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/values b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/values
index f541cad..f541cad 100644
--- a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/values
+++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/values
diff --git a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/body b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/body
index 20dbfd2..20dbfd2 100644
--- a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/body
+++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/body
diff --git a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/values b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/values
index 759a973..759a973 100644
--- a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/values
+++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/values
diff --git a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/values b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/values
index 017229d..017229d 100644
--- a/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/values
+++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/values
diff --git a/.be/bugs/fd96c69d-6f78-4c0c-af6e-e01e9b8516d3/values b/interfaces/web/.be/bugs/fd96c69d-6f78-4c0c-af6e-e01e9b8516d3/values
index 837213e..837213e 100644
--- a/.be/bugs/fd96c69d-6f78-4c0c-af6e-e01e9b8516d3/values
+++ b/interfaces/web/.be/bugs/fd96c69d-6f78-4c0c-af6e-e01e9b8516d3/values
diff --git a/.be/settings b/interfaces/web/.be/settings
index 8ef3e76..8ef3e76 100644
--- a/.be/settings
+++ b/interfaces/web/.be/settings
diff --git a/interfaces/web/.be/version b/interfaces/web/.be/version
new file mode 100644
index 0000000..990837e
--- /dev/null
+++ b/interfaces/web/.be/version
@@ -0,0 +1 @@
+Bugs Everywhere Tree 1 0
diff --git a/.hgignore b/interfaces/web/.hgignore
index a0e81b7..a0e81b7 100644
--- a/.hgignore
+++ b/interfaces/web/.hgignore
diff --git a/.hgtags b/interfaces/web/.hgtags
index eeea432..eeea432 100644
--- a/.hgtags
+++ b/interfaces/web/.hgtags
diff --git a/LICENSE b/interfaces/web/LICENSE
index 44f0935..44f0935 100644
--- a/LICENSE
+++ b/interfaces/web/LICENSE
diff --git a/interfaces/web/README b/interfaces/web/README
new file mode 100644
index 0000000..6bd04e5
--- /dev/null
+++ b/interfaces/web/README
@@ -0,0 +1,20 @@
+-*- markdown -*-
+
+Cherry Flavored Bugs Everywhere
+===============================
+
+CFBE is a quick web interface to [BugsEverywhere](http://bugseverywhere.org/). It's still very much a work-in-progress.
+
+Installing
+----------
+
+I intend to streamline the installation once I'm satisfied with the interface itself. For now, the install process goes something like this:
+
+* Install [CherryPy](http://cherrypy.org/) if you don't have it.
+* Install [Jinja2](http://jinja.pocoo.org/2/) if you don't have it.
+* Install [BugsEverywhere](http://bugseverywhere.org/) if you don't have it.
+* Download a zip/tar of CFBE (or hg clone) from the [Mercurial repository](http://bitbucket.org/sjl/cherryflavoredbugseverywhere/).
+* Unzip (if you grabbed a zip) and put the folder in your Python site-packages directory (or put it anywhere and symlink it to site-packages).
+* Symlink `site-packages/cherryflavoredbugseverywhere/cfbe.py` to `/usr/local/bin/cfbe`
+* Use `cfbe [project_root]` to start up the web interface for that project.
+* Visit http://localhost:8080/ in a browser.
diff --git a/interfaces/web/__init__.py b/interfaces/web/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/interfaces/web/__init__.py
diff --git a/cfbe.py b/interfaces/web/cfbe.py
index 63fbc7e..63fbc7e 100755
--- a/cfbe.py
+++ b/interfaces/web/cfbe.py
diff --git a/static/scripts/jquery.corners.min.js b/interfaces/web/static/scripts/jquery.corners.min.js
index 0b2f979..0b2f979 100644
--- a/static/scripts/jquery.corners.min.js
+++ b/interfaces/web/static/scripts/jquery.corners.min.js
diff --git a/static/style/aal.css b/interfaces/web/static/style/aal.css
index 9bad98f..9bad98f 100644
--- a/static/style/aal.css
+++ b/interfaces/web/static/style/aal.css
diff --git a/static/style/cfbe.css b/interfaces/web/static/style/cfbe.css
index c5f726e..c5f726e 100644
--- a/static/style/cfbe.css
+++ b/interfaces/web/static/style/cfbe.css
diff --git a/templates/base.html b/interfaces/web/templates/base.html
index 8f22d73..8f22d73 100644
--- a/templates/base.html
+++ b/interfaces/web/templates/base.html
diff --git a/templates/bug.html b/interfaces/web/templates/bug.html
index 4d15536..4d15536 100644
--- a/templates/bug.html
+++ b/interfaces/web/templates/bug.html
diff --git a/templates/list.html b/interfaces/web/templates/list.html
index 1d409f7..1d409f7 100644
--- a/templates/list.html
+++ b/interfaces/web/templates/list.html
diff --git a/web.py b/interfaces/web/web.py
index 9155c97..9155c97 100644
--- a/web.py
+++ b/interfaces/web/web.py
diff --git a/libbe/__init__.py b/libbe/__init__.py
new file mode 100644
index 0000000..d32716f
--- /dev/null
+++ b/libbe/__init__.py
@@ -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/bug.py b/libbe/bug.py
new file mode 100644
index 0000000..8bf32dd
--- /dev/null
+++ b/libbe/bug.py
@@ -0,0 +1,853 @@
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+# 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.
+
+"""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 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 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):
+ def __init__(self, goal):
+ msg = "Cannot %s without accessing the disk" % goal
+ Exception.__init__(self, msg)
+
+### Define and describe valid bug categories
+# Use a tuple of (category, description) tuples since we don't have
+# ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
+
+# in order of increasing severity. (name, description) pairs
+severity_def = (
+ ("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."),
+ ("critical","A bug that prevents some features from working at all."),
+ ("fatal","A bug that makes the package unusable."))
+
+# in order of increasing resolution
+# roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
+active_status_def = (
+ ("unconfirmed","A possible bug which lacks independent existance confirmation."),
+ ("open","A working bug that has not been assigned to a developer."),
+ ("assigned","A working bug that has been assigned to a developer."),
+ ("test","The code has been adjusted, but the fix is still being tested."))
+inactive_status_def = (
+ ("closed", "The bug is no longer relevant."),
+ ("fixed", "The bug should no longer occur."),
+ ("wontfix","It's not a bug, it's a feature."))
+
+
+### Convert the description tuples to more useful formats
+
+severity_values = ()
+severity_description = {}
+severity_index = {}
+def load_severities(severity_def):
+ global severity_values
+ global severity_description
+ global severity_index
+ if severity_def == None:
+ return
+ severity_values = tuple([val for val,description in severity_def])
+ severity_description = dict(severity_def)
+ severity_index = {}
+ for i,severity in enumerate(severity_values):
+ severity_index[severity] = i
+load_severities(severity_def)
+
+active_status_values = []
+inactive_status_values = []
+status_values = []
+status_description = {}
+status_index = {}
+def load_status(active_status_def, inactive_status_def):
+ global active_status_values
+ global inactive_status_values
+ global status_values
+ global status_description
+ global status_index
+ if active_status_def == None:
+ active_status_def = globals()["active_status_def"]
+ if inactive_status_def == None:
+ inactive_status_def = globals()["inactive_status_def"]
+ active_status_values = tuple([val for val,description in active_status_def])
+ inactive_status_values = tuple([val for val,description in inactive_status_def])
+ status_values = active_status_values + inactive_status_values
+ status_description = dict(tuple(active_status_def) + tuple(inactive_status_def))
+ status_index = {}
+ for i,status in enumerate(status_values):
+ status_index[status] = i
+load_status(active_status_def, inactive_status_def)
+
+
+class Bug (settings_object.SavedSettingsObject):
+ """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
+ >>> print b.severity
+ minor
+
+ There are two formats for time, int and string. Setting either
+ one will adjust the other appropriately. The string form is the
+ one stored in the bug's settings file on disk.
+
+ >>> print type(b.time)
+ <type 'int'>
+ >>> print type(b.time_string)
+ <type 'str'>
+ >>> b.time = 0
+ >>> print b.time_string
+ Thu, 01 Jan 1970 00:00:00 +0000
+ >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
+ >>> b.time
+ 60
+ >>> print b.settings["time"]
+ Thu, 01 Jan 1970 00:01:00 +0000
+ """
+ settings_properties = []
+ required_saved_properties = []
+ _prop_save_settings = settings_object.prop_save_settings
+ _prop_load_settings = settings_object.prop_load_settings
+ def _versioned_property(settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties,
+ **kwargs):
+ if "settings_properties" not in kwargs:
+ kwargs["settings_properties"] = settings_properties
+ if "required_saved_properties" not in kwargs:
+ kwargs["required_saved_properties"]=required_saved_properties
+ return settings_object.versioned_property(**kwargs)
+
+ @_versioned_property(name="severity",
+ doc="A measure of the bug's importance",
+ default="minor",
+ check_fn=lambda s: s in severity_values,
+ require_save=True)
+ def severity(): return {}
+
+ @_versioned_property(name="status",
+ doc="The bug's current status",
+ default="open",
+ check_fn=lambda s: s in status_values,
+ require_save=True)
+ def status(): return {}
+
+ @property
+ def active(self):
+ return self.status in active_status_values
+
+ @_versioned_property(name="creator",
+ doc="The user who entered the bug into the system")
+ def creator(): return {}
+
+ @_versioned_property(name="reporter",
+ doc="The user who reported the bug")
+ def reporter(): return {}
+
+ @_versioned_property(name="assigned",
+ doc="The developer in charge of the bug")
+ def assigned(): return {}
+
+ @_versioned_property(name="time",
+ doc="An RFC 2822 timestamp for bug creation")
+ def time_string(): return {}
+
+ def _get_time(self):
+ if self.time_string == None:
+ return None
+ return utility.str_to_time(self.time_string)
+ def _set_time(self, value):
+ self.time_string = utility.time_to_str(value)
+ time = property(fget=_get_time,
+ fset=_set_time,
+ doc="An integer version of .time_string")
+
+ def _extra_strings_check_fn(value):
+ return utility.iterable_full_of_strings(value, \
+ alternative=settings_object.EMPTY)
+ def _extra_strings_change_hook(self, old, new):
+ self.extra_strings.sort() # to make merging easier
+ self._prop_save_settings(old, new)
+ @_versioned_property(name="extra_strings",
+ doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.",
+ default=[],
+ check_fn=_extra_strings_check_fn,
+ change_hook=_extra_strings_change_hook,
+ mutable=True)
+ def extra_strings(): return {}
+
+ @_versioned_property(name="summary",
+ doc="A one-line bug description")
+ def summary(): return {}
+
+ def _get_comment_root(self, load_full=False):
+ if self.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. 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 __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
+ self.id = libbe.util.id.ID(self, 'bug')
+ if from_storage == False:
+ if uuid == None:
+ self.uuid = libbe.util.id.uuid_gen()
+ self.time = int(time.time()) # only save to second precision
+ 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
+
+ def __str__(self):
+ return self.string(shortlist=True)
+
+ def __cmp__(self, other):
+ return cmp_full(self, other)
+
+ # serializing methods
+
+ def _setting_attr_string(self, setting):
+ value = getattr(self, setting)
+ if value == None:
+ return ""
+ if type(value) not in types.StringTypes:
+ return str(value)
+ return value
+
+ def string(self, shortlist=False, show_comments=False):
+ if shortlist == False:
+ if self.time == None:
+ timestring = ""
+ else:
+ htime = utility.handy_time(self.time)
+ timestring = "%s (%s)" % (htime, self.time_string)
+ info = [("ID", self.uuid),
+ ("Short name", self.id.user()),
+ ("Severity", self.severity),
+ ("Status", self.status),
+ ("Assigned", self._setting_attr_string("assigned")),
+ ("Reporter", self._setting_attr_string("reporter")),
+ ("Creator", self._setting_attr_string("creator")),
+ ("Created", timestring)]
+ longest_key_len = max([len(k) for k,v in info])
+ infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
+ bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
+ else:
+ statuschar = self.status[0]
+ severitychar = self.severity[0]
+ chars = "%c%c" % (statuschar, severitychar)
+ bugout = "%s:%s: %s" % (self.id.user(),chars,self.summary.rstrip('\n'))
+
+ if show_comments == True:
+ 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
+
+ def xml(self, indent=0, show_comments=False):
+ if self.time == None:
+ timestring = ""
+ else:
+ timestring = utility.time_to_str(self.time)
+
+ 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 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, 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):
+ mf = mapfile.generate(self._get_saved_settings())
+ self.storage.set(self.id.storage('values'), mf)
+
+ def save(self):
+ """
+ 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).
+ """
+ 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.save_comments(self)
+
+ def load_comments(self, load_full=True):
+ if load_full == True:
+ # Force a complete load of the whole comment tree
+ self.comment_root = self._get_comment_root(load_full=True)
+ else:
+ # Setup for fresh lazy-loading. Clear _comment_root, so
+ # next _get_comment_root returns a fresh version. Turn of
+ # writing temporarily so we don't write our blank comment
+ # tree to disk.
+ w = self.storage.writeable
+ self.storage.writeable = False
+ self.comment_root = None
+ self.storage.writeable = w
+
+ def remove(self):
+ 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
+
+ def new_comment(self, body=None):
+ comm = self.comment_root.new_reply(body=body)
+ return comm
+
+ def comment_from_uuid(self, uuid, *args, **kwargs):
+ return self.comment_root.comment_from_uuid(uuid, *args, **kwargs)
+
+ # methods for id generation
+
+ 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
+# less than "less important" bugs. This way sorting a list of bugs
+# will put the most important bugs first in the list. When relative
+# importance is unclear, the sorting follows some arbitrary convention
+# (i.e. dictionary order).
+
+def cmp_severity(bug_1, bug_2):
+ """
+ Compare the severity levels of two bugs, with more severe bugs
+ comparing as less.
+
+ >>> bugA = Bug()
+ >>> bugB = Bug()
+ >>> bugA.severity = bugB.severity = "wishlist"
+ >>> cmp_severity(bugA, bugB) == 0
+ True
+ >>> bugB.severity = "minor"
+ >>> cmp_severity(bugA, bugB) > 0
+ True
+ >>> bugA.severity = "critical"
+ >>> cmp_severity(bugA, bugB) < 0
+ True
+ """
+ if not hasattr(bug_2, "severity") :
+ return 1
+ return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
+
+def cmp_status(bug_1, bug_2):
+ """
+ Compare the status levels of two bugs, with more "open" bugs
+ comparing as less.
+
+ >>> bugA = Bug()
+ >>> bugB = Bug()
+ >>> bugA.status = bugB.status = "open"
+ >>> cmp_status(bugA, bugB) == 0
+ True
+ >>> bugB.status = "closed"
+ >>> cmp_status(bugA, bugB) < 0
+ True
+ >>> bugA.status = "fixed"
+ >>> cmp_status(bugA, bugB) > 0
+ True
+ """
+ if not hasattr(bug_2, "status") :
+ return 1
+ val_2 = status_index[bug_2.status]
+ return cmp(status_index[bug_1.status], status_index[bug_2.status])
+
+def cmp_attr(bug_1, bug_2, attr, invert=False):
+ """
+ Compare a general attribute between two bugs using the
+ conventional comparison rule for that attribute type. If
+ ``invert==True``, sort *against* that convention.
+
+ >>> attr="severity"
+ >>> bugA = Bug()
+ >>> bugB = Bug()
+ >>> bugA.severity = "critical"
+ >>> bugB.severity = "wishlist"
+ >>> cmp_attr(bugA, bugB, attr) < 0
+ True
+ >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
+ True
+ >>> bugB.severity = "critical"
+ >>> cmp_attr(bugA, bugB, attr) == 0
+ True
+ """
+ if not hasattr(bug_2, attr) :
+ return 1
+ val_1 = getattr(bug_1, attr)
+ val_2 = getattr(bug_2, attr)
+ if val_1 == None: val_1 = None
+ if val_2 == None: val_2 = None
+
+ if invert == True :
+ return -cmp(val_1, val_2)
+ else :
+ return cmp(val_1, val_2)
+
+# alphabetical rankings (a < z)
+cmp_uuid = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "uuid")
+cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
+cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
+cmp_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)
+
+def cmp_comments(bug_1, bug_2):
+ """
+ Compare two bugs' comments lists. Doesn't load any new comments,
+ so you should call each bug's .load_comments() first if you want a
+ full comparison.
+ """
+ comms_1 = sorted(bug_1.comments(), key = lambda comm : comm.uuid)
+ comms_2 = sorted(bug_2.comments(), key = lambda comm : comm.uuid)
+ result = cmp(len(comms_1), len(comms_2))
+ if result != 0:
+ return result
+ for c_1,c_2 in zip(comms_1, comms_2):
+ result = cmp(c_1, c_2)
+ if result != 0:
+ return result
+ return 0
+
+DEFAULT_CMP_FULL_CMP_LIST = \
+ (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator,
+ cmp_reporter, cmp_comments, cmp_summary, cmp_uuid, cmp_extra_strings)
+
+class BugCompoundComparator (object):
+ def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
+ self.cmp_list = cmp_list
+ def __call__(self, bug_1, bug_2):
+ for comparison in self.cmp_list :
+ val = comparison(bug_1, bug_2)
+ if val != 0 :
+ return val
+ return 0
+
+cmp_full = BugCompoundComparator()
+
+
+# define some bonus cmp_* functions
+def cmp_last_modified(bug_1, bug_2):
+ """
+ Like cmp_time(), but use most recent comment instead of bug
+ creation for the timestamp.
+ """
+ def last_modified(bug):
+ time = bug.time
+ for comment in bug.comment_root.traverse():
+ if comment.time > time:
+ time = comment.time
+ return time
+ val_1 = last_modified(bug_1)
+ val_2 = last_modified(bug_2)
+ return -cmp(val_1, val_2)
+
+
+if libbe.TESTING == True:
+ suite = doctest.DocTestSuite()
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
new file mode 100644
index 0000000..65136fe
--- /dev/null
+++ b/libbe/bugdir.py
@@ -0,0 +1,563 @@
+# 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>
+#
+# 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:`BugDir` class for storing a collection of bugs.
+"""
+
+import copy
+import errno
+import os
+import os.path
+import time
+
+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):
+ """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 = []
+ required_saved_properties = []
+ _prop_save_settings = settings_object.prop_save_settings
+ _prop_load_settings = settings_object.prop_load_settings
+ def _versioned_property(settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties,
+ **kwargs):
+ if "settings_properties" not in kwargs:
+ kwargs["settings_properties"] = settings_properties
+ if "required_saved_properties" not in kwargs:
+ kwargs["required_saved_properties"]=required_saved_properties
+ return settings_object.versioned_property(**kwargs)
+
+ @_versioned_property(name="target",
+ doc="The current project development target.")
+ def target(): return {}
+
+ def _setup_severities(self, severities):
+ if severities not in [None, settings_object.EMPTY]:
+ bug.load_severities(severities)
+ def _set_severities(self, old_severities, new_severities):
+ self._setup_severities(new_severities)
+ self._prop_save_settings(old_severities, new_severities)
+ @_versioned_property(name="severities",
+ doc="The allowed bug severities and their descriptions.",
+ change_hook=_set_severities)
+ def severities(): return {}
+
+ def _setup_status(self, active_status, inactive_status):
+ bug.load_status(active_status, inactive_status)
+ def _set_active_status(self, old_active_status, new_active_status):
+ self._setup_status(new_active_status, self.inactive_status)
+ self._prop_save_settings(old_active_status, new_active_status)
+ @_versioned_property(name="active_status",
+ doc="The allowed active bug states and their descriptions.",
+ change_hook=_set_active_status)
+ def active_status(): return {}
+
+ def _set_inactive_status(self, old_inactive_status, new_inactive_status):
+ self._setup_status(self.active_status, new_inactive_status)
+ self._prop_save_settings(old_inactive_status, new_inactive_status)
+ @_versioned_property(name="inactive_status",
+ doc="The allowed inactive bug states and their descriptions.",
+ change_hook=_set_inactive_status)
+ def inactive_status(): return {}
+
+ def _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 _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
+
+ @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 __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:
+ 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 load_settings(self, settings_mapfile=None):
+ if settings_mapfile == None:
+ settings_mapfile = \
+ self.storage.get(self.id.storage('settings'), default='\n')
+ try:
+ 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)
+
+ def save_settings(self):
+ mf = mapfile.generate(self._get_saved_settings())
+ self.storage.set(self.id.storage('settings'), mf)
+
+ def load_all_bugs(self):
+ """
+ Warning: this could take a while.
+ """
+ self._clear_bugs()
+ for uuid in self.uuids():
+ self._load_bug(uuid)
+
+ def save(self):
+ """
+ Save any loaded contents to storage. Because of lazy loading
+ of bugs and 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).
+ """
+ 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()
+
+ # methods for managing bugs
+
+ 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):
+ bg = bug.Bug(bugdir=self, uuid=uuid, from_storage=True)
+ self.append(bg)
+ self._bug_map_gen()
+ return bg
+
+ 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 self.storage != None and self.storage.is_writeable():
+ bug.remove()
+
+ def bug_from_uuid(self, uuid):
+ if not self.has_bug(uuid):
+ 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]
+
+ def has_bug(self, bug_uuid):
+ if bug_uuid not in self._bug_map:
+ self._bug_map_gen()
+ if bug_uuid not in self._bug_map:
+ return False
+ return True
+
+ # methods for id generation
+
+ def sibling_uuids(self):
+ return []
+
+class RevisionedBugDir (BugDir):
+ """
+ RevisionedBugDirs are read-only copies used for generating
+ diffs between revisions.
+ """
+ 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:
+ 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/command/__init__.py b/libbe/command/__init__.py
new file mode 100644
index 0000000..0c8d4ff
--- /dev/null
+++ b/libbe/command/__init__.py
@@ -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
index 0000000..6abf05e
--- /dev/null
+++ b/libbe/command/assign.py
@@ -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
index 0000000..f8bbb1f
--- /dev/null
+++ b/libbe/command/base.py
@@ -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.getreader(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()
diff --git a/libbe/command/close.py b/libbe/command/close.py
new file mode 100644
index 0000000..0532ed2
--- /dev/null
+++ b/libbe/command/close.py
@@ -0,0 +1,60 @@
+# 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.
+"""Close a bug"""
+from libbe import cmdutil, bugdir
+__desc__ = __doc__
+
+def execute(args, manipulate_encodings=True):
+ """
+ >>> from libbe import bugdir
+ >>> import os
+ >>> bd = bugdir.SimpleBugDir()
+ >>> os.chdir(bd.root)
+ >>> print bd.bug_from_shortname("a").status
+ open
+ >>> execute(["a"], manipulate_encodings=False)
+ >>> bd._clear_bugs()
+ >>> print bd.bug_from_shortname("a").status
+ closed
+ >>> bd.cleanup()
+ """
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ cmdutil.default_complete(options, args, parser,
+ bugid_args={0: lambda bug : bug.active==True})
+ if len(args) == 0:
+ raise cmdutil.UsageError("Please specify a bug id.")
+ if len(args) > 1:
+ raise cmdutil.UsageError("Too many arguments.")
+ bd = bugdir.BugDir(from_disk=True,
+ manipulate_encodings=manipulate_encodings)
+ bug = cmdutil.bug_from_shortname(bd, args[0])
+ bug.status = "closed"
+ bd.save()
+
+def get_parser():
+ parser = cmdutil.CmdOptionParser("be close BUG-ID")
+ return parser
+
+longhelp="""
+Close the bug identified by BUG-ID.
+"""
+
+def help():
+ return get_parser().help_str() + longhelp
diff --git a/libbe/command/comment.py b/libbe/command/comment.py
new file mode 100644
index 0000000..5bf6acf
--- /dev/null
+++ b/libbe/command/comment.py
@@ -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
index 0000000..fd15630
--- /dev/null
+++ b/libbe/command/commit.py
@@ -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
index 0000000..f87657b
--- /dev/null
+++ b/libbe/command/depend.py
@@ -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
index 0000000..967ab14
--- /dev/null
+++ b/libbe/command/diff.py
@@ -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
index 0000000..4463455
--- /dev/null
+++ b/libbe/command/due.py
@@ -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/help.py b/libbe/command/help.py
new file mode 100644
index 0000000..1fc88f0
--- /dev/null
+++ b/libbe/command/help.py
@@ -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
index 0000000..c9f89f3
--- /dev/null
+++ b/libbe/command/html.py
@@ -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
index 0000000..a890669
--- /dev/null
+++ b/libbe/command/import_xml.py
@@ -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 libbe.command.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
index 0000000..7b83645
--- /dev/null
+++ b/libbe/command/init.py
@@ -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
index 0000000..3803257
--- /dev/null
+++ b/libbe/command/list.py
@@ -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
index 0000000..2dff59c
--- /dev/null
+++ b/libbe/command/merge.py
@@ -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
index 0000000..be18306
--- /dev/null
+++ b/libbe/command/new.py
@@ -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.
+"""
diff --git a/libbe/command/open.py b/libbe/command/open.py
new file mode 100644
index 0000000..0c6bf05
--- /dev/null
+++ b/libbe/command/open.py
@@ -0,0 +1,58 @@
+# 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.
+"""Re-open a bug"""
+from libbe import cmdutil, bugdir
+__desc__ = __doc__
+
+def execute(args, manipulate_encodings=True):
+ """
+ >>> import os
+ >>> bd = bugdir.SimpleBugDir()
+ >>> os.chdir(bd.root)
+ >>> print bd.bug_from_shortname("b").status
+ closed
+ >>> execute(["b"], manipulate_encodings=False)
+ >>> bd._clear_bugs()
+ >>> print bd.bug_from_shortname("b").status
+ open
+ >>> bd.cleanup()
+ """
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ cmdutil.default_complete(options, args, parser,
+ bugid_args={0: lambda bug : bug.active==False})
+ if len(args) == 0:
+ raise cmdutil.UsageError, "Please specify a bug id."
+ if len(args) > 1:
+ raise cmdutil.UsageError, "Too many arguments."
+ bd = bugdir.BugDir(from_disk=True,
+ manipulate_encodings=manipulate_encodings)
+ bug = cmdutil.bug_from_shortname(bd, args[0])
+ bug.status = "open"
+
+def get_parser():
+ parser = cmdutil.CmdOptionParser("be open BUG-ID")
+ return parser
+
+longhelp="""
+Mark a bug as 'open'.
+"""
+
+def help():
+ return get_parser().help_str() + longhelp
diff --git a/libbe/command/remove.py b/libbe/command/remove.py
new file mode 100644
index 0000000..8d8e641
--- /dev/null
+++ b/libbe/command/remove.py
@@ -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
index 0000000..7237343
--- /dev/null
+++ b/libbe/command/serve.py
@@ -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
index 0000000..720dd0f
--- /dev/null
+++ b/libbe/command/set.py
@@ -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
index 0000000..27898f7
--- /dev/null
+++ b/libbe/command/severity.py
@@ -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
index 0000000..ab3be73
--- /dev/null
+++ b/libbe/command/show.py
@@ -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
index 0000000..1659f75
--- /dev/null
+++ b/libbe/command/status.py
@@ -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
index 0000000..d1cf72e
--- /dev/null
+++ b/libbe/command/subscribe.py
@@ -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
index 0000000..f4dc3ba
--- /dev/null
+++ b/libbe/command/tag.py
@@ -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
index 0000000..f8a956b
--- /dev/null
+++ b/libbe/command/target.py
@@ -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
index 0000000..6e8e36c
--- /dev/null
+++ b/libbe/command/util.py
@@ -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)
diff --git a/libbe/comment.py b/libbe/comment.py
new file mode 100644
index 0000000..d8632a4
--- /dev/null
+++ b/libbe/comment.py
@@ -0,0 +1,769 @@
+# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+# 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.
+
+"""Define the :class:`Comment` class for representing bug comments.
+"""
+
+import base64
+import os
+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 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 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):
+ def __init__(self, shortname, shortnames):
+ msg = "Invalid shortname %s\n%s" % (shortname, shortnames)
+ KeyError.__init__(self, msg)
+ self.shortname = shortname
+ self.shortnames = shortnames
+
+class MissingReference(ValueError):
+ def __init__(self, comment):
+ msg = "Missing reference to %s" % (comment.in_reply_to)
+ ValueError.__init__(self, msg)
+ self.reference = comment.in_reply_to
+ self.comment = comment
+
+class DiskAccessRequired (Exception):
+ def __init__(self, goal):
+ msg = "Cannot %s without accessing the disk" % goal
+ Exception.__init__(self, msg)
+
+INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!"
+
+def 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.
+ """
+ uuids = []
+ for id in libbe.util.id.child_uuids(
+ bug.storage.children(
+ bug.id.storage())):
+ uuids.append(id)
+ comments = []
+ 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)
+ bug.comment_root = Comment(bug, uuid=INVALID_UUID)
+ bug.add_comments(comments, ignore_missing_references=True)
+ return bug.comment_root
+
+def save_comments(bug):
+ for comment in bug.comment_root.traverse():
+ comment.save()
+
+
+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
+ >>> c.uuid = "some-UUID"
+ >>> print c.content_type
+ text/plain
+ """
+
+ settings_properties = []
+ required_saved_properties = []
+ _prop_save_settings = settings_object.prop_save_settings
+ _prop_load_settings = settings_object.prop_load_settings
+ def _versioned_property(settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties,
+ **kwargs):
+ if "settings_properties" not in kwargs:
+ kwargs["settings_properties"] = settings_properties
+ if "required_saved_properties" not in kwargs:
+ kwargs["required_saved_properties"]=required_saved_properties
+ return settings_object.versioned_property(**kwargs)
+
+ @_versioned_property(name="Alt-id",
+ doc="Alternate ID for linking imported comments. Internally comments are linked (via In-reply-to) to the parent's UUID. However, these UUIDs are generated internally, so Alt-id is provided as a user-controlled linking target.")
+ def alt_id(): return {}
+
+ @_versioned_property(name="Author",
+ doc="The author of the comment")
+ def author(): return {}
+
+ @_versioned_property(name="In-reply-to",
+ doc="UUID for parent comment or bug")
+ def in_reply_to(): return {}
+
+ @_versioned_property(name="Content-type",
+ doc="Mime type for comment body",
+ default="text/plain",
+ require_save=True)
+ def content_type(): return {}
+
+ @_versioned_property(name="Date",
+ doc="An RFC 2822 timestamp for comment creation")
+ def date(): return {}
+
+ def _get_time(self):
+ if self.date == None:
+ return None
+ return utility.str_to_time(self.date)
+ def _set_time(self, value):
+ self.date = utility.time_to_str(value)
+ time = property(fget=_get_time,
+ fset=_set_time,
+ doc="An integer version of .date")
+
+ def _get_comment_body(self):
+ if self.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):
+ 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"
+ self.storage.set(self.id.storage("body"), new)
+
+ @Property
+ @change_hook_property(hook=_set_comment_body)
+ @cached_property(generator=_get_comment_body)
+ @local_property("body")
+ @doc_property(doc="The meat of the comment")
+ def body(): return {}
+
+ def _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, bug=None, uuid=None, from_storage=False,
+ in_reply_to=None, body=None, content_type=None):
+ """
+ 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.storage = None
+ self.uuid = uuid
+ self.id = libbe.util.id.ID(self, 'comment')
+ if from_storage == False:
+ if uuid == None:
+ self.uuid = libbe.util.id.uuid_gen()
+ self.time = int(time.time()) # only save to second precision
+ 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)
+
+ def __str__(self):
+ """
+ >>> comm = Comment(bug=None, body="Some insightful remarks")
+ >>> comm.uuid = "com-1"
+ >>> comm.date = "Thu, 20 Nov 2008 15:55:11 +0000"
+ >>> comm.author = "Jane Doe <jdoe@example.com>"
+ >>> print comm
+ --------- Comment ---------
+ Name: //com
+ From: Jane Doe <jdoe@example.com>
+ Date: Thu, 20 Nov 2008 15:55:11 +0000
+ <BLANKLINE>
+ Some insightful remarks
+ """
+ return self.string()
+
+ def traverse(self, *args, **kwargs):
+ """Avoid working with the possible dummy root comment"""
+ for comment in Tree.traverse(self, *args, **kwargs):
+ if comment.uuid == INVALID_UUID:
+ continue
+ yield comment
+
+ # serializing methods
+
+ def _setting_attr_string(self, setting):
+ value = getattr(self, setting)
+ if value == None:
+ return ""
+ if type(value) not in types.StringTypes:
+ return str(value)
+ return value
+
+ 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)
+ <comment>
+ <uuid>0123</uuid>
+ <short-name>//012</short-name>
+ <author></author>
+ <date>Thu, 01 Jan 1970 00:00:00 +0000</date>
+ <content-type>text/plain</content-type>
+ <body>Some
+ 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 self.content_type.startswith('text/'):
+ body = (self.body or '').rstrip('\n')
+ else:
+ maintype,subtype = self.content_type.split('/',1)
+ 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))
+ 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"
+ >>> commA.author = u'Fran\xe7ois'
+ >>> commA.extra_strings += ['TAG: very helpful']
+ >>> xml = commA.xml()
+ >>> commB = Comment()
+ >>> 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')
+ 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':
+ 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 = text.decode('unicode_escape').strip()
+ if child.tag == 'uuid':
+ uuid = text
+ continue # don't set the comment's uuid tag.
+ elif child.tag == 'body':
+ body = text
+ 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' \
+ % (child.tag, comment.tag)
+ 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
+ else:
+ self.body = base64.decodestring(body)
+ self.extra_strings = estrs
+
+ 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)
+ --------- Comment ---------
+ Name: //abc
+ From:
+ Date: Thu, 01 Jan 1970 00:00:00 +0000
+ <BLANKLINE>
+ Some
+ insightful
+ remarks
+ """
+ lines = []
+ lines.append("--------- Comment ---------")
+ 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/"):
+ 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",
+ 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.
+
+ >>> a = Comment(bug=None, uuid="a", body="Insightful remarks")
+ >>> a.time = utility.str_to_time("Thu, 20 Nov 2008 01:00:00 +0000")
+ >>> b = a.new_reply("Critique original comment")
+ >>> b.uuid = "b"
+ >>> b.time = utility.str_to_time("Thu, 20 Nov 2008 02:00:00 +0000")
+ >>> c = b.new_reply("Begin flamewar :p")
+ >>> c.uuid = "c"
+ >>> c.time = utility.str_to_time("Thu, 20 Nov 2008 03:00:00 +0000")
+ >>> d = a.new_reply("Useful examples")
+ >>> d.uuid = "d"
+ >>> d.time = utility.str_to_time("Thu, 20 Nov 2008 04:00:00 +0000")
+ >>> a.sort(key=lambda comm : comm.time)
+ >>> print a.string_thread(flatten=True)
+ --------- Comment ---------
+ Name: //a
+ From:
+ Date: Thu, 20 Nov 2008 01:00:00 +0000
+ <BLANKLINE>
+ Insightful remarks
+ --------- Comment ---------
+ Name: //b
+ From:
+ Date: Thu, 20 Nov 2008 02:00:00 +0000
+ <BLANKLINE>
+ Critique original comment
+ --------- Comment ---------
+ Name: //c
+ From:
+ Date: Thu, 20 Nov 2008 03:00:00 +0000
+ <BLANKLINE>
+ Begin flamewar :p
+ --------- Comment ---------
+ Name: //d
+ From:
+ Date: Thu, 20 Nov 2008 04:00:00 +0000
+ <BLANKLINE>
+ Useful examples
+ >>> print a.string_thread()
+ --------- Comment ---------
+ Name: //a
+ From:
+ Date: Thu, 20 Nov 2008 01:00:00 +0000
+ <BLANKLINE>
+ Insightful remarks
+ --------- Comment ---------
+ Name: //b
+ From:
+ Date: Thu, 20 Nov 2008 02:00:00 +0000
+ <BLANKLINE>
+ Critique original comment
+ --------- Comment ---------
+ Name: //c
+ From:
+ Date: Thu, 20 Nov 2008 03:00:00 +0000
+ <BLANKLINE>
+ Begin flamewar :p
+ --------- Comment ---------
+ Name: //d
+ From:
+ Date: Thu, 20 Nov 2008 04:00:00 +0000
+ <BLANKLINE>
+ Useful examples
+ """
+ stringlist = []
+ for depth,comment in self.thread(flatten=flatten):
+ ind = 2*depth+indent
+ string_fn = getattr(comment, string_method_name)
+ stringlist.append(string_fn(indent=ind))
+ return '\n'.join(stringlist)
+
+ 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 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.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 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).
+ """
+ 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)
+
+ def remove(self):
+ 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, content_type=None):
+ """
+ >>> comm = Comment(bug=None, body="Some insightful remarks")
+ >>> repA = comm.new_reply("Critique original comment")
+ >>> repB = repA.new_reply("Begin flamewar :p")
+ >>> repB.in_reply_to == repA.uuid
+ True
+ """
+ reply = Comment(self.bug, body=body, content_type=content_type)
+ self.add_reply(reply)
+ return reply
+
+ def comment_from_uuid(self, uuid, match_alt_id=True):
+ """Use a uuid 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"
+ >>> 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()
+ >>> commentA.author = "John Doe"
+ >>> commentB.author = "Jane Doe"
+ >>> cmp_attr(commentA, commentB, attr) > 0
+ True
+ >>> cmp_attr(commentA, commentB, attr, invert=True) < 0
+ True
+ >>> commentB.author = "John Doe"
+ >>> cmp_attr(commentA, commentB, attr) == 0
+ True
+ """
+ if not hasattr(comment_2, attr) :
+ return 1
+ val_1 = getattr(comment_1, attr)
+ val_2 = getattr(comment_2, attr)
+ if val_1 == None: val_1 = None
+ if val_2 == None: val_2 = None
+
+ if invert == True :
+ return -cmp(val_1, val_2)
+ else :
+ return cmp(val_1, val_2)
+
+# alphabetical rankings (a < z)
+cmp_uuid = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "uuid")
+cmp_author = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "author")
+cmp_in_reply_to = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "in_reply_to")
+cmp_content_type = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "content_type")
+cmp_body = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "body")
+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_extra_strings)
+
+class CommentCompoundComparator (object):
+ def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
+ self.cmp_list = cmp_list
+ def __call__(self, comment_1, comment_2):
+ for comparison in self.cmp_list :
+ val = comparison(comment_1, comment_2)
+ if val != 0 :
+ return val
+ return 0
+
+cmp_full = CommentCompoundComparator()
+
+if libbe.TESTING == True:
+ suite = doctest.DocTestSuite()
diff --git a/libbe/diff.py b/libbe/diff.py
new file mode 100644
index 0000000..dc13b61
--- /dev/null
+++ b/libbe/diff.py
@@ -0,0 +1,691 @@
+# 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.
+
+"""Tools for comparing two :class:`libbe.bug.BugDir`\s.
+"""
+
+import difflib
+import types
+
+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 SubscriptionType (libbe.util.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):
+ 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')
+ >>> bugdir.append(bugs)
+ >>> new = DiffTree('new', 'new bugs: ABC, DEF')
+ >>> bugs.append(new)
+ >>> rem = DiffTree('rem', 'removed bugs: RST, UVW')
+ >>> bugs.append(rem)
+ >>> print bugdir.report_string()
+ target: None -> 1.0
+ bug-count: 5 -> 6
+ new bugs: ABC, DEF
+ removed bugs: RST, UVW
+ >>> print '\\n'.join(bugdir.paths())
+ bugdir
+ bugdir/settings
+ bugdir/bugs
+ bugdir/bugs/new
+ bugdir/bugs/rem
+ >>> bugdir.child_by_path('/') == bugdir
+ True
+ >>> bugdir.child_by_path('/bugs') == bugs
+ True
+ >>> bugdir.child_by_path('/bugs/rem') == rem
+ True
+ >>> bugdir.child_by_path('bugdir') == bugdir
+ True
+ >>> bugdir.child_by_path('bugdir/') == bugdir
+ True
+ >>> bugdir.child_by_path('bugdir/bugs') == bugs
+ True
+ >>> bugdir.child_by_path('/bugs').masked = True
+ >>> print bugdir.report_string()
+ target: None -> 1.0
+ """
+ def __init__(self, name, data=None, data_part_fn=str,
+ requires_children=False, masked=False):
+ libbe.util.tree.Tree.__init__(self)
+ self.name = name
+ self.data = data
+ self.data_part_fn = data_part_fn
+ self.requires_children = requires_children
+ self.masked = masked
+ def paths(self, parent_path=None):
+ paths = []
+ if parent_path == None:
+ path = self.name
+ else:
+ path = '%s/%s' % (parent_path, self.name)
+ paths.append(path)
+ for child in self:
+ paths.extend(child.paths(path))
+ return paths
+ def child_by_path(self, path):
+ if hasattr(path, 'split'): # convert string path to a list of names
+ names = path.split('/')
+ if names[0] == '':
+ names[0] = self.name # replace root with self
+ if len(names) > 1 and names[-1] == '':
+ names = names[:-1] # strip empty tail
+ else: # it was already an array
+ names = path
+ assert len(names) > 0, path
+ if names[0] == self.name:
+ if len(names) == 1:
+ return self
+ for child in self:
+ if names[1] == child.name:
+ return child.child_by_path(names[1:])
+ if len(names) == 1:
+ raise KeyError, "%s doesn't match '%s'" % (names, self.name)
+ raise KeyError, '%s points to child not in %s' % (names, [c.name for c in self])
+ def report_string(self):
+ 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 root
+ data_part = self.data_part(depth)
+ 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:
+ root = child.report(root, self, depth)
+ return root
+ def make_root(self):
+ return []
+ def join(self, root, parent, data_part):
+ if data_part != None:
+ root.append(data_part)
+ def data_part(self, depth, indent=True):
+ if self.data == None:
+ return None
+ if hasattr(self, '_cached_data_part'):
+ return self._cached_data_part
+ data_part = self.data_part_fn(self.data)
+ if indent == True:
+ data_part_lines = data_part.splitlines()
+ indent = ' '*(depth)
+ line_sep = '\n'+indent
+ data_part = indent+line_sep.join(data_part_lines)
+ self._cached_data_part = data_part
+ return data_part
+
+class Diff (object):
+ """Difference tree generator for BugDirs.
+
+ Examples
+ --------
+
+ >>> import copy
+ >>> bd = libbe.bugdir.SimpleBugDir(memory=True)
+ >>> bd_new = copy.deepcopy(bd)
+ >>> bd_new.target = '1.0'
+ >>> a = bd_new.bug_from_uuid('a')
+ >>> rep = a.comment_root.new_reply("I'm closing this bug")
+ >>> rep.uuid = 'acom'
+ >>> rep.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('Bug C', _uuid='c')
+ >>> d = Diff(bd, bd_new)
+ >>> r = d.report_tree()
+ >>> print '\\n'.join(r.paths())
+ bugdir
+ bugdir/settings
+ bugdir/bugs
+ bugdir/bugs/new
+ bugdir/bugs/new/c
+ bugdir/bugs/rem
+ bugdir/bugs/rem/b
+ bugdir/bugs/mod
+ bugdir/bugs/mod/a
+ bugdir/bugs/mod/a/settings
+ bugdir/bugs/mod/a/comments
+ bugdir/bugs/mod/a/comments/new
+ bugdir/bugs/mod/a/comments/new/acom
+ bugdir/bugs/mod/a/comments/rem
+ bugdir/bugs/mod/a/comments/mod
+ >>> print r.report_string()
+ Changed bug directory settings:
+ target: None -> 1.0
+ New bugs:
+ abc/c:om: Bug C
+ Removed bugs:
+ abc/b:cm: Bug B
+ Modified bugs:
+ 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):
+ self.old_bugdir = old_bugdir
+ self.new_bugdir = new_bugdir
+
+ # data assembly methods
+
+ def _changed_bugs(self, subscriptions):
+ """
+ Search for differences in all bugs between .old_bugdir and
+ .new_bugdir. Returns
+ (added_bugs, modified_bugs, removed_bugs)
+ where added_bugs and removed_bugs are lists of added and
+ removed bugs respectively. modified_bugs is a list of
+ (old_bug,new_bug) pairs.
+ """
+ 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 = []
+ 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)
+ return (added, modified, removed)
+ def _bug_modified_cmp(self, left, right):
+ return cmp(left[1], right[1])
+ def _changed_comments(self, old, new):
+ """
+ Search for differences in all loaded comments between the bugs
+ old and new. Returns
+ (added_comments, modified_comments, removed_comments)
+ analogous to ._changed_bugs.
+ """
+ if hasattr(self, '__changed_comments'):
+ if new.uuid in self.__changed_comments:
+ return self.__changed_comments[new.uuid]
+ else:
+ self.__changed_comments = {}
+ added = []
+ removed = []
+ modified = []
+ old.comment_root.sort(key=lambda comm : comm.time)
+ new.comment_root.sort(key=lambda comm : comm.time)
+ old_comment_ids = [c.uuid for c in old.comments()]
+ new_comment_ids = [c.uuid for c in new.comments()]
+ for uuid in new_comment_ids:
+ new_comment = new.comment_from_uuid(uuid)
+ try:
+ old_comment = old.comment_from_uuid(uuid)
+ except KeyError:
+ added.append(new_comment)
+ else:
+ if old_comment != new_comment:
+ modified.append((old_comment, new_comment))
+ for uuid in old_comment_ids:
+ if uuid not in new_comment_ids:
+ 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):
+ """
+ Take two objects old and new, and compare the value of *.attr
+ for attr in the list attribute names. Returns a list of
+ (attr_name, old_value, new_value)
+ tuples.
+ """
+ change_list = []
+ for attr in attributes:
+ old_value = getattr(old, attr)
+ new_value = getattr(new, attr)
+ if old_value != new_value:
+ change_list.append((attr, old_value, new_value))
+ if len(change_list) >= 0:
+ return change_list
+ return None
+ def _settings_properties_attribute_changes(self, old, new,
+ hidden_properties=[]):
+ properties = sorted(new.settings_properties)
+ for p in hidden_properties:
+ properties.remove(p)
+ attributes = [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)
+ def _bug_attribute_changes(self, old, new):
+ return self._settings_properties_attribute_changes(old, new)
+ def _comment_attribute_changes(self, old, new):
+ return self._settings_properties_attribute_changes(old, new)
+
+ # report generation methods
+
+ def 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 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)
+ 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(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)
+ bug_root.append(brem)
+ for bug in rem:
+ b = diff_tree(bug.uuid, bug, self.bug_rem_string)
+ brem.append(b)
+ bmod = diff_tree('mod', 'Modified bugs:', requires_children=True)
+ bug_root.append(bmod)
+ for old,new in mod:
+ b = diff_tree(new.uuid, (old,new), self.bug_mod_string)
+ bmod.append(b)
+ bug_attribute_changes = self._bug_attribute_changes(old, new)
+ if len(bug_attribute_changes) > 0:
+ bset = diff_tree('settings', bug_attribute_changes,
+ self.bug_attribute_change_string)
+ b.append(bset)
+ if old.summary != new.summary:
+ data = (old.summary, new.summary)
+ bsum = diff_tree('summary', data, self.bug_summary_change_string)
+ b.append(bsum)
+ cr = diff_tree('comments')
+ b.append(cr)
+ a,m,d = self._changed_comments(old, new)
+ cnew = diff_tree('new', 'New comments:', requires_children=True)
+ for comment in a:
+ c = diff_tree(comment.uuid, comment, self.comment_add_string)
+ cnew.append(c)
+ crem = diff_tree('rem', 'Removed comments:',requires_children=True)
+ for comment in d:
+ c = diff_tree(comment.uuid, comment, self.comment_rem_string)
+ crem.append(c)
+ cmod = diff_tree('mod','Modified comments:',requires_children=True)
+ for o,n in m:
+ c = diff_tree(n.uuid, (o,n), self.comment_mod_string)
+ cmod.append(c)
+ comm_attribute_changes = self._comment_attribute_changes(o, n)
+ if len(comm_attribute_changes) > 0:
+ cset = diff_tree('settings', comm_attribute_changes,
+ self.comment_attribute_change_string)
+ if o.body != n.body:
+ data = (o.body, n.body)
+ cbody = diff_tree('cbody', data,
+ self.comment_body_change_string)
+ c.append(cbody)
+ cr.extend([cnew, crem, cmod])
+ 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]
+ for i,change_string in enumerate(change_strings):
+ change_strings[i] = indent_string+change_string
+ return u'\n'.join(change_strings)
+ def bugdir_attribute_change_string(self, attribute_changes):
+ return 'Changed bug directory settings:\n%s' % \
+ self.attribute_change_string(attribute_changes, indent=1)
+ def bug_attribute_change_string(self, attribute_changes):
+ return 'Changed bug settings:\n%s' % \
+ self.attribute_change_string(attribute_changes, indent=1)
+ def comment_attribute_change_string(self, attribute_changes):
+ return 'Changed comment settings:\n%s' % \
+ self.attribute_change_string(attribute_changes, indent=1)
+ def bug_add_string(self, bug):
+ return bug.string(shortlist=True)
+ def bug_rem_string(self, bug):
+ return bug.string(shortlist=True)
+ def bug_mod_string(self, bugs):
+ old_bug,new_bug = bugs
+ return new_bug.string(shortlist=True)
+ def bug_summary_change_string(self, summaries):
+ old_summary,new_summary = summaries
+ return 'summary changed:\n %s\n %s' % (old_summary, new_summary)
+ def _comment_summary_string(self, comment):
+ return 'from %s on %s' % (comment.author, time_to_str(comment.time))
+ def comment_add_string(self, comment):
+ summary = self._comment_summary_string(comment)
+ first_line = comment.body.splitlines()[0]
+ return '%s\n %s...' % (summary, first_line)
+ def comment_rem_string(self, comment):
+ summary = self._comment_summary_string(comment)
+ first_line = comment.body.splitlines()[0]
+ return '%s\n %s...' % (summary, first_line)
+ def comment_mod_string(self, comments):
+ old_comment,new_comment = comments
+ return self._comment_summary_string(new_comment)
+ def comment_body_change_string(self, bodies):
+ old_body,new_body = bodies
+ return ''.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
index 0000000..798136e
--- /dev/null
+++ b/libbe/error.py
@@ -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/storage/__init__.py b/libbe/storage/__init__.py
new file mode 100644
index 0000000..6bceac9
--- /dev/null
+++ b/libbe/storage/__init__.py
@@ -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
index 0000000..0ae9c53
--- /dev/null
+++ b/libbe/storage/base.py
@@ -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
index 0000000..7ec9f54
--- /dev/null
+++ b/libbe/storage/http.py
@@ -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()])
diff --git a/libbe/storage/util/__init__.py b/libbe/storage/util/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/libbe/storage/util/__init__.py
diff --git a/libbe/storage/util/config.py b/libbe/storage/util/config.py
new file mode 100644
index 0000000..724d2d3
--- /dev/null
+++ b/libbe/storage/util/config.py
@@ -0,0 +1,114 @@
+# 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.
+
+"""Create, save, and load the per-user config file at :func:`path`.
+"""
+
+import ConfigParser
+import codecs
+import os.path
+
+import libbe
+import libbe.util.encoding
+if libbe.TESTING == True:
+ import doctest
+
+
+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 os.path.expanduser("~/.bugs_everywhere")
+
+def set_val(name, value, section="DEFAULT", encoding=None):
+ """Set a value in the per-user config file.
+
+ 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)
+ config.readfp(f, path())
+ f.close()
+ if value is not None:
+ config.set(section, name, value)
+ else:
+ config.remove_option(section, name)
+ f = codecs.open(path(), 'w', encoding)
+ config.write(f)
+ f.close()
+
+def get_val(name, section="DEFAULT", default=None, encoding=None):
+ """Get a value from the per-user config file
+
+ 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
+ --------
+
+ >>> get_val("junk") is None
+ True
+ >>> set_val("junk", "random")
+ >>> get_val("junk")
+ u'random'
+ >>> set_val("junk", None)
+ >>> get_val("junk") is None
+ True
+ """
+ if os.path.exists(path()):
+ if encoding == None:
+ encoding = default_encoding
+ config = ConfigParser.ConfigParser()
+ f = codecs.open(path(), 'r', encoding)
+ config.readfp(f, path())
+ f.close()
+ try:
+ return config.get(section, name)
+ except ConfigParser.NoOptionError:
+ return default
+ else:
+ return default
+
+if libbe.TESTING == True:
+ suite = doctest.DocTestSuite()
diff --git a/libbe/storage/util/mapfile.py b/libbe/storage/util/mapfile.py
new file mode 100644
index 0000000..55863d7
--- /dev/null
+++ b/libbe/storage/util/mapfile.py
@@ -0,0 +1,146 @@
+# 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.
+
+"""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 libbe
+if libbe.TESTING == True:
+ import doctest
+
+
+class IllegalKey(Exception):
+ def __init__(self, key):
+ Exception.__init__(self, 'Illegal key "%s"' % key)
+ self.key = key
+
+class IllegalValue(Exception):
+ def __init__(self, value):
+ Exception.__init__(self, 'Illegal value "%s"' % value)
+ self.value = value
+
+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.
+
+ Examples
+ --------
+
+ >>> generate({'q':'p'})
+ 'q: p\\n\\n'
+ >>> generate({'q':u'Fran\u00e7ais'})
+ 'q: Fran\\xc3\\xa7ais\\n\\n'
+ >>> generate({'q':u'hello'})
+ 'q: hello\\n\\n'
+ >>> generate({'q=':'p'})
+ Traceback (most recent call last):
+ IllegalKey: Illegal key "q="
+ >>> generate({'q:':'p'})
+ Traceback (most recent call last):
+ IllegalKey: Illegal key "q:"
+ >>> generate({'q\\n':'p'})
+ Traceback (most recent call last):
+ IllegalKey: Illegal key "q\\n"
+ >>> generate({'':'p'})
+ Traceback (most recent call last):
+ IllegalKey: Illegal key ""
+ >>> generate({'>q':'p'})
+ Traceback (most recent call last):
+ IllegalKey: Illegal key ">q"
+ >>> generate({'q':'p\\n'})
+ Traceback (most recent call last):
+ IllegalValue: Illegal value "p\\n"
+
+ See Also
+ --------
+ parse : inverse
+ """
+ keys = map.keys()
+ keys.sort()
+ for key in keys:
+ try:
+ assert not key.startswith('>')
+ assert('\n' not in key)
+ assert('=' not in key)
+ assert(':' not in key)
+ assert(len(key) > 0)
+ except AssertionError:
+ raise IllegalKey(unicode(key).encode('unicode_escape'))
+ if '\n' in map[key]:
+ raise IllegalValue(unicode(map[key]).encode('unicode_escape'))
+
+ lines = []
+ for key in keys:
+ lines.append(yaml.safe_dump({key: map[key]},
+ default_flow_style=False,
+ allow_unicode=True))
+ lines.append('')
+ return '\n'.join(lines)
+
+def parse(contents):
+ """Parse a YAML mapfile string.
+
+ Examples
+ --------
+
+ >>> parse('q: p\\n\\n')['q']
+ 'p'
+ >>> parse('q: \\'p\\'\\n\\n')['q']
+ 'p'
+ >>> contents = generate({'a':'b', 'c':'d', 'e':'f'})
+ >>> dict = parse(contents)
+ >>> dict['a']
+ 'b'
+ >>> dict['c']
+ 'd'
+ >>> dict['e']
+ 'f'
+ >>> 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
+
+ See Also
+ --------
+ generate : inverse
+
+ """
+ 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 {}
+
+if libbe.TESTING == True:
+ suite = doctest.DocTestSuite()
diff --git a/libbe/storage/util/properties.py b/libbe/storage/util/properties.py
new file mode 100644
index 0000000..b5681b1
--- /dev/null
+++ b/libbe/storage/util/properties.py
@@ -0,0 +1,666 @@
+# 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 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 libbe
+if libbe.TESTING == True:
+ import unittest
+
+
+class ValueCheckError (ValueError):
+ def __init__(self, name, value, allowed):
+ action = "in" # some list of allowed values
+ if type(allowed) == types.FunctionType:
+ action = "allowed by" # some allowed-value check function
+ msg = "%s not %s %s for %s" % (value, action, allowed, name)
+ ValueError.__init__(self, msg)
+ self.name = name
+ self.value = value
+ self.allowed = allowed
+
+def Property(funcs):
+ """
+ End a chain of property decorators, returning a property.
+ """
+ args = {}
+ args["fget"] = funcs.get("fget", None)
+ args["fset"] = funcs.get("fset", None)
+ args["fdel"] = funcs.get("fdel", None)
+ args["doc"] = funcs.get("doc", None)
+
+ #print "Creating a property with"
+ #for key, val in args.items(): print key, value
+ return property(**args)
+
+def doc_property(doc=None):
+ """
+ Add a docstring to a chain of property decorators.
+ """
+ def decorator(funcs=None):
+ """
+ Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...}
+ or a function fn() returning such a dict.
+ """
+ if hasattr(funcs, "__call__"):
+ funcs = funcs() # convert from function-arg to dict
+ funcs["doc"] = doc
+ return funcs
+ return decorator
+
+def local_property(name, null=None, mutable_null=False):
+ """
+ Define get/set access to per-parent-instance local storage. Uses
+ ._<name>_value to store the value for a particular owner instance.
+ If the ._<name>_value attribute does not exist, returns null.
+
+ If mutable_null == True, we only release deepcopies of the null to
+ the outside world.
+ """
+ def decorator(funcs):
+ if hasattr(funcs, "__call__"):
+ funcs = funcs()
+ fget = funcs.get("fget", None)
+ fset = funcs.get("fset", None)
+ def _fget(self):
+ if fget is not None:
+ fget(self)
+ if mutable_null == True:
+ ret_null = copy.deepcopy(null)
+ else:
+ ret_null = null
+ value = getattr(self, "_%s_value" % name, ret_null)
+ return value
+ def _fset(self, value):
+ setattr(self, "_%s_value" % name, value)
+ if fset is not None:
+ fset(self, value)
+ funcs["fget"] = _fget
+ funcs["fset"] = _fset
+ funcs["name"] = name
+ return funcs
+ return decorator
+
+def settings_property(name, null=None):
+ """
+ Similar to local_property, except where local_property stores the
+ value in instance._<name>_value, settings_property stores the
+ value in instance.settings[name].
+ """
+ def decorator(funcs):
+ if hasattr(funcs, "__call__"):
+ funcs = funcs()
+ fget = funcs.get("fget", None)
+ fset = funcs.get("fset", None)
+ def _fget(self):
+ if fget is not None:
+ fget(self)
+ value = self.settings.get(name, null)
+ return value
+ def _fset(self, value):
+ self.settings[name] = value
+ if fset is not None:
+ fset(self, value)
+ funcs["fget"] = _fget
+ funcs["fset"] = _fset
+ funcs["name"] = name
+ return funcs
+ return decorator
+
+
+# Allow comparison and caching with _original_ values for mutables,
+# since
+#
+# >>> a = []
+# >>> b = a
+# >>> b.append(1)
+# >>> a
+# [1]
+# >>> a==b
+# True
+def _hash_mutable_value(value):
+ return repr(value)
+def _init_mutable_property_cache(self):
+ if not hasattr(self, "_mutable_property_cache_hash"):
+ # first call to _fget for any mutable property
+ self._mutable_property_cache_hash = {}
+ self._mutable_property_cache_copy = {}
+def _set_cached_mutable_property(self, cacher_name, property_name, value):
+ _init_mutable_property_cache(self)
+ self._mutable_property_cache_hash[(cacher_name, property_name)] = \
+ _hash_mutable_value(value)
+ self._mutable_property_cache_copy[(cacher_name, property_name)] = \
+ copy.deepcopy(value)
+def _get_cached_mutable_property(self, cacher_name, property_name, default=None):
+ _init_mutable_property_cache(self)
+ if (cacher_name, property_name) not in self._mutable_property_cache_copy:
+ return default
+ return self._mutable_property_cache_copy[(cacher_name, property_name)]
+def _cmp_cached_mutable_property(self, cacher_name, property_name, value, default=None):
+ _init_mutable_property_cache(self)
+ if (cacher_name, property_name) not in self._mutable_property_cache_hash:
+ _set_cached_mutable_property(self, cacher_name, property_name, default)
+ old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)]
+ return cmp(_hash_mutable_value(value), old_hash)
+
+
+def defaulting_property(default=None, null=None,
+ mutable_default=False):
+ """
+ Define a default value for get access to a property.
+ If the stored value is null, then default is returned.
+
+ If mutable_default == True, we only release deepcopies of the
+ default to the outside world.
+
+ null should never escape to the outside world, so don't worry
+ about it being a mutable.
+ """
+ def decorator(funcs):
+ if hasattr(funcs, "__call__"):
+ funcs = funcs()
+ fget = funcs.get("fget")
+ fset = funcs.get("fset")
+ name = funcs.get("name", "<unknown>")
+ def _fget(self):
+ value = fget(self)
+ if value == null:
+ if mutable_default == True:
+ return copy.deepcopy(default)
+ else:
+ return default
+ return value
+ def _fset(self, value):
+ if value == default:
+ value = null
+ fset(self, value)
+ funcs["fget"] = _fget
+ funcs["fset"] = _fset
+ return funcs
+ return decorator
+
+def fn_checked_property(value_allowed_fn):
+ """
+ Define allowed values for get/set access to a property.
+ """
+ def decorator(funcs):
+ if hasattr(funcs, "__call__"):
+ funcs = funcs()
+ fget = funcs.get("fget")
+ fset = funcs.get("fset")
+ name = funcs.get("name", "<unknown>")
+ def _fget(self):
+ value = fget(self)
+ if value_allowed_fn(value) != True:
+ raise ValueCheckError(name, value, value_allowed_fn)
+ return value
+ def _fset(self, value):
+ if value_allowed_fn(value) != True:
+ raise ValueCheckError(name, value, value_allowed_fn)
+ fset(self, value)
+ funcs["fget"] = _fget
+ funcs["fset"] = _fset
+ return funcs
+ return decorator
+
+def checked_property(allowed=[]):
+ """
+ Define allowed values for get/set access to a property.
+ """
+ def decorator(funcs):
+ if hasattr(funcs, "__call__"):
+ funcs = funcs()
+ fget = funcs.get("fget")
+ fset = funcs.get("fset")
+ name = funcs.get("name", "<unknown>")
+ def _fget(self):
+ value = fget(self)
+ if value not in allowed:
+ raise ValueCheckError(name, value, allowed)
+ return value
+ def _fset(self, value):
+ if value not in allowed:
+ raise ValueCheckError(name, value, allowed)
+ fset(self, value)
+ funcs["fget"] = _fget
+ funcs["fset"] = _fset
+ return funcs
+ return decorator
+
+def cached_property(generator, initVal=None, mutable=False):
+ """
+ Allow caching of values generated by generator(instance), where
+ instance is the instance to which this property belongs. Uses
+ ._<name>_cache to store a cache flag for a particular owner
+ instance.
+
+ When the cache flag is True or missing and the stored value is
+ initVal, the first fget call triggers the generator function,
+ whose output is stored in _<name>_cached_value. That and
+ subsequent calls to fget will return this cached value.
+
+ If the input value is no longer initVal (e.g. a value has been
+ loaded from disk or set with fset), that value overrides any
+ cached value, and this property has no effect.
+
+ When the cache flag is False and the stored value is initVal, the
+ generator is not cached, but is called on every fget.
+
+ The cache flag is missing on initialization. Particular instances
+ may override by setting their own flag.
+
+ In the case that mutable == True, all caching is disabled and the
+ generator is called whenever the cached value would otherwise be
+ used.
+ """
+ def decorator(funcs):
+ if hasattr(funcs, "__call__"):
+ funcs = funcs()
+ fget = funcs.get("fget")
+ name = funcs.get("name", "<unknown>")
+ def _fget(self):
+ cache = getattr(self, "_%s_cache" % name, True)
+ value = fget(self)
+ if value == initVal:
+ if cache == True and mutable == False:
+ if hasattr(self, "_%s_cached_value" % name):
+ value = getattr(self, "_%s_cached_value" % name)
+ else:
+ value = generator(self)
+ setattr(self, "_%s_cached_value" % name, value)
+ else:
+ value = generator(self)
+ return value
+ funcs["fget"] = _fget
+ return funcs
+ return decorator
+
+def primed_property(primer, initVal=None, unprimeableVal=None):
+ """
+ Just like a cached_property, except that instead of returning a
+ 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. 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
+ value == initVal.
+ """
+ def decorator(funcs):
+ if hasattr(funcs, "__call__"):
+ funcs = funcs()
+ fget = funcs.get("fget")
+ name = funcs.get("name", "<unknown>")
+ def _fget(self):
+ prime = getattr(self, "_%s_prime" % name, False)
+ if prime == False:
+ value = fget(self)
+ if prime == True or (prime == False and value == initVal):
+ primer(self)
+ value = fget(self)
+ 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` 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
+ change the stored value if you want.
+
+ In the case of mutables, things are slightly trickier. Because
+ the property-owning class has no way of knowing when the value
+ changes. We work around this by caching a private deepcopy of the
+ mutable value, and checking for changes whenever the property is
+ set (obviously) or retrieved (to check for external changes). So
+ long as you're conscientious about accessing the property after
+ making external modifications, mutability won't be a problem::
+
+ t.x.append(5) # external modification
+ t.x # dummy access notices change and triggers hook
+
+ 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__"):
+ funcs = funcs()
+ fget = funcs.get("fget")
+ fset = funcs.get("fset")
+ name = funcs.get("name", "<unknown>")
+ def _fget(self, new_value=None, from_fset=False): # only used if mutable == True
+ if from_fset == True:
+ value = new_value # compare new value with cached
+ else:
+ value = fget(self) # compare current value with cached
+ if _cmp_cached_mutable_property(self, "change hook property", name, value, default) != 0:
+ # there has been a change, cache new value
+ old_value = _get_cached_mutable_property(self, "change hook property", name, default)
+ _set_cached_mutable_property(self, "change hook property", name, value)
+ if from_fset == True: # return previously cached value
+ value = old_value
+ else: # the value changed while we weren't looking
+ hook(self, old_value, value)
+ return value
+ def _fset(self, value):
+ if mutable == True: # get cached previous value
+ old_value = _fget(self, new_value=value, from_fset=True)
+ else:
+ old_value = fget(self)
+ fset(self, value)
+ if value != old_value:
+ hook(self, old_value, value)
+ if mutable == True:
+ funcs["fget"] = _fget
+ funcs["fset"] = _fset
+ return funcs
+ return decorator
+
+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
+ 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
+ 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
index 0000000..6e4da55
--- /dev/null
+++ b/libbe/storage/util/settings_object.py
@@ -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
index 0000000..f3c4912
--- /dev/null
+++ b/libbe/storage/util/upgrade.py
@@ -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
index 0000000..552d43e
--- /dev/null
+++ b/libbe/storage/vcs/__init__.py
@@ -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
index 0000000..3a50414
--- /dev/null
+++ b/libbe/storage/vcs/arch.py
@@ -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))
+
+
+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
index 0000000..d85c94d
--- /dev/null
+++ b/libbe/storage/vcs/base.py
@@ -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))
+
+
+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
index 0000000..5a62968
--- /dev/null
+++ b/libbe/storage/vcs/bzr.py
@@ -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))
+
+
+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
index 0000000..0f23278
--- /dev/null
+++ b/libbe/storage/vcs/darcs.py
@@ -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._vcs_get_file_contents(pref_path).strip()
+ 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))
+
+
+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
index 0000000..4df9bc8
--- /dev/null
+++ b/libbe/storage/vcs/git.py
@@ -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))
+
+
+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
index 0000000..9378336
--- /dev/null
+++ b/libbe/storage/vcs/hg.py
@@ -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))
+
+
+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
index 0000000..3b461a5
--- /dev/null
+++ b/libbe/ui/__init__.py
@@ -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
index 0000000..dd10954
--- /dev/null
+++ b/libbe/ui/command_line.py
@@ -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
index 0000000..3b461a5
--- /dev/null
+++ b/libbe/ui/util/__init__.py
@@ -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/util/editor.py b/libbe/ui/util/editor.py
new file mode 100644
index 0000000..1a430c7
--- /dev/null
+++ b/libbe/ui/util/editor.py
@@ -0,0 +1,115 @@
+# 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.
+
+"""
+Define editor_string(), a function that invokes an editor to accept
+user-produced text as a string.
+"""
+
+import codecs
+import locale
+import os
+import sys
+import tempfile
+
+import libbe
+import libbe.util.encoding
+
+if libbe.TESTING == True:
+ import doctest
+
+
+comment_marker = u"== Anything below this line will be ignored\n"
+
+class CantFindEditor(Exception):
+ def __init__(self):
+ Exception.__init__(self, "Can't find editor to get string from")
+
+def editor_string(comment=None, encoding=None):
+ """Invokes the editor, and returns the user-produced text as a string
+
+ >>> if "EDITOR" in os.environ:
+ ... del os.environ["EDITOR"]
+ >>> if "VISUAL" in os.environ:
+ ... del os.environ["VISUAL"]
+ >>> editor_string()
+ Traceback (most recent call last):
+ CantFindEditor: Can't find editor to get string from
+ >>> os.environ["EDITOR"] = "echo bar > "
+ >>> editor_string()
+ u'bar\\n'
+ >>> os.environ["VISUAL"] = "echo baz > "
+ >>> editor_string()
+ u'baz\\n'
+ >>> 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 = libbe.util.encoding.get_filesystem_encoding()
+ editor = None
+ for name in ('VISUAL', 'EDITOR'):
+ if name in os.environ and os.environ[name] != '':
+ editor = os.environ[name]
+ break
+ if editor == None:
+ raise CantFindEditor()
+ fhandle, fname = tempfile.mkstemp()
+ try:
+ if comment is not None:
+ cstring = u'\n'+comment_string(comment)
+ os.write(fhandle, cstring.encode(encoding))
+ os.close(fhandle)
+ oldmtime = os.path.getmtime(fname)
+ os.system("%s %s" % (editor, fname))
+ output = libbe.util.encoding.get_file_contents(
+ fname, encoding=encoding, decode=True)
+ output = trimmed_string(output)
+ if output.rstrip('\n') == "":
+ output = None
+ finally:
+ os.unlink(fname)
+ return output
+
+
+def comment_string(comment):
+ """
+ >>> comment_string('hello') == comment_marker+"hello"
+ True
+ """
+ return comment_marker + comment
+
+
+def trimmed_string(instring):
+ """
+ >>> trimmed_string("hello\\n"+comment_marker)
+ u'hello\\n'
+ >>> trimmed_string("hi!\\n" + comment_string('Booga'))
+ u'hi!\\n'
+ """
+ out = []
+ for line in instring.splitlines(True):
+ if line.startswith(comment_marker):
+ break
+ out.append(line)
+ return ''.join(out)
+
+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
index 0000000..88b58af
--- /dev/null
+++ b/libbe/ui/util/pager.py
@@ -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
index 0000000..460a1dd
--- /dev/null
+++ b/libbe/ui/util/user.py
@@ -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/util/__init__.py b/libbe/util/__init__.py
new file mode 100644
index 0000000..0f4850f
--- /dev/null
+++ b/libbe/util/__init__.py
@@ -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
+
diff --git a/libbe/util/encoding.py b/libbe/util/encoding.py
new file mode 100644
index 0000000..8eea438
--- /dev/null
+++ b/libbe/util/encoding.py
@@ -0,0 +1,91 @@
+# 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.
+
+"""
+Support input/output/filesystem encodings (e.g. UTF-8).
+"""
+
+import codecs
+import locale
+import sys
+import types
+
+import libbe
+if libbe.TESTING == True:
+ import doctest
+
+
+ENCODING = None # override get_encoding() output by setting this
+
+def get_encoding():
+ """
+ Guess a useful input/output/filesystem encoding... Maybe we need
+ seperate encodings for input/output and filesystem? Hmm...
+ """
+ if ENCODING != None:
+ return ENCODING
+ encoding = locale.getpreferredencoding() or sys.getdefaultencoding()
+ if sys.platform != 'win32' or sys.version_info[:2] > (2, 3):
+ encoding = locale.getlocale(locale.LC_TIME)[1] or encoding
+ # Python 2.3 on windows doesn't know about 'XYZ' alias for 'cpXYZ'
+ return encoding
+
+def 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")
+ False
+ >>> known_encoding(get_encoding())
+ True
+ """
+ try:
+ codecs.lookup(encoding)
+ return True
+ except LookupError:
+ return False
+
+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()
+
+if libbe.TESTING == True:
+ suite = doctest.DocTestSuite()
diff --git a/libbe/util/id.py b/libbe/util/id.py
new file mode 100644
index 0000000..9192ac8
--- /dev/null
+++ b/libbe/util/id.py
@@ -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()])
diff --git a/libbe/util/plugin.py b/libbe/util/plugin.py
new file mode 100644
index 0000000..e598c34
--- /dev/null
+++ b/libbe/util/plugin.py
@@ -0,0 +1,67 @@
+# 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>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Allow simple listing and loading of the various becommands and libbe
+submodules (i.e. "plugins").
+"""
+
+import os
+import os.path
+import sys
+
+
+_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 modnames(prefix):
+ """
+ >>> 'list' in [n for n in modnames('libbe.command')]
+ True
+ >>> 'plugin' in [n for n in modnames('libbe.util')]
+ True
+ """
+ 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]
diff --git a/libbe/util/subproc.py b/libbe/util/subproc.py
new file mode 100644
index 0000000..b02b8e8
--- /dev/null
+++ b/libbe/util/subproc.py
@@ -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()
diff --git a/libbe/util/tree.py b/libbe/util/tree.py
new file mode 100644
index 0000000..812b0bd
--- /dev/null
+++ b/libbe/util/tree.py
@@ -0,0 +1,258 @@
+# 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.
+
+"""Define :class:`Tree`, a traversable tree structure.
+"""
+
+import libbe
+if libbe.TESTING == True:
+ import doctest
+
+class Tree(list):
+ """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"
+ >>> e = Tree(); e.n = "e"
+ >>> c = Tree([f,e]); c.n = "c"
+ >>> g = Tree(); g.n = "g"
+ >>> d = Tree([g]); d.n = "d"
+ >>> b = Tree([d]); b.n = "b"
+ >>> a = Tree(); a.n = "a"
+ >>> a.append(c)
+ >>> a.append(b)
+
+ 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
+ b
+ d
+ g
+ c
+ e
+ 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
+ b
+ d
+ g
+ c
+ e
+ f
+ h
+ i
+
+ We can also check if a node is contained in a tree.
+
+ >>> a.has_descendant(g)
+ True
+ >>> c.has_descendant(g)
+ False
+ >>> a.has_descendant(a)
+ False
+ >>> a.has_descendant(a, match_self=True)
+ True
+ """
+ def __cmp__(self, other):
+ return cmp(id(self), id(other))
+
+ def __eq__(self, other):
+ return self.__cmp__(other) == 0
+
+ def __ne__(self, other):
+ return self.__cmp__(other) != 0
+
+ def branch_len(self):
+ """Return the largest number of nodes from root to leaf (inclusive).
+
+ 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
+ else:
+ return 1 + max([child.branch_len() for child in self])
+
+ def sort(self, *args, **kwargs):
+ """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):
+ """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
+ for child in self:
+ for descendant in child.traverse():
+ yield descendant
+ else: # breadth first, Wikipedia algorithm
+ # http://en.wikipedia.org/wiki/Breadth-first_search
+ queue = [self]
+ while len(queue) > 0:
+ node = queue.pop(0)
+ yield node
+ queue.extend(node)
+
+ def thread(self, flatten=False):
+ """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 :meth:`branch_len`)::
+
+ (0, a)
+ (1, b)
+ (1, c)
+ (0, d)
+ (0, e)
+ (0, f)
+
+ """
+ stack = [] # ancestry of the current node
+ if flatten == True:
+ depthDict = {}
+
+ for node in self.traverse(depth_first=True):
+ while len(stack) > 0 \
+ and id(node) not in [id(c) for c in stack[-1]]:
+ stack.pop(-1)
+ if flatten == False:
+ depth = len(stack)
+ else:
+ if len(stack) == 0:
+ depth = 0
+ else:
+ parent = stack[-1]
+ depth = depthDict[id(parent)]
+ if len(parent) > 1 and node != parent[-1]:
+ depth += 1
+ depthDict[id(node)] = depth
+ yield (depth,node)
+ stack.append(node)
+
+ def has_descendant(self, descendant, depth_first=True, match_self=False):
+ """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):
+ if descendant == d:
+ return True
+ return False
+
+if libbe.TESTING == True:
+ suite = doctest.DocTestSuite()
diff --git a/libbe/util/utility.py b/libbe/util/utility.py
new file mode 100644
index 0000000..c12e9a2
--- /dev/null
+++ b/libbe/util/utility.py
@@ -0,0 +1,248 @@
+# 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.
+
+"""
+Assorted utility functions that don't fit in anywhere else.
+"""
+
+import calendar
+import codecs
+import os
+import shutil
+import tempfile
+import time
+import types
+
+import 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. 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)
+ old_path = None
+ while True:
+ check_path = os.path.join(path, filename)
+ if os.path.exists(check_path):
+ return check_path
+ if path == old_path:
+ return None
+ old_path = path
+ path = os.path.dirname(path)
+
+class Dir (object):
+ """A temporary directory for testing use.
+
+ 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
+ def cleanup(self):
+ if self.removed == False:
+ shutil.rmtree(self.path)
+ self.removed = True
+ def __call__(self):
+ 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 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()
+ >>> str_to_time(time_to_str(q)) == int(q)
+ 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":
+ str_time = str_time.replace(timezone_str, "+0000")
+ time_val = calendar.timegm(time.strptime(str_time, RFC_2822_TIME_FMT))
+ timesign = -int(timezone_str[0]+"1") # "+" -> time_val ahead of GMT
+ timezone_tuple = time.strptime(timezone_str[1:], "%H%M")
+ timezone = timezone_tuple.tm_hour*3600 + timezone_tuple.tm_min*60
+ return time_val + timesign*timezone
+
+def handy_time(time_val):
+ """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'
+ """
+ time_val = str_to_time(str_time)
+ return time_to_str(time_val)
+
+def iterable_full_of_strings(value, alternative=None):
+ """Require an iterable full of strings.
+
+ 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"])
+ True
+ >>> iterable_full_of_strings(["abc", None, u"hij"])
+ False
+ >>> iterable_full_of_strings(None, alternative=None)
+ True
+ """
+ if value == alternative:
+ return True
+ elif not hasattr(value, '__iter__'):
+ return False
+ for x in value:
+ if type(x) not in types.StringTypes:
+ return False
+ return True
+
+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/version.py b/libbe/version.py
new file mode 100644
index 0000000..2792de4
--- /dev/null
+++ b/libbe/version.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env 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.
+
+"""
+Store version info for this BE installation. By default, use the
+bzr-generated information in _version.py, but allow manual overriding
+by setting _VERSION. This allows support of both the "I don't want to
+be bothered setting version strings" and the "I want complete control
+over the version strings" workflows.
+"""
+
+import copy
+
+import libbe._version as _version
+import libbe.storage
+
+# Manually set a version string (optional, defaults to bzr revision id)
+#_VERSION = "1.2.3"
+
+def version(verbose=False):
+ """
+ Returns the version string for this BE installation. If
+ verbose==True, the string will include extra lines with more
+ detail (e.g. bzr branch nickname, etc.).
+ """
+ if "_VERSION" in globals():
+ string = _VERSION
+ else:
+ string = _version.version_info["revision_id"]
+ if verbose == True:
+ 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\n"
+ "storage version: %(storage)s"
+ % info)
+ return string
+
+if __name__ == "__main__":
+ print version(verbose=True)
diff --git a/misc/completion/be.bash b/misc/completion/be.bash
new file mode 100644
index 0000000..834bf25
--- /dev/null
+++ b/misc/completion/be.bash
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Bash completion script for be (Bugs Everywhere)
+#
+# System wide installation:
+# Copy this file to /etc/bash_completion/be
+# Per-user installation:
+# Copy this file to ~/.be-completion.sh and source it in your .bashrc:
+# source ~/.be-completion.sh
+#
+# For a good intro to Bash completion, see Steve Kemp's article
+# "An introduction to bash completion: part 2"
+# http://www.debian-administration.org/articles/317
+
+# Requires:
+# be [X Y Z] --complete
+# to print a list of available completions at that point
+_be()
+{
+ local cur prev opts
+ COMPREPLY=()
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
+
+ if [ $COMP_CWORD -eq 1 ]; then
+ # no command yet, show all commands
+ COMPREPLY=( $( compgen -W "$(be --complete)" -- $cur ) )
+ else
+ # remove the first word (should be "be") for security reasons
+ unset COMP_WORDS[0]
+ # remove the current word and all later words, because they
+ # are not needed for completion.
+ for i in `seq $COMP_CWORD ${#COMP_WORDS[@]}`; do
+ unset COMP_WORDS[$i];
+ done
+ COMPREPLY=( $( compgen -W "$(be "${COMP_WORDS[@]}" --complete $cur)" -- $cur ) )
+ fi
+}
+
+complete -F _be be
diff --git a/misc/logo.svg b/misc/logo.svg
new file mode 100644
index 0000000..43964b1
--- /dev/null
+++ b/misc/logo.svg
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:xml="http://www.w3.org/XML/1998/namespace"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ sodipodi:docname="logo.svg"
+ sodipodi:docbase="/home/abentley/arch/be"
+ inkscape:version="0.41"
+ sodipodi:version="0.32"
+ id="svg2"
+ height="297mm"
+ width="210mm">
+ <defs
+ id="defs3" />
+ <sodipodi:namedview
+ inkscape:window-y="63"
+ inkscape:window-x="22"
+ inkscape:window-height="759"
+ inkscape:window-width="910"
+ inkscape:current-layer="layer1"
+ inkscape:document-units="px"
+ inkscape:cy="836.68762"
+ inkscape:cx="408.57141"
+ inkscape:zoom="0.67765957"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base" />
+ <metadata
+ id="metadata4">
+ <rdf:RDF
+ id="RDF5">
+ <cc:Work
+ id="Work6"
+ rdf:about="">
+ <dc:format
+ id="format7">image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage"
+ id="type9" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:groupmode="layer"
+ inkscape:label="Layer 1">
+ <g
+ id="g2065">
+ <text
+ sodipodi:linespacing="100%"
+ id="text1291"
+ y="143.78516"
+ x="36.981991"
+ style="font-size:78.161568;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1.0000000;stroke:none;stroke-width:1.0000000pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0000000;font-family:Attic;text-anchor:start;writing-mode:lr;line-height:100%"
+ xml:space="preserve"><tspan
+ y="143.78516"
+ x="36.981991"
+ id="tspan1293"
+ sodipodi:role="line">Bugs Everwhere</tspan></text>
+ <path
+ id="path1299"
+ d="M 435.39129,169.91317 C 435.39129,178.31858 440.70441,153.65014 445.24931,146.30250 C 450.43734,137.91513 453.18058,163.90397 453.46434,166.96184 C 453.55532,167.94222 453.46434,168.92939 453.46434,169.91317"
+ style="fill:none;fill-opacity:0.75000000;fill-rule:evenodd;stroke:#000000;stroke-width:4.2500000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1.0000000;stroke-dasharray:none;stroke-miterlimit:4.0000000" />
+ <path
+ id="path1303"
+ d="M 440.06423,89.055564 C 440.06423,95.774126 450.18051,79.127074 454.66879,73.752225 C 458.94498,68.631300 461.04359,61.697359 464.40532,56.262692 C 469.05687,48.742525 470.05816,49.640806 479.00985,46.424830"
+ style="stroke-dasharray:none;stroke-opacity:1.0000000;stroke-miterlimit:4.0000000;stroke-linejoin:round;stroke-linecap:round;stroke-width:4.2500000;stroke:#000000;fill-rule:evenodd;fill-opacity:0.75000000;fill:none" />
+ <path
+ id="path1305"
+ d="M 457.10286,68.286745 C 463.68121,65.923450 451.21494,56.705449 448.58360,50.797213"
+ style="stroke-dasharray:none;stroke-opacity:1.0000000;stroke-miterlimit:4.0000000;stroke-linejoin:round;stroke-linecap:round;stroke-width:4.2500000;stroke:#000000;fill-rule:evenodd;fill-opacity:0.75000000;fill:none" />
+ </g>
+ </g>
+</svg>
diff --git a/misc/xml/be-mail-to-xml b/misc/xml/be-mail-to-xml
new file mode 100755
index 0000000..5a1a88f
--- /dev/null
+++ b/misc/xml/be-mail-to-xml
@@ -0,0 +1,175 @@
+#!/usr/bin/env 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.
+"""
+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.
+"""
+
+import base64
+import email.utils
+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
+
+BREAK = u'--' # signature separator
+DEFAULT_ENCODING = get_output_encoding()
+
+KNOWN_IDS = []
+
+def normalize_email_address(address):
+ """
+ Standardize whitespace, etc.
+ """
+ addr = email.utils.formataddr(email.utils.parseaddr(address))
+ if len(addr) == 0:
+ return None
+ return addr
+
+def normalize_RFC_2822_date(date):
+ """
+ Some email clients write non-RFC 2822-compliant date tags like:
+ Fri, 18 Sep 2009 08:49:02 -0400 (EDT)
+ with the non-standard (EDT) timezone name. This funtion attempts
+ to deal with such inconsistencies.
+ """
+ time_tuple = email.utils.parsedate(date)
+ assert time_tuple != None, \
+ 'unparsable date: "%s"' % date
+ return time_to_str(mktime(time_tuple))
+
+def 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 = {}
+ new_fields = {}
+ new_fields[u'alt-id'] = message[u'message-id']
+ new_fields[u'in-reply-to'] = message[u'in-reply-to']
+ new_fields[u'author'] = normalize_email_address(message[u'from'])
+ new_fields[u'date'] = message[u'date']
+ if new_fields[u'date'] != None:
+ new_fields[u'date'] = normalize_RFC_2822_date(new_fields[u'date'])
+ new_fields[u'content-type'] = message.get_content_type()
+ for k,v in new_fields.items():
+ if v != None and type(v) != types.UnicodeType:
+ fields[k] = unicode(v, encoding=DEFAULT_ENCODING)
+ elif v == None and k in fields:
+ new_fields[k] = fields[k]
+ for k,v in fields.items():
+ if k not in new_fields:
+ new_fields.k = fields[k]
+ fields = new_fields
+
+ if fields[u'in-reply-to'] == None:
+ if message[u'references'] != None:
+ refs = message[u'references'].split()
+ for ref in refs: # search for a known reference id.
+ if ref in KNOWN_IDS:
+ fields[u'in-reply-to'] = ref
+ break
+ if fields[u'in-reply-to'] == None and len(refs) > 0:
+ fields[u'in-reply-to'] = refs[0] # default to the first
+ else: # check for mutliple in-reply-to references.
+ refs = fields[u'in-reply-to'].split()
+ found_ref = False
+ for ref in refs: # search for a known reference id.
+ if ref in KNOWN_IDS:
+ fields[u'in-reply-to'] = ref
+ found_ref = True
+ break
+ if found_ref == False and len(refs) > 0:
+ fields[u'in-reply-to'] = refs[0] # default to the first
+
+ if fields[u'alt-id'] != None:
+ KNOWN_IDS.append(fields[u'alt-id'])
+
+ if message.is_multipart():
+ ret = []
+ alt_id = fields[u'alt-id']
+ from_str = fields[u'author']
+ date = fields[u'date']
+ for m in message.walk():
+ if m == message:
+ continue
+ fields[u'author'] = from_str
+ fields[u'date'] = date
+ if len(ret) > 0: # we've added one part already
+ fields.pop(u'alt-id') # don't pass alt-id to other parts
+ fields[u'in-reply-to'] = alt_id # others respond to first
+ ret.append(comment_message_to_xml(m, fields))
+ return u'\n'.join(ret)
+
+ charset = message.get_content_charset(DEFAULT_ENCODING).lower()
+ #assert charset == DEFAULT_ENCODING.lower(), \
+ # u"Unknown charset: %s" % charset
+
+ if message[u'content-transfer-encoding'] == None:
+ encoding = DEFAULT_ENCODING
+ else:
+ encoding = message[u'content-transfer-encoding'].lower()
+ body = message.get_payload(decode=True) # attempt to decode
+ assert body != None, "Unable to decode?"
+ if fields[u'content-type'].startswith(u"text/"):
+ body = strip_footer(unicode(body, encoding=charset))
+ else:
+ body = base64.encode(body)
+ fields[u'body'] = body
+ lines = [u"<comment>"]
+ for tag,body in fields.items():
+ if body != None:
+ ebody = escape(body)
+ lines.append(u" <%s>%s</%s>" % (tag, ebody, tag))
+ lines.append(u"</comment>")
+ return u'\n'.join(lines)
+
+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"<be-xml>"
+ for message in mb:
+ print comment_message_to_xml(message)
+ print u"</be-xml>"
+
+
+if __name__ == "__main__":
+ import sys
+ import codecs
+
+ sys.stdout = codecs.getwriter(DEFAULT_ENCODING)(sys.stdout)
+ main(sys.argv)
diff --git a/misc/xml/be-xml-to-mbox b/misc/xml/be-xml-to-mbox
new file mode 100755
index 0000000..c8b7479
--- /dev/null
+++ b/misc/xml/be-xml-to-mbox
@@ -0,0 +1,220 @@
+#!/usr/bin/env 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.
+"""
+Convert xml output of `be list --xml` into mbox format for browsing
+with a mail reader. For example
+ $ be list --xml --status=all | be-xml-to-mbox | catmutt
+
+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.
+"""
+
+#from mailbox import mbox, Message # the mailbox people really want an on-disk copy
+import email.utils
+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
+ from xml.etree import ElementTree
+except ImportError: # look for non-core module
+ from elementtree import ElementTree
+from xml.sax.saxutils import unescape
+
+
+DEFAULT_DOMAIN = "invalid.com"
+DEFAULT_EMAIL = "dummy@" + DEFAULT_DOMAIN
+DEFAULT_ENCODING = get_output_encoding()
+
+def rfc2822_to_asctime(rfc2822_string):
+ """Convert an RFC 2822-fomatted string into a asctime string.
+ >>> rfc2822_to_asctime("Thu, 01 Jan 1970 00:00:00 +0000")
+ "Thu Jan 01 00:00:00 1970"
+ """
+ if rfc2822_string == "":
+ return asctime(gmtime(0))
+ return asctime(gmtime(rfc2822_to_gmtime_integer(rfc2822_string)))
+
+class LimitedAttrDict (dict):
+ """
+ Dict with error checking, to avoid invalid bug/comment fields.
+ """
+ _attrs = [] # override with list of valid attribute names
+ def __init__(self, **kwargs):
+ dict.__init__(self)
+ for key,value in kwargs.items():
+ self[key] = value
+ def __setitem__(self, key, item):
+ self._validate_key(key)
+ dict.__setitem__(self, key, item)
+ def _validate_key(self, key):
+ if key in self._attrs:
+ return
+ elif type(key) not in types.StringTypes:
+ raise TypeError, "Invalid attribute type %s for '%s'" % (type(key), key)
+ else:
+ raise ValueError, "Invalid attribute name '%s'" % key
+
+class Bug (LimitedAttrDict):
+ _attrs = [u"uuid",
+ u"short-name",
+ u"severity",
+ u"status",
+ u"assigned",
+ u"reporter",
+ u"creator",
+ u"created",
+ u"summary",
+ u"comments",
+ u"extra-strings"]
+ def print_to_mbox(self):
+ 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)
+ def init_from_etree(self, element):
+ assert element.tag == "bug", element.tag
+ for field in element.getchildren():
+ text = unescape(unicode(field.text).decode("unicode_escape").strip())
+ if field.tag == "comment":
+ comm = Comment()
+ comm.init_from_etree(field)
+ if "comments" in self:
+ self["comments"].append(comm)
+ else:
+ self["comments"] = [comm]
+ elif field.tag == "extra-string":
+ if "extra-strings" in self:
+ self["extra-strings"].append(text)
+ else:
+ self["extra-strings"] = [text]
+ else:
+ self[field.tag] = text
+
+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",
+ u"short-name",
+ u"in-reply-to",
+ u"author",
+ u"date",
+ u"content-type",
+ u"body",
+ u"extra-strings"]
+ def print_to_mbox(self, bug=None):
+ if bug == None:
+ bug = Bug()
+ bug[u"uuid"] = u"no-uuid"
+ name,addr = email.utils.parseaddr(self["author"])
+ print "From %s %s" % (addr, rfc2822_to_asctime(self["date"]))
+ if "uuid" in self: id = self["uuid"]
+ elif "alt-id" in self: id = self["alt-id"]
+ else: id = None
+ if id != None:
+ 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 = ""
+ if "short-name" in self:
+ subject += self["short-name"]+u": "
+ if "summary" in bug:
+ subject += bug["summary"]
+ else:
+ subject += u"no-subject"
+ print "Subject: %s" % subject
+ if "in-reply-to" not in self.keys():
+ self["in-reply-to"] = bug["uuid"]
+ print "In-Reply-To: %s" % 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)
+ 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 == "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":
+ b = Bug()
+ b.init_from_etree(element)
+ b.print_to_mbox()
+ elif element.tag == "comment":
+ c = Comment()
+ c.init_from_etree(element)
+ c.print_to_mbox()
+ elif element.tag in ["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()
+ else:
+ xml_unicode = codecs.open(sys.argv[1], "r", DEFAULT_ENCODING).read()
+ xml_str = xml_unicode.encode("unicode_escape").replace(r"\n", "\n")
+ elist = ElementTree.XML(xml_str)
+ print_to_mbox(elist)
diff --git a/misc/xml/catmutt b/misc/xml/catmutt
new file mode 100755
index 0000000..601f14f
--- /dev/null
+++ b/misc/xml/catmutt
@@ -0,0 +1,59 @@
+#!/bin/sh
+
+# catmutt - wrap mutt allowing mboxes read from stdin.
+#
+# Copyright (C) 1998-1999 Moritz Barsnick <barsnick (at) gmx (dot) net>,
+# 2009 William Trevor King <wking (at) drexel (dot) edu>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# developed from grepm-0.6
+# http://www.barsnick.net/sw/grepm.html
+
+PROGNAME=`basename "$0"`
+export TMPDIR="${TMPDIR-/tmp}" # used by mktemp
+umask 077
+
+if [ $# -gt 0 ] && [ "$1" = "--help" ]; then
+ echo 1>&2 "Usage: ${PROGNAME} [--help] mutt-arguments"
+ echo 1>&2 ""
+ echo 1>&2 "Read a mailbox file from stdin and opens it with mutt."
+ echo 1>&2 "For example: cat somefile.mbox | ${PROGNAME}"
+ exit 0
+fi
+
+# Note: the -t/-p options to mktemp are deprecated for mktemp (GNU
+# coreutils) 7.1 in favor of --tmpdir but the --tmpdir option does not
+# exist yet for my 6.10-3ubuntu2 coreutils
+TMPFILE=`mktemp -t catmutt.XXXXXX` || exit 1
+
+trap "rm -f ${TMPFILE}; exit 1" 1 2 3 13 15
+
+cat > "${TMPFILE}" || exit 1
+
+# Now that we've read in the mailbox file, reopen stdin for mutt/user
+# interaction. When in a pipe we're not technically in a tty, so use
+# a little hack from "greno" at
+# http://www.linuxforums.org/forum/linux-programming-scripting/98607-bash-stdin-problem.html
+tty="/dev/`ps -p$$ --no-heading | awk '{print $2}'`"
+exec < ${tty}
+
+if [ `wc -c "${TMPFILE}" | awk '{print $1}'` -gt 0 ]; then
+ echo 1>&2 "Calling mutt on temporary mailbox file (${TMPFILE})."
+ mutt -R -f "${TMPFILE}" "$@"
+else
+ echo 1>&2 "Empty mailbox input."
+fi
+
+rm -f "${TMPFILE}" && echo 1>&2 "Deleted temporary mailbox file (${TMPFILE})."
diff --git a/release.py b/release.py
new file mode 100755
index 0000000..d038cce
--- /dev/null
+++ b/release.py
@@ -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)
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..ab0a608
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+
+from distutils.core import setup
+from libbe import _version
+
+rev_id = _version.version_info["revision_id"]
+rev_date = rev_id.split("-")[1]
+
+setup(
+ name='Bugs Everywhere',
+ version=rev_date,
+ description='Bugtracker supporting distributed revision control',
+ url='http://bugseverywhere.org/',
+ 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/man/be.1']),
+ ]
+ )
diff --git a/test.py b/test.py
new file mode 100644
index 0000000..e6e5c2c
--- /dev/null
+++ b/test.py
@@ -0,0 +1,122 @@
+# 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.
+
+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
+
+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
+ 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
index 0000000..40db42a
--- /dev/null
+++ b/test_upgrade.py
@@ -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
diff --git a/test_usage.sh b/test_usage.sh
new file mode 100755
index 0000000..9b7dafe
--- /dev/null
+++ b/test_usage.sh
@@ -0,0 +1,154 @@
+#!/bin/bash
+#
+# Run through some simple usage cases. This both tests that important
+# features work, and gives an example of suggested usage to get people
+# started.
+#
+# 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
+# one in your working tree.
+
+set -e # exit immediately on failed command
+set -o pipefail # pipes fail if any stage fails
+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
+
+if [ $# -gt 1 ]
+then
+ echo "usage: test_usage.sh [VCS]"
+ echo ""
+ echo "where VCS is one of"
+ for VCS in arch bzr darcs git hg none
+ do
+ echo " $VCS"
+ done
+ exit 1
+elif [ $# -eq 0 ]
+then
+ for VCS in arch bzr darcs git hg none
+ do
+ echo -e "\n\nTesting $VCS\n\n"
+ $0 "$VCS" || exit 1
+ done
+ exit 0
+fi
+
+VCS="$1"
+
+TESTDIR=`mktemp -d /tmp/BEtest.XXXXXXXXXX`
+cd $TESTDIR
+
+# Initialize the VCS repository
+if [ "$VCS" == "arch" ]
+then
+ ID=`tla my-id`
+ ARCH_PARAM_DIR="$HOME/.arch-params"
+ ARCH_ARCHIVE_ROOT=`mktemp -d /tmp/BEtest.XXXXXXXXXX`
+ UNIQUE=`echo "$ARCH_ARCHIVE_ROOT" | sed 's/\/tmp\/BEtest.//;s/[0-9]//g'`
+ ARCH_ARCHIVE="j@x.com--BE-test-usage-$UNIQUE"
+ ARCH_PROJECT="BE-test-usage--twig--99.5"
+ ARCH_ARCHIVE_DIR="$ARCH_ARCHIVE_ROOT/$ARCH_PROJECT"
+ echo "tla make-archive $ARCH_ARCHIVE $ARCH_ARCHIVE_DIR"
+ tla make-archive $ARCH_ARCHIVE $ARCH_ARCHIVE_DIR
+ echo "tla archive-setup -A $ARCH_ARCHIVE $ARCH_PROJECT"
+ tla archive-setup -A $ARCH_ARCHIVE $ARCH_PROJECT
+ echo "tla init-tree -A $ARCH_ARCHIVE $ARCH_PROJECT"
+ tla init-tree -A $ARCH_ARCHIVE $ARCH_PROJECT
+ echo "Adjusing the naming conventions to allow .files"
+ 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 [ "$VCS" == "bzr" ]
+then
+ ID=`bzr whoami`
+ bzr init
+elif [ "$VCS" == "darcs" ]
+then
+ if [ -z "$DARCS_EMAIL" ]; then
+ export DARCS_EMAIL="J. Doe <jdoe@example.com>"
+ fi
+ ID="$DARCS_EMAIL"
+ darcs init
+elif [ "$VCS" == "git" ]
+then
+ NAME=`git config user.name`
+ EMAIL=`git config user.email`
+ ID="$NAME <$EMAIL>"
+ git init
+elif [ "$VCS" == "hg" ]
+then
+ ID=`hg showconfig ui.username`
+ hg init
+elif [ "$VCS" == "none" ]
+then
+ ID=`id -nu`
+else
+ echo "Unrecognized VCS '$VCS'"
+ exit 1
+fi
+
+if [ -z "$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 # 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 VCS
+be set # show settings
+be comment $BUG/ "No it isn't" # comment on the first comment
+be show $BUG # show details on a given bug
+be status closed $BUG # set bug status to 'closed'
+be comment $BUG "It's closed, but I can still comment."
+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 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 --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 --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 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"
+be commit "But this will fail" || echo "Failed"
+
+cd /
+rm -rf $TESTDIR
+
+if [ "$VCS" == "arch" ]
+then
+ # Cleanup everything outside of TESTDIR
+ rm -rf "$ARCH_ARCHIVE_ROOT"
+ rm -rf "$ARCH_PARAM_DIR/=locations/$ARCH_ARCHIVE"
+fi
+
+exec 2>&6 6>&- # restore stderr and close fd 6
diff --git a/update_copyright.py b/update_copyright.py
new file mode 100755
index 0000000..2490ba9
--- /dev/null
+++ b/update_copyright.py
@@ -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)