aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body1
-rw-r--r--.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values21
-rw-r--r--.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body7
-rw-r--r--.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values28
-rw-r--r--.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body5
-rw-r--r--.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values8
-rw-r--r--.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values31
-rw-r--r--.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body1
-rw-r--r--.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values8
-rw-r--r--.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values25
-rw-r--r--.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body1
-rw-r--r--.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values8
-rw-r--r--.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values31
-rw-r--r--.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body1
-rw-r--r--.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values21
-rw-r--r--.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body1
-rw-r--r--.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values28
-rw-r--r--.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values2
-rw-r--r--.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body1
-rw-r--r--.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values8
-rw-r--r--.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body1
-rw-r--r--.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values8
-rw-r--r--.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values31
-rw-r--r--.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body1
-rw-r--r--.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values21
-rw-r--r--.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values2
-rw-r--r--.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body1
-rw-r--r--.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values21
-rw-r--r--.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body3
-rw-r--r--.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values8
-rw-r--r--.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values25
-rw-r--r--.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body1
-rw-r--r--.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values21
-rw-r--r--.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body15
-rw-r--r--.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values21
-rw-r--r--.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values35
-rw-r--r--.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body3
-rw-r--r--.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values8
-rw-r--r--.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values19
-rw-r--r--.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values31
-rw-r--r--.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body1
-rw-r--r--.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values21
-rw-r--r--.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body17
-rw-r--r--.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values28
-rw-r--r--.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body1
-rw-r--r--.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values28
-rw-r--r--.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body1
-rw-r--r--.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values8
-rw-r--r--.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body1
-rw-r--r--.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values11
-rw-r--r--.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body1
-rw-r--r--.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values8
-rw-r--r--.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values25
-rw-r--r--.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body1
-rw-r--r--.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values21
-rw-r--r--.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values2
-rw-r--r--.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body1
-rw-r--r--.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values21
-rw-r--r--.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values2
-rw-r--r--.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values19
-rw-r--r--.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values32
-rw-r--r--.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values2
-rw-r--r--.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body1
-rw-r--r--.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values8
-rw-r--r--.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values31
-rw-r--r--.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body5
-rw-r--r--.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values21
-rw-r--r--.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body14
-rw-r--r--.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values21
-rw-r--r--.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body9
-rw-r--r--.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values21
-rw-r--r--.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values35
-rw-r--r--.be/settings18
-rw-r--r--README.dev41
-rwxr-xr-xbe12
-rw-r--r--becommands/assign.py25
-rw-r--r--becommands/close.py16
-rw-r--r--becommands/comment.py74
-rw-r--r--becommands/diff.py49
-rw-r--r--becommands/help.py29
-rw-r--r--becommands/list.py88
-rw-r--r--becommands/merge.py164
-rw-r--r--becommands/new.py36
-rw-r--r--becommands/open.py16
-rw-r--r--becommands/remove.py13
-rw-r--r--becommands/set.py64
-rw-r--r--becommands/set_root.py23
-rw-r--r--becommands/severity.py70
-rw-r--r--becommands/show.py29
-rw-r--r--becommands/status.py74
-rw-r--r--becommands/target.py32
-rw-r--r--libbe/arch.py26
-rw-r--r--libbe/bug.py357
-rw-r--r--libbe/bugdir.py375
-rw-r--r--libbe/bzr.py2
-rw-r--r--libbe/cmdutil.py84
-rw-r--r--libbe/comment.py295
-rw-r--r--libbe/config.py28
-rw-r--r--libbe/diff.py39
-rw-r--r--libbe/editor.py103
-rw-r--r--libbe/encoding.py53
-rw-r--r--libbe/git.py4
-rw-r--r--libbe/hg.py4
-rw-r--r--libbe/mapfile.py65
-rw-r--r--libbe/properties.py479
-rw-r--r--libbe/rcs.py37
-rw-r--r--libbe/restconvert.py130
-rw-r--r--libbe/settings_object.py353
-rw-r--r--libbe/tree.py5
-rw-r--r--libbe/utility.py73
-rw-r--r--test.py9
-rwxr-xr-xtest_usage.sh10
112 files changed, 3225 insertions, 1110 deletions
diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body
new file mode 100644
index 0000000..3b5e0e7
--- /dev/null
+++ b/.be/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/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values
new file mode 100644
index 0000000..5ed19bf
--- /dev/null
+++ b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Thu, 04 Dec 2008 13:35:41 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body
new file mode 100644
index 0000000..9106d37
--- /dev/null
+++ b/.be/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/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values
new file mode 100644
index 0000000..2a1c84d
--- /dev/null
+++ b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values
@@ -0,0 +1,28 @@
+
+
+
+Content-type=text/restructured
+
+
+
+
+
+
+Date=Thu, 06 Apr 2006 16:54:57 +0000
+
+
+
+
+
+
+From=abentley
+
+
+
+
+
+
+In-reply-to=144c238c-75d1-40f1-82c1-647668bcf2bc
+
+
+
diff --git a/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body
new file mode 100644
index 0000000..6f00ded
--- /dev/null
+++ b/.be/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/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values
new file mode 100644
index 0000000..dae549f
--- /dev/null
+++ b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values
@@ -0,0 +1,8 @@
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:16:11 +0000
+
+
+From: W. Trevor King <wking@drexel.edu>
+
diff --git a/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values
index 3a5c80c..400c6de 100644
--- a/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values
+++ b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values
@@ -1,35 +1,14 @@
+creator: abentley
+severity: minor
-creator=abentley
+status: closed
+summary: Arbitrary numerical priorities?
-
-severity=minor
-
-
-
-
-
-
-status=open
-
-
-
-
-
-
-summary=Arbitrary numerical priorities?
-
-
-
-
-
-
-time=Wed, 04 Jan 2006 21:09:30 +0000
-
-
+time: Wed, 04 Jan 2006 21:09:30 +0000
diff --git a/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body
new file mode 100644
index 0000000..dd40bfa
--- /dev/null
+++ b/.be/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/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values
new file mode 100644
index 0000000..ad389a7
--- /dev/null
+++ b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values
@@ -0,0 +1,8 @@
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:21:08 +0000
+
+
+From: W. Trevor King <wking@drexel.edu>
+
diff --git a/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values
index 402cd43..1d358cd 100644
--- a/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values
+++ b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values
@@ -1,28 +1,11 @@
+creator: abentley
+severity: minor
-creator=abentley
+status: closed
-
-
-
-severity=minor
-
-
-
-
-
-
-status=open
-
-
-
-
-
-
-summary=implement message-change log
-
-
+summary: implement message-change log
diff --git a/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body
new file mode 100644
index 0000000..708159c
--- /dev/null
+++ b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body
@@ -0,0 +1 @@
+Implemented
diff --git a/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values
new file mode 100644
index 0000000..6e9546e
--- /dev/null
+++ b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values
@@ -0,0 +1,8 @@
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:40:08 +0000
+
+
+From: W. Trevor King <wking@drexel.edu>
+
diff --git a/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values
index 9485ae7..8704a7e 100644
--- a/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values
+++ b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values
@@ -1,35 +1,14 @@
+assigned: abentley
+creator: abentley
-assigned=abentley
+severity: minor
+status: fixed
-
-creator=abentley
-
-
-
-
-
-
-severity=minor
-
-
-
-
-
-
-status=open
-
-
-
-
-
-
-summary=Per-tree configuration: default-assigneed?
-
-
+summary: 'Per-tree configuration: default-assigneed?'
diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body
new file mode 100644
index 0000000..5dde31f
--- /dev/null
+++ b/.be/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/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values
new file mode 100644
index 0000000..1f7615e
--- /dev/null
+++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Thu, 04 Dec 2008 13:44:33 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body
new file mode 100644
index 0000000..f03ef32
--- /dev/null
+++ b/.be/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/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values
new file mode 100644
index 0000000..e939438
--- /dev/null
+++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values
@@ -0,0 +1,28 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Thu, 04 Dec 2008 13:46:32 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
+
+
+
+In-reply-to=9e33512e-e3cb-42ec-bc99-8e77587d0d3f
+
+
+
diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values
index d17ea97..dc0b6b0 100644
--- a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values
+++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values
@@ -15,7 +15,7 @@ severity=minor
-status=closed
+status=fixed
diff --git a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body
new file mode 100644
index 0000000..ab2dc28
--- /dev/null
+++ b/.be/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/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values
new file mode 100644
index 0000000..667dc94
--- /dev/null
+++ b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values
@@ -0,0 +1,8 @@
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:05:50 +0000
+
+
+From: W. Trevor King <wking@drexel.edu>
+
diff --git a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body
new file mode 100644
index 0000000..d0b8404
--- /dev/null
+++ b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body
@@ -0,0 +1 @@
+Implemented.
diff --git a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values
new file mode 100644
index 0000000..225f59e
--- /dev/null
+++ b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values
@@ -0,0 +1,8 @@
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 15:42:07 +0000
+
+
+From: W. Trevor King <wking@drexel.edu>
+
diff --git a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values
index 5d081cf..37197a7 100644
--- a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values
+++ b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values
@@ -1,35 +1,14 @@
+creator: abentley
+severity: minor
-creator=abentley
+status: closed
+summary: Do we need a severity between serious and minor? EG "Moderate"?
-
-severity=serious
-
-
-
-
-
-
-status=open
-
-
-
-
-
-
-summary=Do we need a severity between serious and minor? EG "Moderate"?
-
-
-
-
-
-
-time=Wed, 25 Jan 2006 23:14:07 +0000
-
-
+time: Wed, 25 Jan 2006 23:14:07 +0000
diff --git a/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body
new file mode 100644
index 0000000..fb08206
--- /dev/null
+++ b/.be/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/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values
new file mode 100644
index 0000000..1f7615e
--- /dev/null
+++ b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Thu, 04 Dec 2008 13:44:33 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values
index 89ec800..85cf442 100644
--- a/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values
+++ b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values
@@ -15,7 +15,7 @@ severity=minor
-status=closed
+status=fixed
diff --git a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body
new file mode 100644
index 0000000..bd80264
--- /dev/null
+++ b/.be/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/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values
new file mode 100644
index 0000000..d7e4f49
--- /dev/null
+++ b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Thu, 04 Dec 2008 13:35:42 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body
new file mode 100644
index 0000000..dd464bf
--- /dev/null
+++ b/.be/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/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values
new file mode 100644
index 0000000..84da235
--- /dev/null
+++ b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values
@@ -0,0 +1,8 @@
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:29:30 +0000
+
+
+From: W. Trevor King <wking@drexel.edu>
+
diff --git a/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values
index a9e974e..b8e8291 100644
--- a/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values
+++ b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values
@@ -1,28 +1,11 @@
+creator: abentley
+severity: wishlist
-creator=abentley
+status: closed
-
-
-
-severity=wishlist
-
-
-
-
-
-
-status=open
-
-
-
-
-
-
-summary=Support per-tree settings for severity, target, BE ids
-
-
+summary: Support per-tree settings for severity, target, BE ids
diff --git a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body
new file mode 100644
index 0000000..8d1ec26
--- /dev/null
+++ b/.be/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/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values
new file mode 100644
index 0000000..39df7ff
--- /dev/null
+++ b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Thu, 27 Nov 2008 14:26:18 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body
new file mode 100644
index 0000000..bb443b8
--- /dev/null
+++ b/.be/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/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values
new file mode 100644
index 0000000..ea73789
--- /dev/null
+++ b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Thu, 27 Nov 2008 13:43:47 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values
new file mode 100644
index 0000000..f88ca6c
--- /dev/null
+++ b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values
@@ -0,0 +1,35 @@
+
+
+
+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/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body
new file mode 100644
index 0000000..dd9b459
--- /dev/null
+++ b/.be/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/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values
new file mode 100644
index 0000000..8270e8e
--- /dev/null
+++ b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values
@@ -0,0 +1,8 @@
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 18:05:38 +0000
+
+
+From: W. Trevor King <wking@drexel.edu>
+
diff --git a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values
index 27ec173..eb5d3c0 100644
--- a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values
+++ b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values
@@ -1,21 +1,8 @@
+Content-type: text/plain
-
-Content-type=text/plain
-
-
-
-
-
-
-Date=Fri, 31 Mar 2006 22:15:09 +0000
-
-
-
-
-
-
-From=abentley
+Date: Fri, 31 Mar 2006 22:15:09 +0000
+From: abentley
diff --git a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values
index 800a5ce..5d35985 100644
--- a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values
+++ b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values
@@ -1,35 +1,14 @@
+creator: abentley
+severity: minor
-creator=abentley
+status: closed
+summary: Provide search
-
-severity=minor
-
-
-
-
-
-
-status=open
-
-
-
-
-
-
-summary=Provide search
-
-
-
-
-
-
-time=Wed, 25 Jan 2006 15:43:59 +0000
-
-
+time: Wed, 25 Jan 2006 15:43:59 +0000
diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body
new file mode 100644
index 0000000..073f0b8
--- /dev/null
+++ b/.be/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/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values
new file mode 100644
index 0000000..b0ecc8f
--- /dev/null
+++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Tue, 25 Nov 2008 02:24:04 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body
new file mode 100644
index 0000000..7f46872
--- /dev/null
+++ b/.be/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/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values
new file mode 100644
index 0000000..a93e649
--- /dev/null
+++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values
@@ -0,0 +1,28 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Sat, 22 Nov 2008 21:43:29 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
+
+
+
+In-reply-to=0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1
+
+
+
diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body
new file mode 100644
index 0000000..62c14e6
--- /dev/null
+++ b/.be/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/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values
new file mode 100644
index 0000000..35b6806
--- /dev/null
+++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values
@@ -0,0 +1,28 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Sun, 23 Nov 2008 12:37:57 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
+
+
+
+In-reply-to=0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1
+
+
+
diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body
new file mode 100644
index 0000000..6c46db0
--- /dev/null
+++ b/.be/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/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values
new file mode 100644
index 0000000..afd88e5
--- /dev/null
+++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values
@@ -0,0 +1,8 @@
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:05:49 +0000
+
+
+From: W. Trevor King <wking@drexel.edu>
+
diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body
new file mode 100644
index 0000000..d0b8404
--- /dev/null
+++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body
@@ -0,0 +1 @@
+Implemented.
diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values
new file mode 100644
index 0000000..366395d
--- /dev/null
+++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values
@@ -0,0 +1,11 @@
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 15:42:07 +0000
+
+
+From: W. Trevor King <wking@drexel.edu>
+
+
+In-reply-to: 2628eeca-96c6-4933-8484-d55bb1dbf985
+
diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body
new file mode 100644
index 0000000..f7659c3
--- /dev/null
+++ b/.be/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/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values
new file mode 100644
index 0000000..80e328b
--- /dev/null
+++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values
@@ -0,0 +1,8 @@
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:07:25 +0000
+
+
+From: W. Trevor King <wking@drexel.edu>
+
diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values
index 8a5d9e2..2f65fbc 100644
--- a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values
+++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values
@@ -1,28 +1,11 @@
+creator: abentley
+severity: minor
-creator=abentley
+status: fixed
-
-
-
-severity=minor
-
-
-
-
-
-
-status=open
-
-
-
-
-
-
-summary=Customizable severity levels?
-
-
+summary: Customizable severity levels?
diff --git a/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body
new file mode 100644
index 0000000..dd40bfa
--- /dev/null
+++ b/.be/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/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values
new file mode 100644
index 0000000..1ff89fa
--- /dev/null
+++ b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Thu, 04 Dec 2008 13:48:47 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values
index ebaf786..3c4e210 100644
--- a/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values
+++ b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values
@@ -15,7 +15,7 @@ severity=minor
-status=open
+status=closed
diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body
new file mode 100644
index 0000000..090895e
--- /dev/null
+++ b/.be/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/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values
new file mode 100644
index 0000000..8283996
--- /dev/null
+++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Tue, 25 Nov 2008 02:24:05 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values
index 5aed729..84a0a11 100644
--- a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values
+++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values
@@ -15,7 +15,7 @@ severity=minor
-status=fixed
+status=closed
diff --git a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values
index cb5a094..e964891 100644
--- a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values
+++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values
@@ -1,21 +1,8 @@
+Content-type: text/plain
-
-Content-type=text/plain
-
-
-
-
-
-
-Date=Sat, 15 Nov 2008 23:56:51 +0000
-
-
-
-
-
-
-From=wking
+Date: Sat, 15 Nov 2008 23:56:51 +0000
+From: wking
diff --git a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values
index d6d5870..29d76c7 100644
--- a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values
+++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values
@@ -1,35 +1,15 @@
+creator: abentley
+severity: critical
-creator=abentley
+status: closed
+summary: OK, maybe not fatal, but how about a new name that suggests process tracking,
+ not just bugs?
-
-severity=critical
-
-
-
-
-
-
-status=open
-
-
-
-
-
-
-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
-
-
+time: Fri, 27 Jan 2006 14:37:25 +0000
diff --git a/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values b/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values
index ac013c5..608b460 100644
--- a/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values
+++ b/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values
@@ -15,7 +15,7 @@ severity=minor
-status=open
+status=fixed
diff --git a/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body
new file mode 100644
index 0000000..dd40bfa
--- /dev/null
+++ b/.be/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/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values
new file mode 100644
index 0000000..7bf391a
--- /dev/null
+++ b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values
@@ -0,0 +1,8 @@
+Content-type: text/plain
+
+
+Date: Thu, 04 Dec 2008 17:20:20 +0000
+
+
+From: W. Trevor King <wking@drexel.edu>
+
diff --git a/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values
index 7305e89..dde51b9 100644
--- a/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values
+++ b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values
@@ -1,35 +1,14 @@
+creator: abentley
+severity: minor
-creator=abentley
+status: closed
+summary: Allow different sorts
-
-severity=minor
-
-
-
-
-
-
-status=open
-
-
-
-
-
-
-summary=Allow different sorts
-
-
-
-
-
-
-time=Wed, 25 Jan 2006 15:43:19 +0000
-
-
+time: Wed, 25 Jan 2006 15:43:19 +0000
diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body
new file mode 100644
index 0000000..02bbe3a
--- /dev/null
+++ b/.be/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/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values
new file mode 100644
index 0000000..eb56317
--- /dev/null
+++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Tue, 25 Nov 2008 19:41:02 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body
new file mode 100644
index 0000000..d97791d
--- /dev/null
+++ b/.be/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/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values
new file mode 100644
index 0000000..f976972
--- /dev/null
+++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Tue, 25 Nov 2008 02:36:16 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body
new file mode 100644
index 0000000..7bb09ff
--- /dev/null
+++ b/.be/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/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values
new file mode 100644
index 0000000..bf5085b
--- /dev/null
+++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Tue, 25 Nov 2008 03:02:59 +0000
+
+
+
+
+
+
+From=W. Trevor King <wking@drexel.edu>
+
+
+
diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values
new file mode 100644
index 0000000..e710d29
--- /dev/null
+++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values
@@ -0,0 +1,35 @@
+
+
+
+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/settings b/.be/settings
index 47dda13..a9bd6dd 100644
--- a/.be/settings
+++ b/.be/settings
@@ -1,7 +1,13 @@
-
-
-
-rcs_name=bzr
-
-
+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.
+
+
+rcs_name: bzr
diff --git a/README.dev b/README.dev
index bb39ba5..644d965 100644
--- a/README.dev
+++ b/README.dev
@@ -14,6 +14,10 @@ provide the following elements:
The entry function for your plugin. args is everything from
sys.argv after the name of your plugin (e.g. for the command
`be open abc', args=['abc']).
+
+ Note: be supports command-completion. To avoid raising errors you
+ need to deal with possible '--complete' options and arguments.
+ See the 'Command completion' section below for more information.
help()
Return the string to be output by `be help <yourplugin>',
`be <yourplugin> --help', etc.
@@ -26,3 +30,40 @@ consistent interface
alter the parser (e.g. add some more options) before returning it.
Again, you can just browse around in becommands to get a feel for things.
+
+Testing
+-------
+
+Run any doctests in your plugin with
+ be$ python test.py <yourplugin>
+for example
+ be$ python test.py merge
+
+
+Command completion
+------------------
+
+BE implements a general framework to make it easy to support command
+completion for arbitrary plugins. In order to support this system,
+all becommands should properly handle the '--complete' commandline
+argument, returning a list of possible completions. For example
+ $ be --commands
+ lists options accepted by be and the names of all available becommands.
+ $ be list --commands
+ lists options accepted by becommand/list
+ $ be list --status --commands
+ lists arguments accepted by the becommand/list --status option
+ $ be show -- --commands
+ lists possible vals for the first positional argument of becommand/show
+This is a lot of information, but command-line completion is really
+convenient for the user. See becommand/list.py and becommand/show.py
+for example implementations. The basic idea is to raise
+ cmdutil.GetCompletions(['list','of','possible','completions'])
+once you've determined what that list should be.
+
+However, command completion is not critical. The first priority is to
+implement the target functionality, with fancy shell sugar coming
+later. In recognition of this, cmdutil provides the default_complete
+function which ensures that if '--complete' is any one of the
+arguments, options, or option-arguments, GetCompletions will be raised
+with and empty list.
diff --git a/be b/be
index 1ef7b3a..35dab69 100755
--- a/be
+++ b/be
@@ -24,6 +24,10 @@ __doc__ == cmdutil.help()
if len(sys.argv) == 1 or sys.argv[1] in ('--help', '-h'):
print cmdutil.help()
+elif sys.argv[1] == '--complete':
+ for command, module in cmdutil.iter_commands():
+ print command
+ print '\n'.join(["--help","--complete","--options"])
else:
try:
try:
@@ -33,9 +37,15 @@ else:
except cmdutil.GetHelp:
print cmdutil.help(sys.argv[1])
sys.exit(0)
- except cmdutil.UsageError:
+ except cmdutil.GetCompletions, e:
+ print '\n'.join(e.completions)
+ sys.exit(0)
+ except cmdutil.UsageError, e:
+ print "Invalid usage:", e
+ print "\nArgs:", sys.argv[1:]
print cmdutil.help(sys.argv[1])
sys.exit(1)
except cmdutil.UserError, e:
+ print "ERROR:"
print e
sys.exit(1)
diff --git a/becommands/assign.py b/becommands/assign.py
index cb732b3..2f9ff21 100644
--- a/becommands/assign.py
+++ b/becommands/assign.py
@@ -15,40 +15,43 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Assign an individual or group to fix a bug"""
-from libbe import cmdutil, bugdir
+from libbe import cmdutil, bugdir, settings_object
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> import os
>>> bd = bugdir.simple_bug_dir()
>>> os.chdir(bd.root)
- >>> bd.bug_from_shortname("a").assigned is None
+ >>> bd.bug_from_shortname("a").assigned is settings_object.EMPTY
True
- >>> execute(["a"])
+ >>> execute(["a"], test=True)
>>> bd._clear_bugs()
>>> bd.bug_from_shortname("a").assigned == bd.user_id
True
- >>> execute(["a", "someone"])
+ >>> execute(["a", "someone"], test=True)
>>> bd._clear_bugs()
>>> print bd.bug_from_shortname("a").assigned
someone
- >>> execute(["a","none"])
+ >>> execute(["a","none"], test=True)
>>> bd._clear_bugs()
- >>> bd.bug_from_shortname("a").assigned is None
+ >>> bd.bug_from_shortname("a").assigned is settings_object.EMPTY
True
"""
- options, args = get_parser().parse_args(args)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ cmdutil.default_complete(options, args, parser,
+ bugid_args={0: lambda bug : bug.active==True})
assert(len(args) in (0, 1, 2))
if len(args) == 0:
- raise cmdutil.UserError("Please specify a bug id.")
+ raise cmdutil.UsageError("Please specify a bug id.")
if len(args) > 2:
help()
- raise cmdutil.UserError("Too many arguments.")
- bd = bugdir.BugDir(from_disk=True)
+ raise cmdutil.UsageError("Too many arguments.")
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
bug = bd.bug_from_shortname(args[0])
if len(args) == 1:
bug.assigned = bd.user_id
diff --git a/becommands/close.py b/becommands/close.py
index 8d2ccdb..d8826b0 100644
--- a/becommands/close.py
+++ b/becommands/close.py
@@ -18,7 +18,7 @@
from libbe import cmdutil, bugdir
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> from libbe import bugdir
>>> import os
@@ -26,18 +26,20 @@ def execute(args):
>>> os.chdir(bd.root)
>>> print bd.bug_from_shortname("a").status
open
- >>> execute(["a"])
+ >>> execute(["a"], test=True)
>>> bd._clear_bugs()
>>> print bd.bug_from_shortname("a").status
closed
"""
- options, args = get_parser().parse_args(args)
+ 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.UserError("Please specify a bug id.")
+ raise cmdutil.UsageError("Please specify a bug id.")
if len(args) > 1:
- help()
- raise cmdutil.UserError("Too many arguments.")
- bd = bugdir.BugDir(from_disk=True)
+ raise cmdutil.UsageError("Too many arguments.")
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
bug = bd.bug_from_shortname(args[0])
bug.status = "closed"
bd.save()
diff --git a/becommands/comment.py b/becommands/comment.py
index 172f818..b15a06e 100644
--- a/becommands/comment.py
+++ b/becommands/comment.py
@@ -15,19 +15,19 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Add a comment to a bug"""
-from libbe import cmdutil, bugdir, utility
+from libbe import cmdutil, bugdir, settings_object, editor
import os
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> import time
>>> bd = bugdir.simple_bug_dir()
>>> os.chdir(bd.root)
- >>> execute(["a", "This is a comment about a"])
+ >>> execute(["a", "This is a comment about a"], test=True)
>>> bd._clear_bugs()
>>> bug = bd.bug_from_shortname("a")
- >>> bug.load_comments()
+ >>> bug.load_comments(load_full=False)
>>> comment = bug.comment_root[0]
>>> print comment.body
This is a comment about a
@@ -36,31 +36,32 @@ def execute(args):
True
>>> comment.time <= int(time.time())
True
- >>> comment.in_reply_to is None
+ >>> comment.in_reply_to is settings_object.EMPTY
True
>>> if 'EDITOR' in os.environ:
... del os.environ["EDITOR"]
- >>> execute(["b"])
+ >>> execute(["b"], test=True)
Traceback (most recent call last):
UserError: No comment supplied, and EDITOR not specified.
>>> os.environ["EDITOR"] = "echo 'I like cheese' > "
- >>> execute(["b"])
+ >>> execute(["b"], test=True)
>>> bd._clear_bugs()
>>> bug = bd.bug_from_shortname("b")
- >>> bug.load_comments()
+ >>> bug.load_comments(load_full=False)
>>> comment = bug.comment_root[0]
>>> print comment.body
I like cheese
<BLANKLINE>
"""
- options, args = get_parser().parse_args(args)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ complete(options, args, parser)
if len(args) == 0:
- raise cmdutil.UserError("Please specify a bug or comment id.")
+ raise cmdutil.UsageError("Please specify a bug or comment id.")
if len(args) > 2:
- help()
- raise cmdutil.UserError("Too many arguments.")
+ raise cmdutil.UsageError("Too many arguments.")
shortname = args[0]
if shortname.count(':') > 1:
@@ -73,20 +74,20 @@ def execute(args):
bugname = shortname
is_reply = False
- bd = bugdir.BugDir(from_disk=True)
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
bug = bd.bug_from_shortname(bugname)
- bug.load_comments()
+ bug.load_comments(load_full=False)
if is_reply:
- parent = bug.comment_root.comment_from_shortname(shortname, bug_shortname=bugname)
+ parent = bug.comment_root.comment_from_shortname(shortname,
+ bug_shortname=bugname)
else:
parent = bug.comment_root
if len(args) == 1:
try:
- body = utility.editor_string("Please enter your comment above")
- except utility.CantFindEditor:
- raise cmdutil.UserError(
- "No comment supplied, and EDITOR not specified.")
+ body = editor.editor_string("Please enter your comment above")
+ except editor.CantFindEditor, e:
+ raise cmdutil.UserError, "No comment supplied, and EDITOR not specified."
if body is None:
raise cmdutil.UserError("No comment entered.")
body = body.decode('utf-8')
@@ -107,8 +108,41 @@ 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).
$EDITOR is used to launch an editor. If unspecified, no comment will be
-created.)
+created.
"""
def help():
return get_parser().help_str() + longhelp
+
+def complete(options, args, parser):
+ for option,value in cmdutil.option_value_pairs(options, parser):
+ if value == "--complete":
+ # no argument-options at the moment, so this is future-proofing
+ raise cmdutil.GetCompletions()
+ for pos,value in enumerate(args):
+ if value == "--complete":
+ if pos == 0: # fist positional argument is a bug or comment id
+ if len(args) >= 2:
+ partial = args[1].split(':')[0] # take only bugid portion
+ else:
+ partial = ""
+ ids = []
+ try:
+ bd = bugdir.BugDir(from_disk=True,
+ manipulate_encodings=False)
+ bugs = []
+ for uuid in bd.list_uuids():
+ if uuid.startswith(partial):
+ bug = bd.bug_from_uuid(uuid)
+ if bug.active == True:
+ bugs.append(bug)
+ for bug in bugs:
+ shortname = bd.bug_shortname(bug)
+ ids.append(shortname)
+ bug.load_comments(load_full=False)
+ for id,comment in bug.comment_shortnames(shortname):
+ ids.append(id)
+ except bugdir.NoBugDir:
+ pass
+ raise cmdutil.GetCompletions(ids)
+ raise cmdutil.GetCompletions()
diff --git a/becommands/diff.py b/becommands/diff.py
index 77194ff..c090fa8 100644
--- a/becommands/diff.py
+++ b/becommands/diff.py
@@ -20,7 +20,7 @@ from libbe import cmdutil, bugdir, diff
import os
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> import os
>>> bd = bugdir.simple_bug_dir()
@@ -31,7 +31,7 @@ def execute(args):
>>> changed = bd.rcs.commit("Closed bug a")
>>> os.chdir(bd.root)
>>> if bd.rcs.versioned == True:
- ... execute([original])
+ ... execute([original], test=True)
... else:
... print "a:cm: Bug A\\nstatus: open -> closed\\n"
Modified bug reports:
@@ -39,25 +39,52 @@ def execute(args):
status: open -> closed
<BLANKLINE>
"""
- options, args = get_parser().parse_args(args)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ cmdutil.default_complete(options, args, parser)
if len(args) == 0:
revision = None
if len(args) == 1:
revision = args[0]
if len(args) > 1:
- help()
- raise cmdutil.UserError("Too many arguments.")
- bd = bugdir.BugDir(from_disk=True)
+ raise cmdutil.UsageError("Too many arguments.")
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
if bd.rcs.versioned == False:
print "This directory is not revision-controlled."
else:
old_bd = bd.duplicate_bugdir(revision)
r,m,a = diff.diff(old_bd, bd)
- diff.diff_report((r,m,a), bd)
+
+ optbugs = []
+ if options.all == True:
+ options.new = options.modified = options.removed = True
+ if options.new == True:
+ optbugs.extend(a)
+ if options.modified == True:
+ optbugs.extend([new for old,new in m])
+ if options.removed == True:
+ optbugs.extend(r)
+ if len(optbugs) > 0:
+ for bug in optbugs:
+ print bug.uuid
+ else :
+ print diff.diff_report((r,m,a), bd).encode(bd.encoding)
bd.remove_duplicate_bugdir()
def get_parser():
- parser = cmdutil.CmdOptionParser("be diff [SPECIFIER]")
+ parser = cmdutil.CmdOptionParser("be diff [options] REVISION")
+ # boolean options
+ bools = (("n", "new", "Print UUIDS for new bugs"),
+ ("m", "modified", "Print UUIDS for modified bugs"),
+ ("r", "removed", "Print UUIDS for removed bugs"),
+ ("a", "all", "Print UUIDS for all changed bugs"))
+ for s in bools:
+ attr = s[1].replace('-','_')
+ short = "-%c" % s[0]
+ long = "--%s" % s[1]
+ help = s[2]
+ parser.add_option(short, long, action="store_true",
+ dest=attr, help=help)
return parser
longhelp="""
@@ -65,7 +92,11 @@ Uses the RCS to compare the current tree with a previous tree, and prints
a pretty report. If specifier is given, it is a specifier for the particular
previous tree to use. Specifiers are specific to their RCS.
-For Arch: a fully-qualified revision name
+For Arch your specifier must be a fully-qualified revision name.
+
+Besides the standard summary output, you can use the options to output
+UUIDS for the different categories. This output can be used as the
+input to 'be show' to get and understanding of the current status.
"""
def help():
diff --git a/becommands/help.py b/becommands/help.py
index bf0b4fc..7e0209d 100644
--- a/becommands/help.py
+++ b/becommands/help.py
@@ -21,20 +21,28 @@ __desc__ = __doc__
def execute(args):
"""
Print help of specified command.
+ >>> execute(["help"])
+ Usage: be help [COMMAND]
+ <BLANKLINE>
+ Options:
+ -h, --help Print a help message
+ --complete Print a list of available completions
+ <BLANKLINE>
+ Print help for specified command or list of all commands.
+ <BLANKLINE>
"""
- options, args = get_parser().parse_args(args)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ complete(options, args, parser)
if len(args) > 1:
- raise cmdutil.UserError("Too many arguments.")
+ raise cmdutil.UsageError("Too many arguments.")
if len(args) == 0:
print cmdutil.help()
else:
try:
print cmdutil.help(args[0])
except AttributeError:
- print "No help available"
-
- return
-
+ print "No help available"
def get_parser():
parser = cmdutil.CmdOptionParser("be help [COMMAND]")
@@ -46,3 +54,12 @@ Print help for specified command or list of all commands.
def help():
return get_parser().help_str() + longhelp
+
+def complete(options, args, parser):
+ for option, value in cmdutil.option_value_pairs(options, parser):
+ if value == "--complete":
+ # no argument-options at the moment, so this is future-proofing
+ raise cmdutil.GetCompletions()
+ if "--complete" in args:
+ cmds = [command for command,module in cmdutil.iter_commands()]
+ raise cmdutil.GetCompletions(cmds)
diff --git a/becommands/list.py b/becommands/list.py
index 63e1cd6..8c69eaa 100644
--- a/becommands/list.py
+++ b/becommands/list.py
@@ -15,39 +15,39 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""List bugs"""
-from libbe import cmdutil, bugdir
-from libbe.bug import cmp_full, severity_values, status_values, \
- active_status_values, inactive_status_values
+from libbe import cmdutil, bugdir, bug
import os
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> import os
>>> bd = bugdir.simple_bug_dir()
>>> os.chdir(bd.root)
- >>> execute([])
+ >>> execute([], test=True)
a:om: Bug A
- >>> execute(["--status", "all"])
+ >>> execute(["--status", "all"], test=True)
a:om: Bug A
b:cm: Bug B
"""
- options, args = get_parser().parse_args(args)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ complete(options, args, parser)
if len(args) > 0:
- help()
- raise cmdutil.UserError("Too many arguments.")
- bd = bugdir.BugDir(from_disk=True)
+ raise cmdutil.UsageError("Too many arguments.")
+
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
bd.load_all_bugs()
# select status
if options.status != None:
if options.status == "all":
- status = status_values
+ status = bug.status_values
else:
status = options.status.split(',')
else:
status = []
if options.active == True:
- status.extend(list(active_status_values))
+ status.extend(list(bug.active_status_values))
if options.unconfirmed == True:
status.append("unconfirmed")
if options.open == True:
@@ -55,11 +55,11 @@ def execute(args):
if options.test == True:
status.append("test")
if status == []: # set the default value
- status = active_status_values
+ status = bug.active_status_values
# select severity
if options.severity != None:
if options.severity == "all":
- severity = severity_values
+ severity = bug.severity_values
else:
severity = options.severity.split(',')
else:
@@ -67,10 +67,10 @@ def execute(args):
if options.wishlist == True:
severity.extend("wishlist")
if options.important == True:
- serious = severity_values.index("serious")
- severity.append(list(severity_values[serious:]))
+ serious = bug.severity_values.index("serious")
+ severity.append(list(bug.severity_values[serious:]))
if severity == []: # set the default value
- severity = severity_values
+ severity = bug.severity_values
# select assigned
if options.assigned != None:
if options.assigned == "all":
@@ -114,15 +114,18 @@ def execute(args):
if len(bugs) == 0:
print "No matching bugs found"
- def list_bugs(cur_bugs, title=None, no_target=False):
- cur_bugs.sort(cmp_full)
+ def list_bugs(cur_bugs, title=None, just_uuids=False):
+ cur_bugs.sort(bug.cmp_full)
if len(cur_bugs) > 0:
if title != None:
print cmdutil.underlined(title)
- for bug in cur_bugs:
- print bug.string(shortlist=True)
+ for bg in cur_bugs:
+ if just_uuids:
+ print bg.uuid
+ else:
+ print bg.string(shortlist=True)
- list_bugs(bugs, no_target=False)
+ list_bugs(bugs, just_uuids=options.uuids)
def get_parser():
parser = cmdutil.CmdOptionParser("be list [options]")
@@ -134,11 +137,12 @@ def get_parser():
help="List options matching ASSIGNED", default=None)
parser.add_option("-t", "--target", metavar="TARGET", dest="target",
help="List options matching TARGET", default=None)
- # boolean shortucts. All of these are special cases of long forms
- bools = (("w", "wishlist", "List bugs with 'wishlist' severity"),
+ # boolean options. All but uuids are special cases of long forms
+ bools = (("u", "uuids", "Only print the bug UUIDS"),
+ ("w", "wishlist", "List bugs with 'wishlist' severity"),
("i", "important", "List bugs with >= 'serious' severity"),
("A", "active", "List all active bugs"),
- ("u", "unconfirmed", "List unconfirmed bugs"),
+ ("U", "unconfirmed", "List unconfirmed bugs"),
("o", "open", "List open bugs"),
("T", "test", "List bugs in testing"),
("m", "mine", "List bugs assigned to you"),
@@ -152,9 +156,20 @@ def get_parser():
dest=attr, help=help)
return parser
-longhelp="""
-This command lists bugs. There are several criteria that you can
-search by:
+
+def help():
+ longhelp="""
+This command lists bugs. Normally it prints a short string like
+ 576:om: Allow attachments
+Where
+ 576 the bug id
+ o the bug status is 'open' (first letter)
+ m the bug severity is 'minor' (first letter)
+ Allo... the bug summary string
+
+You can optionally (-u) print only the bug ids.
+
+There are several criteria that you can filter by:
* status
* severity
* assigned (who the bug is assigned to)
@@ -174,8 +189,17 @@ target
In addition, there are some shortcut options that set boolean flags.
The boolean options are ignored if the matching string option is used.
-""" % (','.join(status_values),
- ','.join(severity_values))
-
-def help():
+""" % (','.join(bug.status_values),
+ ','.join(bug.severity_values))
return get_parser().help_str() + longhelp
+
+def complete(options, args, parser):
+ for option, value in cmdutil.option_value_pairs(options, parser):
+ if value == "--complete":
+ if option == "status":
+ raise cmdutil.GetCompletions(bug.status_values)
+ elif option == "severity":
+ raise cmdutil.GetCompletions(bug.severity_values)
+ raise cmdutil.GetCompletions()
+ if "--complete" in args:
+ raise cmdutil.GetCompletions() # no positional arguments for list
diff --git a/becommands/merge.py b/becommands/merge.py
new file mode 100644
index 0000000..927bb63
--- /dev/null
+++ b/becommands/merge.py
@@ -0,0 +1,164 @@
+# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
+# <abentley@panoramicfeedback.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You 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
+"""Merge duplicate bugs"""
+from libbe import cmdutil, bugdir
+import os, copy
+__desc__ = __doc__
+
+def execute(args, test=False):
+ """
+ >>> from libbe import utility
+ >>> bd = bugdir.simple_bug_dir()
+ >>> a = bd.bug_from_shortname("a")
+ >>> a.comment_root.time = 0
+ >>> dummy = a.new_comment("Testing")
+ >>> dummy.time = 1
+ >>> dummy = dummy.new_reply("Testing...")
+ >>> dummy.time = 2
+ >>> b = bd.bug_from_shortname("b")
+ >>> b.status = "open"
+ >>> b.comment_root.time = 0
+ >>> dummy = b.new_comment("1 2")
+ >>> dummy.time = 1
+ >>> dummy = dummy.new_reply("1 2 3 4")
+ >>> dummy.time = 2
+ >>> bd.save()
+ >>> os.chdir(bd.root)
+ >>> execute(["a", "b"], test=True)
+ Merging bugs a and b
+ >>> bd._clear_bugs()
+ >>> a = bd.bug_from_shortname("a")
+ >>> a.load_comments()
+ >>> mergeA = a.comment_from_shortname(":3")
+ >>> mergeA.time = 3
+ >>> print a.string(show_comments=True)
+ ID : a
+ Short name : a
+ Severity : minor
+ Status : open
+ Assigned :
+ Target :
+ Reporter :
+ Creator : John Doe <jdoe@example.com>
+ Created : Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000)
+ Bug A
+ --------- Comment ---------
+ Name: a:1
+ From: wking <wking@thor.yang.physics.drexel.edu>
+ Date: Thu, 01 Jan 1970 00:00:01 +0000
+ <BLANKLINE>
+ Testing
+ --------- Comment ---------
+ Name: a:2
+ From: wking <wking@thor.yang.physics.drexel.edu>
+ Date: Thu, 01 Jan 1970 00:00:02 +0000
+ <BLANKLINE>
+ Testing...
+ --------- Comment ---------
+ Name: a:3
+ From: wking <wking@thor.yang.physics.drexel.edu>
+ Date: Thu, 01 Jan 1970 00:00:03 +0000
+ <BLANKLINE>
+ Merged from bug b
+ --------- Comment ---------
+ Name: a:4
+ From: wking <wking@thor.yang.physics.drexel.edu>
+ Date: Thu, 01 Jan 1970 00:00:01 +0000
+ <BLANKLINE>
+ 1 2
+ --------- Comment ---------
+ Name: a:5
+ From: wking <wking@thor.yang.physics.drexel.edu>
+ Date: Thu, 01 Jan 1970 00:00:02 +0000
+ <BLANKLINE>
+ 1 2 3 4
+ >>> b = bd.bug_from_shortname("b")
+ >>> b.load_comments()
+ >>> mergeB = b.comment_from_shortname(":3")
+ >>> mergeB.time = 3
+ >>> print b.string(show_comments=True)
+ ID : b
+ Short name : b
+ Severity : minor
+ Status : closed
+ Assigned :
+ Target :
+ Reporter :
+ Creator : Jane Doe <jdoe@example.com>
+ Created : Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000)
+ Bug B
+ --------- Comment ---------
+ Name: b:1
+ From: wking <wking@thor.yang.physics.drexel.edu>
+ Date: Thu, 01 Jan 1970 00:00:01 +0000
+ <BLANKLINE>
+ 1 2
+ --------- Comment ---------
+ Name: b:2
+ From: wking <wking@thor.yang.physics.drexel.edu>
+ Date: Thu, 01 Jan 1970 00:00:02 +0000
+ <BLANKLINE>
+ 1 2 3 4
+ --------- Comment ---------
+ Name: b:3
+ From: wking <wking@thor.yang.physics.drexel.edu>
+ Date: Thu, 01 Jan 1970 00:00:03 +0000
+ <BLANKLINE>
+ Merged into bug a
+ >>> print b.status
+ closed
+ """
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ cmdutil.default_complete(options, args, parser,
+ bugid_args={0: lambda bug : bug.active==True,
+ 1: lambda bug : bug.active==True})
+
+ if len(args) < 2:
+ raise cmdutil.UsageError("Please specify two bug ids.")
+ if len(args) > 2:
+ help()
+ raise cmdutil.UsageError("Too many arguments.")
+
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
+ bugA = bd.bug_from_shortname(args[0])
+ bugA.load_comments()
+ bugB = bd.bug_from_shortname(args[1])
+ bugB.load_comments()
+ mergeA = bugA.new_comment("Merged from bug %s" % bugB.uuid)
+ newCommTree = copy.deepcopy(bugB.comment_root)
+ for comment in newCommTree.traverse():
+ comment.bug = bugA
+ for comment in newCommTree:
+ mergeA.add_reply(comment, allow_time_inversion=True)
+ bugB.new_comment("Merged into bug %s" % bugA.uuid)
+ bugB.status = "closed"
+ bd.save()
+ print "Merging bugs %s and %s" % (bugA.uuid, bugB.uuid)
+
+def get_parser():
+ parser = cmdutil.CmdOptionParser("be merge BUG-ID BUG-ID")
+ return parser
+
+longhelp="""
+The second bug (B) is merged into the first (A). This adds merge
+comments to both bugs, closes B, and appends B's comment tree to A's
+merge comment.
+"""
+
+def help():
+ return get_parser().help_str() + longhelp
diff --git a/becommands/new.py b/becommands/new.py
index caa1549..1c5246c 100644
--- a/becommands/new.py
+++ b/becommands/new.py
@@ -15,39 +15,53 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Create a new bug"""
-from libbe import cmdutil, bugdir
+from libbe import cmdutil, bugdir, settings_object
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> import os, time
>>> from libbe import bug
>>> bd = bugdir.simple_bug_dir()
>>> os.chdir(bd.root)
>>> bug.uuid_gen = lambda: "X"
- >>> execute (["this is a test",])
+ >>> execute (["this is a test",], test=True)
Created bug with ID X
>>> bd.load()
>>> bug = bd.bug_from_uuid("X")
- >>> bug.summary
- u'this is a test'
+ >>> print bug.summary
+ this is a test
>>> bug.time <= int(time.time())
True
- >>> bug.severity
- u'minor'
- >>> bug.target == None
+ >>> print bug.severity
+ minor
+ >>> bug.target == settings_object.EMPTY
True
"""
- options, args = get_parser().parse_args(args)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ cmdutil.default_complete(options, args, parser)
if len(args) != 1:
- raise cmdutil.UserError("Please supply a summary message")
- bd = bugdir.BugDir(from_disk=True)
+ raise cmdutil.UsageError("Please supply a summary message")
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
bug = bd.new_bug(summary=args[0])
+ if options.reporter != None:
+ bug.reporter = options.reporter
+ else:
+ bug.reporter = bug.creator
+ if options.assigned != None:
+ bug.assigned = options.assigned
+ elif bd.default_assignee != settings_object.EMPTY:
+ bug.assigned = bd.default_assignee
bd.save()
print "Created bug with ID %s" % bd.bug_shortname(bug)
def get_parser():
parser = cmdutil.CmdOptionParser("be new SUMMARY")
+ parser.add_option("-r", "--reporter", metavar="REPORTER", dest="reporter",
+ help="The user who reported the bug", default=None)
+ parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned",
+ help="The developer in charge of the bug", default=None)
return parser
longhelp="""
diff --git a/becommands/open.py b/becommands/open.py
index 788a183..7a18fd0 100644
--- a/becommands/open.py
+++ b/becommands/open.py
@@ -18,25 +18,27 @@
from libbe import cmdutil, bugdir
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> import os
>>> bd = bugdir.simple_bug_dir()
>>> os.chdir(bd.root)
>>> print bd.bug_from_shortname("b").status
closed
- >>> execute(["b"])
+ >>> execute(["b"], test=True)
>>> bd._clear_bugs()
>>> print bd.bug_from_shortname("b").status
open
"""
- options, args = get_parser().parse_args(args)
+ 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.UserError("Please specify a bug id.")
+ raise cmdutil.UsageError, "Please specify a bug id."
if len(args) > 1:
- help()
- raise cmdutil.UserError("Too many arguments.")
- bd = bugdir.BugDir(from_disk=True)
+ raise cmdutil.UsageError, "Too many arguments."
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
bug = bd.bug_from_shortname(args[0])
bug.status = "open"
bd.save()
diff --git a/becommands/remove.py b/becommands/remove.py
index 8f7c2c6..fa264b8 100644
--- a/becommands/remove.py
+++ b/becommands/remove.py
@@ -18,7 +18,7 @@
from libbe import cmdutil, bugdir
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> from libbe import mapfile
>>> import os
@@ -26,7 +26,7 @@ def execute(args):
>>> os.chdir(bd.root)
>>> print bd.bug_from_shortname("b").status
closed
- >>> execute (["b"])
+ >>> execute (["b"], test=True)
Removed bug b
>>> bd._clear_bugs()
>>> try:
@@ -35,10 +35,13 @@ def execute(args):
... print "Bug not found"
Bug not found
"""
- options, args = get_parser().parse_args(args)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ cmdutil.default_complete(options, args, parser,
+ bugid_args={0: lambda bug : bug.active==True})
if len(args) != 1:
- raise cmdutil.UserError("Please specify a bug id.")
- bd = bugdir.BugDir(from_disk=True)
+ raise cmdutil.UsageError, "Please specify a bug id."
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
bug = bd.bug_from_shortname(args[0])
bd.remove_bug(bug)
bd.save()
diff --git a/becommands/set.py b/becommands/set.py
index 287ceb4..b8a125e 100644
--- a/becommands/set.py
+++ b/becommands/set.py
@@ -15,38 +15,60 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Change tree settings"""
-from libbe import cmdutil, bugdir
+from libbe import cmdutil, bugdir, settings_object
__desc__ = __doc__
-def execute(args):
+def _value_string(bd, setting):
+ val = bd.settings.get(setting, settings_object.EMPTY)
+ if val == settings_object.EMPTY:
+ default = getattr(bd, bd._setting_name_to_attr_name(setting))
+ if default != settings_object.EMPTY:
+ val = "None (%s)" % default
+ else:
+ val = None
+ return str(val)
+
+def execute(args, test=False):
"""
>>> import os
>>> bd = bugdir.simple_bug_dir()
>>> os.chdir(bd.root)
- >>> execute(["target"])
+ >>> execute(["target"], test=True)
None
- >>> execute(["target", "tomorrow"])
- >>> execute(["target"])
+ >>> execute(["target", "tomorrow"], test=True)
+ >>> execute(["target"], test=True)
tomorrow
- >>> execute(["target", "none"])
- >>> execute(["target"])
+ >>> execute(["target", "none"], test=True)
+ >>> execute(["target"], test=True)
None
"""
- options, args = get_parser().parse_args(args)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ complete(options, args, parser)
if len(args) > 2:
- help()
- raise cmdutil.UserError("Too many arguments.")
- bd = bugdir.BugDir(from_disk=True)
+ raise cmdutil.UsageError, "Too many arguments"
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
if len(args) == 0:
- keys = bd.settings.keys()
+ keys = bd.settings_properties
keys.sort()
for key in keys:
- print "%16s: %s" % (key, bd.settings[key])
+ print "%16s: %s" % (key, _value_string(bd, key))
elif len(args) == 1:
- print bd.settings.get(args[0])
+ print _value_string(bd, args[0])
else:
if args[1] != "none":
- bd.settings[args[0]] = args[1]
+ if args[0] not in bd.settings_properties:
+ msg = "Invalid setting %s\n" % args[0]
+ msg += 'Allowed settings:\n '
+ msg += '\n '.join(bd.settings_properties)
+ raise cmdutil.UserError(msg)
+ old_setting = bd.settings.get(args[0])
+ try:
+ setattr(bd, args[0], args[1])
+ except bugdir.InvalidValue, e:
+ bd.settings[args[0]] = old_setting
+ bd.save()
+ raise cmdutil.UserError(e)
else:
del bd.settings[args[0]]
bd.save()
@@ -73,3 +95,15 @@ To unset a setting, set it to "none".
def help():
return get_parser().help_str() + longhelp
+
+def complete(options, args, parser):
+ for option, value in cmdutil.option_value_pairs(options, parser):
+ if value == "--complete":
+ # no argument-options at the moment, so this is future-proofing
+ raise cmdutil.GetCompletions()
+ for pos,value in enumerate(args):
+ if value == "--complete":
+ if pos == 0: # first positional argument is a setting name
+ props = bugdir.BugDir.settings_properties
+ raise cmdutil.GetCompletions(props)
+ raise cmdutil.GetCompletions() # no positional arguments for list
diff --git a/becommands/set_root.py b/becommands/set_root.py
index e17bd87..3749e28 100644
--- a/becommands/set_root.py
+++ b/becommands/set_root.py
@@ -19,7 +19,7 @@ import os.path
from libbe import cmdutil, bugdir
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> from libbe import utility, rcs
>>> import os
@@ -29,7 +29,7 @@ def execute(args):
... except bugdir.NoBugDir, e:
... True
True
- >>> execute([dir.path])
+ >>> execute([dir.path], test=True)
No revision control detected.
Directory initialized.
>>> del(dir)
@@ -40,34 +40,33 @@ def execute(args):
>>> rcs.init('.')
>>> print rcs.name
Arch
- >>> execute([])
+ >>> execute([], test=True)
Using Arch for revision control.
Directory initialized.
>>> rcs.cleanup()
>>> try:
- ... execute(['.'])
+ ... execute(['.'], test=True)
... except cmdutil.UserError, e:
... str(e).startswith("Directory already initialized: ")
True
- >>> execute(['/highly-unlikely-to-exist'])
+ >>> execute(['/highly-unlikely-to-exist'], test=True)
Traceback (most recent call last):
UserError: No such directory: /highly-unlikely-to-exist
>>> os.chdir('/')
"""
- options, args = get_parser().parse_args(args)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ cmdutil.default_complete(options, args, parser)
if len(args) > 1:
- print help()
- raise cmdutil.UserError, "Too many arguments"
+ raise cmdutil.UsageError
if len(args) == 1:
basedir = args[0]
else:
basedir = "."
- if os.path.exists(basedir) == False:
- pass
- #raise cmdutil.UserError, "No such directory: %s" % basedir
try:
- bd = bugdir.BugDir(basedir, from_disk=False, sink_to_existing_root=False, assert_new_BugDir=True)
+ bd = bugdir.BugDir(basedir, from_disk=False, sink_to_existing_root=False, assert_new_BugDir=True,
+ manipulate_encodings=not test)
except bugdir.NoRootEntry:
raise cmdutil.UserError("No such directory: %s" % basedir)
except bugdir.AlreadyInitialized:
diff --git a/becommands/severity.py b/becommands/severity.py
index 3adefaa..5d27222 100644
--- a/becommands/severity.py
+++ b/becommands/severity.py
@@ -15,29 +15,29 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Show or change a bug's severity level"""
-from libbe import cmdutil, bugdir
-from libbe.bug import severity_values, severity_description
+from libbe import cmdutil, bugdir, bug
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> import os
>>> bd = bugdir.simple_bug_dir()
>>> os.chdir(bd.root)
- >>> execute(["a"])
+ >>> execute(["a"], test=True)
minor
- >>> execute(["a", "wishlist"])
- >>> execute(["a"])
+ >>> execute(["a", "wishlist"], test=True)
+ >>> execute(["a"], test=True)
wishlist
- >>> execute(["a", "none"])
+ >>> execute(["a", "none"], test=True)
Traceback (most recent call last):
UserError: Invalid severity level: none
"""
- options, args = get_parser().parse_args(args)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ complete(options, args, parser)
if len(args) not in (1,2):
- print help()
- return
- bd = bugdir.BugDir(from_disk=True)
+ raise cmdutil.UsageError
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
bug = bd.bug_from_shortname(args[0])
if len(args) == 1:
print bug.severity
@@ -46,7 +46,7 @@ def execute(args):
bug.severity = args[1]
except ValueError, e:
if e.name != "severity":
- raise
+ raise e
raise cmdutil.UserError ("Invalid severity level: %s" % e.value)
bd.save()
@@ -54,7 +54,8 @@ def get_parser():
parser = cmdutil.CmdOptionParser("be severity BUG-ID [SEVERITY]")
return parser
-longhelp=["""
+def help():
+ longhelp=["""
Show or change a bug's severity level.
If no severity is specified, the current value is printed. If a severity level
@@ -62,13 +63,38 @@ is specified, it will be assigned to the bug.
Severity levels are:
"""]
-longest_severity_len = max([len(s) for s in severity_values])
-for severity in severity_values :
- description = severity_description[severity]
- s = "%*s : %s\n" % (longest_severity_len, severity, description)
- longhelp.append(s)
-longhelp = ''.join(longhelp)
-
-
-def help():
+ try: # See if there are any per-tree severity configurations
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False)
+ except bugdir.NoBugDir, e:
+ pass # No tree, just show the defaults
+ longest_severity_len = max([len(s) for s in bug.severity_values])
+ for severity in bug.severity_values :
+ description = bug.severity_description[severity]
+ s = "%*s : %s\n" % (longest_severity_len, severity, description)
+ longhelp.append(s)
+ longhelp = ''.join(longhelp)
return get_parser().help_str() + longhelp
+
+def complete(options, args, parser):
+ for option,value in cmdutil.option_value_pairs(options, parser):
+ if value == "--complete":
+ # no argument-options at the moment, so this is future-proofing
+ raise cmdutil.GetCompletions()
+ for pos,value in enumerate(args):
+ if value == "--complete":
+ try: # See if there are any per-tree severity configurations
+ bd = bugdir.BugDir(from_disk=True,
+ manipulate_encodings=False)
+ except bugdir.NoBugDir:
+ bd = None
+ if pos == 0: # fist positional argument is a bug id
+ ids = []
+ if bd != None:
+ bd.load_all_bugs()
+ filter = lambda bg : bg.active==True
+ bugs = [bg for bg in bd if filter(bg)==True]
+ ids = [bd.bug_shortname(bg) for bg in bugs]
+ raise cmdutil.GetCompletions(ids)
+ elif pos == 1: # second positional argument is a severity
+ raise cmdutil.GetCompletions(bug.severity_values)
+ raise cmdutil.GetCompletions()
diff --git a/becommands/show.py b/becommands/show.py
index 1ee354c..7c48257 100644
--- a/becommands/show.py
+++ b/becommands/show.py
@@ -18,33 +18,52 @@
from libbe import cmdutil, bugdir
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> import os
>>> bd = bugdir.simple_bug_dir()
>>> os.chdir(bd.root)
- >>> execute (["a",])
+ >>> execute (["a",], test=True)
ID : a
Short name : a
Severity : minor
Status : open
Assigned :
Target :
+ Reporter :
Creator : John Doe <jdoe@example.com>
Created : Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000)
Bug A
<BLANKLINE>
+ >>> execute (["--xml", "a"], test=True)
+ <bug>
+ <uuid>a</uuid>
+ <short-name>a</short-name>
+ <severity>minor</severity>
+ <status>open</status>
+ <assigned><class 'libbe.settings_object.EMPTY'></assigned>
+ <target><class 'libbe.settings_object.EMPTY'></target>
+ <reporter><class 'libbe.settings_object.EMPTY'></reporter>
+ <creator>John Doe <jdoe@example.com></creator>
+ <created>Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000)</created>
+ <summary>Bug A</summary>
+ </bug>
"""
- options, args = get_parser().parse_args(args)
+ 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.UserError("Please specify a bug id.")
- bd = bugdir.BugDir(from_disk=True)
+ raise cmdutil.UsageError
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
for bugid in args:
bug = bd.bug_from_shortname(bugid)
if options.dumpXML:
print bug.xml(show_comments=True)
else:
print bug.string(show_comments=True)
+ if bugid != args[-1]:
+ print "" # add a blank line between bugs
def get_parser():
parser = cmdutil.CmdOptionParser("be show [options] BUG-ID [BUG-ID ...]")
diff --git a/becommands/status.py b/becommands/status.py
index a30b3d6..40e9b51 100644
--- a/becommands/status.py
+++ b/becommands/status.py
@@ -15,33 +15,33 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Show or change a bug's status"""
-from libbe import cmdutil, bugdir
-from libbe.bug import status_values, status_description
+from libbe import cmdutil, bugdir, bug
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> import os
>>> bd = bugdir.simple_bug_dir()
>>> os.chdir(bd.root)
- >>> execute(["a"])
+ >>> execute(["a"], test=True)
open
- >>> execute(["a", "closed"])
- >>> execute(["a"])
+ >>> execute(["a", "closed"], test=True)
+ >>> execute(["a"], test=True)
closed
- >>> execute(["a", "none"])
+ >>> execute(["a", "none"], test=True)
Traceback (most recent call last):
UserError: Invalid status: none
"""
- options, args = get_parser().parse_args(args)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ complete(options, args, parser)
if len(args) not in (1,2):
- print help()
- return
- bd = bugdir.BugDir(from_disk=True)
+ raise cmdutil.UsageError
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
bug = bd.bug_from_shortname(args[0])
if len(args) == 1:
print bug.status
- elif len(args) == 2:
+ else:
try:
bug.status = args[1]
except ValueError, e:
@@ -54,20 +54,46 @@ def get_parser():
parser = cmdutil.CmdOptionParser("be status BUG-ID [STATUS]")
return parser
-longhelp=["""
-Show or change a bug's severity level.
-If no severity is specified, the current value is printed. If a severity level
+def help():
+ longhelp=["""
+Show or change a bug's status.
+
+If no status is specified, the current value is printed. If a status
is specified, it will be assigned to the bug.
-Severity levels are:
+Status levels are:
"""]
-longest_status_len = max([len(s) for s in status_values])
-for status in status_values :
- description = status_description[status]
- s = "%*s : %s\n" % (longest_status_len, status, description)
- longhelp.append(s)
-longhelp = ''.join(longhelp)
-
-def help():
+ try: # See if there are any per-tree status configurations
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False)
+ except bugdir.NoBugDir, e:
+ pass # No tree, just show the defaults
+ longest_status_len = max([len(s) for s in bug.status_values])
+ for status in bug.status_values :
+ description = bug.status_description[status]
+ s = "%*s : %s\n" % (longest_status_len, status, description)
+ longhelp.append(s)
+ longhelp = ''.join(longhelp)
return get_parser().help_str() + longhelp
+
+def complete(options, args, parser):
+ for option,value in cmdutil.option_value_pairs(options, parser):
+ if value == "--complete":
+ # no argument-options at the moment, so this is future-proofing
+ raise cmdutil.GetCompletions()
+ for pos,value in enumerate(args):
+ if value == "--complete":
+ try: # See if there are any per-tree status configurations
+ bd = bugdir.BugDir(from_disk=True,
+ manipulate_encodings=False)
+ except bugdir.NoBugDir:
+ bd = None
+ if pos == 0: # fist positional argument is a bug id
+ ids = []
+ if bd != None:
+ bd.load_all_bugs()
+ ids = [bd.bug_shortname(bg) for bg in bd]
+ raise cmdutil.GetCompletions(ids)
+ elif pos == 1: # second positional argument is a status
+ raise cmdutil.GetCompletions(bug.status_values)
+ raise cmdutil.GetCompletions()
diff --git a/becommands/target.py b/becommands/target.py
index dce100f..c83ffa7 100644
--- a/becommands/target.py
+++ b/becommands/target.py
@@ -15,36 +15,38 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Show or change a bug's target for fixing"""
-from libbe import cmdutil, bugdir
+from libbe import cmdutil, bugdir, settings_object
__desc__ = __doc__
-def execute(args):
+def execute(args, test=False):
"""
>>> import os
>>> bd = bugdir.simple_bug_dir()
>>> os.chdir(bd.root)
- >>> execute(["a"])
+ >>> execute(["a"], test=True)
No target assigned.
- >>> execute(["a", "tomorrow"])
- >>> execute(["a"])
+ >>> execute(["a", "tomorrow"], test=True)
+ >>> execute(["a"], test=True)
tomorrow
- >>> execute(["a", "none"])
- >>> execute(["a"])
+ >>> execute(["a", "none"], test=True)
+ >>> execute(["a"], test=True)
No target assigned.
"""
- options, args = get_parser().parse_args(args)
- assert(len(args) in (0, 1, 2))
- if len(args) == 0:
- print help()
- return
- bd = bugdir.BugDir(from_disk=True)
+ parser = get_parser()
+ options, args = parser.parse_args(args)
+ cmdutil.default_complete(options, args, parser,
+ bugid_args={0: lambda bug : bug.active==True})
+ if len(args) not in (1, 2):
+ raise cmdutil.UsageError
+ bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
bug = bd.bug_from_shortname(args[0])
if len(args) == 1:
- if bug.target is None:
+ if bug.target is None or bug.target is settings_object.EMPTY:
print "No target assigned."
else:
print bug.target
- elif len(args) == 2:
+ else:
+ assert len(args) == 2
if args[1] == "none":
bug.target = None
else:
diff --git a/libbe/arch.py b/libbe/arch.py
index fd953a4..1173535 100644
--- a/libbe/arch.py
+++ b/libbe/arch.py
@@ -14,10 +14,11 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+import codecs
import os
+import re
import shutil
import time
-import re
import unittest
import doctest
@@ -133,13 +134,16 @@ class Arch(RCS):
"""
tagpath = os.path.join(path, "{arch}", "=tagging-method")
lines_out = []
- for line in file(tagpath, "rb"):
- line.decode("utf-8")
+ 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)
- file(tagpath, "wb").write("".join(lines_out).encode("utf-8"))
+ 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
@@ -215,7 +219,9 @@ class Arch(RCS):
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')
- file(inv_path, "ab").write(rule)
+ 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
@@ -233,12 +239,16 @@ class Arch(RCS):
pass
def _rcs_get_file_contents(self, path, revision=None):
if revision == None:
- return file(self._u_abspath(path), "rb").read()
+ return RCS._rcs_get_file_contents(self, path, revision)
else:
status,output,error = \
self._invoke_client("file-find", path, revision)
- path = output.rstrip('\n')
- return file(self._u_abspath(path), "rb").read()
+ relpath = output.rstrip('\n')
+ abspath = os.path.join(self.rootdir, relpath)
+ f = codecs.open(abspath, "r", self.encoding)
+ contents = f.read()
+ f.close()
+ return contents
def _rcs_duplicate_repo(self, directory, revision=None):
if revision == None:
RCS._rcs_duplicate_repo(self, directory, revision)
diff --git a/libbe/bug.py b/libbe/bug.py
index afa9e09..fe059fa 100644
--- a/libbe/bug.py
+++ b/libbe/bug.py
@@ -21,6 +21,10 @@ import time
import doctest
from beuuid import uuid_gen
+from properties import Property, doc_property, local_property, \
+ defaulting_property, checked_property, cached_property, \
+ primed_property, change_hook_property, settings_property
+import settings_object
import mapfile
import comment
import utility
@@ -30,9 +34,9 @@ import utility
# 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
-severity_level_def = (
- ("wishlist","A feature that could improve usefullness, but not a bug."),
+# in order of increasing severity. (name, description) pairs
+severity_def = (
+ ("wishlist","A feature that could improve usefulness, but not a bug."),
("minor","The standard bug level."),
("serious","A bug that requires workarounds."),
("critical","A bug that prevents some features from working at all."),
@@ -48,86 +52,189 @@ active_status_def = (
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."),
- ("disabled", "?"))
+ ("wontfix","It's not a bug, it's a feature."))
### Convert the description tuples to more useful formats
-severity_values = tuple([val for val,description in severity_level_def])
-severity_description = dict(severity_level_def)
+severity_values = ()
+severity_description = {}
severity_index = {}
-for i in range(len(severity_values)):
- severity_index[severity_values[i]] = i
-
-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(active_status_def+inactive_status_def)
+def load_severities(severity_def):
+ global severity_values
+ global severity_description
+ global severity_index
+ if severity_def == settings_object.EMPTY:
+ return
+ severity_values = tuple([val for val,description in severity_def])
+ severity_description = dict(severity_def)
+ severity_index = {}
+ for i,severity in enumerate(severity_values):
+ severity_index[severity] = i
+load_severities(severity_def)
+
+active_status_values = []
+inactive_status_values = []
+status_values = []
+status_description = {}
status_index = {}
-for i in range(len(status_values)):
- status_index[status_values[i]] = i
-
-
-def checked_property(name, valid):
+def load_status(active_status_def, inactive_status_def):
+ global active_status_values
+ global inactive_status_values
+ global status_values
+ global status_description
+ global status_index
+ if active_status_def == settings_object.EMPTY:
+ active_status_def = globals()["active_status_def"]
+ if inactive_status_def == settings_object.EMPTY:
+ inactive_status_def = globals()["inactive_status_def"]
+ active_status_values = tuple([val for val,description in active_status_def])
+ inactive_status_values = tuple([val for val,description in inactive_status_def])
+ status_values = active_status_values + inactive_status_values
+ status_description = dict(tuple(active_status_def) + tuple(inactive_status_def))
+ status_index = {}
+ for i,status in enumerate(status_values):
+ status_index[status] = i
+load_status(active_status_def, inactive_status_def)
+
+
+class Bug(settings_object.SavedSettingsObject):
"""
- Provide access to an attribute name, testing for valid values.
+ >>> 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
"""
- def getter(self):
- value = getattr(self, "_"+name)
- if value not in valid:
- raise InvalidValue(name, value)
- return value
-
- def setter(self, value):
- if value not in valid:
- raise InvalidValue(name, value)
- return setattr(self, "_"+name, value)
- return property(getter, setter)
+ settings_properties = []
+ required_saved_properties = []
+ _prop_save_settings = settings_object.prop_save_settings
+ _prop_load_settings = settings_object.prop_load_settings
+ def _versioned_property(settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties,
+ **kwargs):
+ if "settings_properties" not in kwargs:
+ kwargs["settings_properties"] = settings_properties
+ if "required_saved_properties" not in kwargs:
+ kwargs["required_saved_properties"]=required_saved_properties
+ return settings_object.versioned_property(**kwargs)
+
+ @_versioned_property(name="severity",
+ doc="A measure of the bug's importance",
+ default="minor",
+ check_fn=lambda s: s in severity_values,
+ require_save=True)
+ def severity(): return {}
+
+ @_versioned_property(name="status",
+ doc="The bug's current status",
+ default="open",
+ check_fn=lambda s: s in status_values,
+ require_save=True)
+ def status(): return {}
+
+ @property
+ def active(self):
+ return self.status in active_status_values
+ @_versioned_property(name="target",
+ doc="The deadline for fixing this bug")
+ def target(): return {}
+
+ @_versioned_property(name="creator",
+ doc="The user who entered the bug into the system")
+ def creator(): return {}
+
+ @_versioned_property(name="reporter",
+ doc="The user who reported the bug")
+ def reporter(): return {}
+
+ @_versioned_property(name="assigned",
+ doc="The developer in charge of the bug")
+ def assigned(): return {}
+
+ @_versioned_property(name="time",
+ doc="An RFC 2822 timestamp for bug creation")
+ def time_string(): return {}
+
+ def _get_time(self):
+ if self.time_string == None or self.time_string == settings_object.EMPTY:
+ return None
+ return utility.str_to_time(self.time_string)
+ def _set_time(self, value):
+ self.time_string = utility.time_to_str(value)
+ time = property(fget=_get_time,
+ fset=_set_time,
+ doc="An integer version of .time_string")
+
+ @_versioned_property(name="summary",
+ doc="A one-line bug description")
+ def summary(): return {}
+
+ def _get_comment_root(self, load_full=False):
+ if self.sync_with_disk:
+ return comment.loadComments(self, load_full=load_full)
+ else:
+ return comment.Comment(self, uuid=comment.INVALID_UUID)
-class Bug(object):
- severity = checked_property("severity", severity_values)
- status = checked_property("status", status_values)
+ @Property
+ @cached_property(generator=_get_comment_root)
+ @local_property("comment_root")
+ @doc_property(doc="The trunk of the comment tree")
+ def comment_root(): return {}
- def _get_active(self):
- return self.status in active_status_values
+ def _get_rcs(self):
+ if hasattr(self.bugdir, "rcs"):
+ return self.bugdir.rcs
- active = property(_get_active)
+ @Property
+ @cached_property(generator=_get_rcs)
+ @local_property("rcs")
+ @doc_property(doc="A revision control system instance.")
+ def rcs(): return {}
def __init__(self, bugdir=None, uuid=None, from_disk=False,
load_comments=False, summary=None):
+ settings_object.SavedSettingsObject.__init__(self)
self.bugdir = bugdir
- if bugdir != None:
- self.rcs = bugdir.rcs
- else:
- self.rcs = None
+ self.uuid = uuid
if from_disk == True:
- self._comments_loaded = False
- self.uuid = uuid
- self.load(load_comments=load_comments)
+ self.sync_with_disk = True
else:
- # Note: defaults should match those in Bug.load()
- self._comments_loaded = True
- if uuid != None:
- self.uuid = uuid
- else:
+ self.sync_with_disk = False
+ if uuid == None:
self.uuid = uuid_gen()
- self.summary = summary
+ self.time = int(time.time()) # only save to second precision
if self.rcs != None:
self.creator = self.rcs.get_user_id()
- else:
- self.creator = None
- self.target = None
- self.status = "open"
- self.severity = "minor"
- self.assigned = None
- self.time = int(time.time()) # only save to second precision
- self.comment_root = comment.Comment(self, uuid=comment.INVALID_UUID)
+ self.summary = summary
def __repr__(self):
return "Bug(uuid=%r)" % self.uuid
+ def _setting_attr_string(self, setting):
+ value = getattr(self, setting)
+ if value == settings_object.EMPTY:
+ return ""
+ else:
+ return str(value)
+
def xml(self, show_comments=False):
if self.bugdir == None:
shortname = self.uuid
@@ -147,6 +254,7 @@ class Bug(object):
("status", self.status),
("assigned", self.assigned),
("target", self.target),
+ ("reporter", self.reporter),
("creator", self.creator),
("created", timestring),
("summary", self.summary)]
@@ -155,9 +263,7 @@ class Bug(object):
if v is not None:
ret += ' <%s>%s</%s>\n' % (k,v,k)
- if show_comments:
- if self._comments_loaded == False:
- self.load_comments()
+ if show_comments == True:
comout = self.comment_root.xml_thread(auto_name_map=True,
bug_shortname=shortname)
ret += comout
@@ -171,27 +277,20 @@ class Bug(object):
else:
shortname = self.bugdir.bug_shortname(self)
if shortlist == False:
- if self.time == None:
- timestring = ""
+ if self.time_string == "":
+ timestring = self.time_string
else:
htime = utility.handy_time(self.time)
- ftime = utility.time_to_str(self.time)
- timestring = "%s (%s)" % (htime, ftime)
+ timestring = "%s (%s)" % (htime, self.time_string)
info = [("ID", self.uuid),
("Short name", shortname),
("Severity", self.severity),
("Status", self.status),
- ("Assigned", self.assigned),
- ("Target", self.target),
- ("Creator", self.creator),
+ ("Assigned", self._setting_attr_string("assigned")),
+ ("Target", self._setting_attr_string("target")),
+ ("Reporter", self._setting_attr_string("reporter")),
+ ("Creator", self._setting_attr_string("creator")),
("Created", timestring)]
- newinfo = []
- for k,v in info:
- if v == None:
- newinfo.append((k,""))
- else:
- newinfo.append((k,v))
- info = newinfo
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')
@@ -199,12 +298,13 @@ class Bug(object):
statuschar = self.status[0]
severitychar = self.severity[0]
chars = "%c%c" % (statuschar, severitychar)
- bugout = "%s:%s: %s" % (shortname, chars, self.summary.rstrip('\n'))
+ bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
if show_comments == True:
- if self._comments_loaded == False:
- self.load_comments()
- comout = self.comment_root.string_thread(auto_name_map=True,
+ # take advantage of the string_thread(auto_name_map=True)
+ # SIDE-EFFECT of sorting by comment time.
+ comout = self.comment_root.string_thread(flatten=False,
+ auto_name_map=True,
bug_shortname=shortname)
output = bugout + '\n' + comout.rstrip('\n')
else :
@@ -224,75 +324,67 @@ class Bug(object):
assert name in ["values", "comments"]
return os.path.join(my_dir, name)
- def load(self, load_comments=False):
- map = mapfile.map_load(self.rcs, self.get_path("values"))
- self.summary = map.get("summary")
- self.creator = map.get("creator")
- self.target = map.get("target")
- self.status = map.get("status", "open")
- self.severity = map.get("severity", "minor")
- self.assigned = map.get("assigned")
- self.time = map.get("time")
- if self.time is not None:
- self.time = utility.str_to_time(self.time)
-
- if load_comments == True:
- self.load_comments()
-
- def load_comments(self):
- self.comment_root = comment.loadComments(self)
- self._comments_loaded = True
+ def load_settings(self):
+ self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
+ self._setup_saved_settings()
- def comments(self):
- if self._comments_loaded == False:
- self.load_comments()
- for comment in self.comment_root.traverse():
- yield comment
-
- def _add_attr(self, map, name):
- value = getattr(self, name)
- if value is not None:
- map[name] = value
-
- def save(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
+ # _get_comment_root returns a fresh version. Turn of
+ # syncing temporarily so we don't write our blank comment
+ # tree to disk.
+ self.sync_with_disk = False
+ self.comment_root = None
+ self.sync_with_disk = True
+
+ def save_settings(self):
assert self.summary != None, "Can't save blank bug"
- map = {}
- self._add_attr(map, "assigned")
- self._add_attr(map, "summary")
- self._add_attr(map, "creator")
- self._add_attr(map, "target")
- self._add_attr(map, "status")
- self._add_attr(map, "severity")
- if self.time is not None:
- map["time"] = utility.time_to_str(self.time)
-
+
self.rcs.mkdir(self.get_path())
path = self.get_path("values")
- mapfile.map_save(self.rcs, path, map)
+ mapfile.map_save(self.rcs, path, self._get_saved_settings())
+
+ def save(self):
+ self.save_settings()
- if self._comments_loaded:
- if len(self.comment_root) > 0:
- self.rcs.mkdir(self.get_path("comments"))
- comment.saveComments(self)
+ if len(self.comment_root) > 0:
+ self.rcs.mkdir(self.get_path("comments"))
+ comment.saveComments(self)
def remove(self):
- self.load_comments()
self.comment_root.remove()
path = self.get_path()
self.rcs.recursive_remove(path)
+ def comments(self):
+ for comment in self.comment_root.traverse():
+ yield comment
+
def new_comment(self, body=None):
- comm = comment.comment_root.new_reply(body=body)
+ comm = self.comment_root.new_reply(body=body)
return comm
def comment_from_shortname(self, shortname, *args, **kwargs):
- return self.comment_root.comment_from_shortname(shortname, *args, **kwargs)
+ return self.comment_root.comment_from_shortname(shortname,
+ *args, **kwargs)
def comment_from_uuid(self, uuid):
return self.comment_root.comment_from_uuid(uuid)
+ def comment_shortnames(self, shortname=None):
+ """
+ SIDE-EFFECT : Comment.comment_shortnames will sort the comment
+ tree by comment.time
+ """
+ for id, comment in self.comment_root.comment_shortnames(shortname):
+ yield (id, comment)
-# the general rule for bug sorting is that "more important" bugs are
+
+# 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
@@ -359,10 +451,15 @@ def cmp_attr(bug_1, bug_2, attr, invert=False):
"""
if not hasattr(bug_2, attr) :
return 1
+ val_1 = getattr(bug_1, attr)
+ val_2 = getattr(bug_2, attr)
+ if val_1 == settings_object.EMPTY: val_1 = None
+ if val_2 == settings_object.EMPTY: val_2 = None
+
if invert == True :
- return -cmp(getattr(bug_1, attr), getattr(bug_2, attr))
+ return -cmp(val_1, val_2)
else :
- return cmp(getattr(bug_1, attr), getattr(bug_2, attr))
+ return cmp(val_1, val_2)
# alphabetical rankings (a < z)
cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index 7e4cf3e..7885224 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -22,10 +22,16 @@ import copy
import unittest
import doctest
+from properties import Property, doc_property, local_property, \
+ defaulting_property, checked_property, fn_checked_property, \
+ cached_property, primed_property, change_hook_property, \
+ settings_property
+import settings_object
import mapfile
import bug
-import utility
import rcs
+import encoding
+import utility
class NoBugDir(Exception):
@@ -64,28 +70,29 @@ class MultipleBugMatches(ValueError):
TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n"
-def setting_property(name, valid=None, doc=None):
- def getter(self):
- value = self.settings.get(name)
- if valid is not None:
- if value not in valid and value != None:
- raise InvalidValue(name, value)
- return value
+class BugDir (list, settings_object.SavedSettingsObject):
+ """
+ Sink to existing root
+ ======================
- def setter(self, value):
- if value != getter(self):
- if value is None:
- del self.settings[name]
- else:
- self.settings[name] = value
- self._save_settings(self.get_path("settings"), self.settings)
+ Consider the following usage case:
+ You have a bug directory rooted in
+ /path/to/source
+ by which I mean the '.be' directory is at
+ /path/to/source/.be
+ However, you're of in some subdirectory like
+ /path/to/source/GUI/testing
+ and you want to comment on a bug. Setting sink_to_root=True wen
+ you initialize your BugDir will cause it to search for the '.be'
+ file in the ancestors of the path you passed in as 'root'.
+ /path/to/source/GUI/testing/.be miss
+ /path/to/source/GUI/.be miss
+ /path/to/source/.be hit!
+ So it still roots itself appropriately without much work for you.
+
+ File-system access
+ ==================
- return property(getter, setter, doc=doc)
-
-
-class BugDir (list):
- """
- File-system access:
When rooted in non-bugdir directory, BugDirs live completely in
memory until the first call to .save(). This creates a '.be'
sub-directory containing configurations options, bugs, comments,
@@ -95,13 +102,166 @@ class BugDir (list):
will only load information from the file system when it loads new
bugs/comments that it doesn't already have in memory, or when it
explicitly asked to do so (e.g. .load() or __init__(from_disk=True)).
+
+ Allow RCS initialization
+ ========================
+
+ This one is for testing purposes. Setting it to True allows the
+ BugDir to search for an installed RCS backend and initialize it in
+ the root directory. This is a convenience option for supporting
+ tests of versioning functionality (e.g. .duplicate_bugdir).
+
+ Disable encoding manipulation
+ =============================
+
+ This one is for testing purposed. You might have non-ASCII
+ Unicode in your bugs, comments, files, etc. BugDir instances try
+ and support your preferred encoding scheme (e.g. "utf-8") when
+ dealing with stream and file input/output. For stream output,
+ this involves replacing sys.stdout and sys.stderr
+ (libbe.encode.set_IO_stream_encodings). However this messes up
+ doctest's output catching. In order to support doctest tests
+ using BugDirs, set manipulate_encodings=False, and stick to ASCII
+ in your tests.
"""
+
+ settings_properties = []
+ required_saved_properties = []
+ _prop_save_settings = settings_object.prop_save_settings
+ _prop_load_settings = settings_object.prop_load_settings
+ def _versioned_property(settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties,
+ **kwargs):
+ if "settings_properties" not in kwargs:
+ kwargs["settings_properties"] = settings_properties
+ if "required_saved_properties" not in kwargs:
+ kwargs["required_saved_properties"]=required_saved_properties
+ return settings_object.versioned_property(**kwargs)
+
+ @_versioned_property(name="target",
+ doc="The current project development target")
+ def target(): return {}
+
+ def _guess_encoding(self):
+ return encoding.get_encoding()
+ def _check_encoding(value):
+ if value != None and value != settings_object.EMPTY:
+ return encoding.known_encoding(value)
+ def _setup_encoding(self, new_encoding):
+ if new_encoding != None and new_encoding != settings_object.EMPTY:
+ if self._manipulate_encodings == True:
+ encoding.set_IO_stream_encodings(new_encoding)
+ def _set_encoding(self, old_encoding, new_encoding):
+ self._setup_encoding(new_encoding)
+ self._prop_save_settings(old_encoding, new_encoding)
+
+ @_versioned_property(name="encoding",
+ doc="""The default input/output encoding to use (e.g. "utf-8").""",
+ change_hook=_set_encoding,
+ generator=_guess_encoding,
+ check_fn=_check_encoding)
+ def encoding(): return {}
+
+ def _setup_user_id(self, user_id):
+ self.rcs.user_id = user_id
+ def _guess_user_id(self):
+ return self.rcs.get_user_id()
+ def _set_user_id(self, old_user_id, new_user_id):
+ self._setup_user_id(new_user_id)
+ self._prop_save_settings(old_user_id, new_user_id)
+
+ @_versioned_property(name="user_id",
+ doc=
+"""The user's prefered name, e.g. 'John Doe <jdoe@example.com>'. Note
+that the Arch RCS backend *enforces* ids with this format.""",
+ change_hook=_set_user_id,
+ generator=_guess_user_id)
+ def user_id(): return {}
+
+ @_versioned_property(name="default_assignee",
+ doc=
+"""The default assignee for new bugs e.g. 'John Doe <jdoe@example.com>'.""")
+ def default_assignee(): return {}
+
+ @_versioned_property(name="rcs_name",
+ doc="""The name of the current RCS. Kept seperate to make saving/loading
+settings easy. Don't set this attribute. Set .rcs instead, and
+.rcs_name will be automatically adjusted.""",
+ default="None",
+ allowed=["None", "Arch", "bzr", "git", "hg"])
+ def rcs_name(): return {}
+
+ def _get_rcs(self, rcs_name=None):
+ """Get and root a new revision control system"""
+ if rcs_name == None:
+ rcs_name = self.rcs_name
+ new_rcs = rcs.rcs_by_name(rcs_name)
+ self._change_rcs(None, new_rcs)
+ return new_rcs
+ def _change_rcs(self, old_rcs, new_rcs):
+ new_rcs.encoding = self.encoding
+ new_rcs.root(self.root)
+ self.rcs_name = new_rcs.name
+
+ @Property
+ @change_hook_property(hook=_change_rcs)
+ @cached_property(generator=_get_rcs)
+ @local_property("rcs")
+ @doc_property(doc="A revision control system instance.")
+ def rcs(): return {}
+
+ def _bug_map_gen(self):
+ map = {}
+ for bug in self:
+ map[bug.uuid] = bug
+ for uuid in self.list_uuids():
+ if uuid not in map:
+ map[uuid] = None
+ self._bug_map_value = map # ._bug_map_value used by @local_property
+
+ @Property
+ @primed_property(primer=_bug_map_gen)
+ @local_property("bug_map")
+ @doc_property(doc="A dict of (bug-uuid, bug-instance) pairs.")
+ def _bug_map(): return {}
+
+ def _setup_severities(self, severities):
+ if severities != None and severities != settings_object.EMPTY:
+ bug.load_severities(severities)
+ def _set_severities(self, old_severities, new_severities):
+ self._setup_severities(new_severities)
+ self._prop_save_settings(old_severities, new_severities)
+ @_versioned_property(name="severities",
+ doc="The allowed bug severities and their descriptions.",
+ change_hook=_set_severities)
+ def severities(): return {}
+
+ def _setup_status(self, active_status, inactive_status):
+ bug.load_status(active_status, inactive_status)
+ def _set_active_status(self, old_active_status, new_active_status):
+ self._setup_status(new_active_status, self.inactive_status)
+ self._prop_save_settings(old_active_status, new_active_status)
+ @_versioned_property(name="active_status",
+ doc="The allowed active bug states and their descriptions.",
+ change_hook=_set_active_status)
+ def active_status(): return {}
+
+ def _set_inactive_status(self, old_inactive_status, new_inactive_status):
+ self._setup_status(self.active_status, new_inactive_status)
+ self._prop_save_settings(old_inactive_status, new_inactive_status)
+ @_versioned_property(name="inactive_status",
+ doc="The allowed inactive bug states and their descriptions.",
+ change_hook=_set_inactive_status)
+ def inactive_status(): return {}
+
+
def __init__(self, root=None, sink_to_existing_root=True,
assert_new_BugDir=False, allow_rcs_init=False,
+ manipulate_encodings=True,
from_disk=False, rcs=None):
list.__init__(self)
- self._save_user_id = False
- self.settings = {}
+ settings_object.SavedSettingsObject.__init__(self)
+ self._manipulate_encodings = manipulate_encodings
if root == None:
root = os.getcwd()
if sink_to_existing_root == True:
@@ -110,16 +270,22 @@ class BugDir (list):
if not os.path.exists(root):
raise NoRootEntry(root)
self.root = root
+ # get a temporary rcs until we've loaded settings
+ self.sync_with_disk = False
+ self.rcs = self._guess_rcs()
+
if from_disk == True:
+ self.sync_with_disk = True
self.load()
else:
+ self.sync_with_disk = False
if assert_new_BugDir == True:
if os.path.exists(self.get_path()):
raise AlreadyInitialized, self.get_path()
if rcs == None:
rcs = self._guess_rcs(allow_rcs_init)
self.rcs = rcs
- user_id = self.rcs.get_user_id()
+ self._setup_user_id(self.user_id)
def _find_root(self, path):
"""
@@ -128,7 +294,8 @@ class BugDir (list):
"""
if not os.path.exists(path):
raise NoRootEntry(path)
- versionfile = utility.search_parent_directories(path, os.path.join(".be", "version"))
+ versionfile=utility.search_parent_directories(path,
+ os.path.join(".be", "version"))
if versionfile != None:
beroot = os.path.dirname(versionfile)
root = os.path.dirname(beroot)
@@ -139,11 +306,11 @@ class BugDir (list):
raise NoBugDir(path)
return beroot
- def get_version(self, path=None):
- if self.rcs_name == None:
- # Use a temporary RCS to check the version for the first time
+ def get_version(self, path=None, use_none_rcs=False):
+ if use_none_rcs == True:
RCS = rcs.rcs_by_name("None")
RCS.root(self.root)
+ RCS.encoding = encoding.get_encoding()
else:
RCS = self.rcs
@@ -156,57 +323,6 @@ class BugDir (list):
self.rcs.set_file_contents(self.get_path("version"),
TREE_VERSION_STRING)
- rcs_name = setting_property("rcs_name",
- ("None", "bzr", "git", "Arch", "hg"),
- doc=
-"""The name of the current RCS. Kept seperate to make saving/loading
-settings easy. Don't set this attribute. Set .rcs instead, and
-.rcs_name will be automatically adjusted.""")
-
- _rcs = None
-
- def _get_rcs(self):
- return self._rcs
-
- def _set_rcs(self, new_rcs):
- if new_rcs == None:
- new_rcs = rcs.rcs_by_name("None")
- self._rcs = new_rcs
- new_rcs.root(self.root)
- self.rcs_name = new_rcs.name
-
- rcs = property(_get_rcs, _set_rcs,
- doc="A revision control system (RCS) instance")
-
- _user_id = setting_property("user-id", doc=
-"""The user's prefered name. Kept seperate to make saving/loading
-settings easy. Don't set this attribute. Set .user_id instead,
-and ._user_id will be automatically adjusted. This setting is
-only saved if ._save_user_id == True""")
-
- def _get_user_id(self):
- if self._user_id == None and self.rcs != None:
- self._user_id = self.rcs.get_user_id()
- return self._user_id
-
- def _set_user_id(self, user_id):
- if self.rcs != None:
- self.rcs.user_id = user_id
- self._user_id = user_id
-
- user_id = property(_get_user_id, _set_user_id, doc=
-"""The user's prefered name, e.g 'John Doe <jdoe@example.com>'. Note
-that the Arch RCS backend *enforces* ids with this format.""")
-
- target = setting_property("target",
- doc="The current project development target")
-
- def save_user_id(self, user_id=None):
- if user_id == None:
- user_id = self.user_id
- self._save_user_id = True
- self.user_id = user_id
-
def get_path(self, *args):
my_dir = os.path.join(self.root, ".be")
if len(args) == 0:
@@ -224,24 +340,20 @@ that the Arch RCS backend *enforces* ids with this format.""")
if allow_rcs_init == True:
new_rcs = rcs.installed_rcs()
new_rcs.init(self.root)
- self.rcs = new_rcs
return new_rcs
def load(self):
- version = self.get_version()
+ version = self.get_version(use_none_rcs=True)
if version != TREE_VERSION_STRING:
raise NotImplementedError, \
"BugDir cannot handle version '%s' yet." % version
else:
if not os.path.exists(self.get_path()):
raise NoBugDir(self.get_path())
- self.settings = self._get_settings(self.get_path("settings"))
+ self.load_settings()
self.rcs = rcs.rcs_by_name(self.rcs_name)
- if self._user_id != None: # was a user name in the settings file
- self.save_user_id()
-
- self._bug_map_gen()
+ self._setup_user_id(self.user_id)
def load_all_bugs(self):
"Warning: this could take a while."
@@ -252,43 +364,35 @@ that the Arch RCS backend *enforces* ids with this format.""")
def save(self):
self.rcs.mkdir(self.get_path())
self.set_version()
- self._save_settings(self.get_path("settings"), self.settings)
+ self.save_settings()
self.rcs.mkdir(self.get_path("bugs"))
for bug in self:
bug.save()
+ def load_settings(self):
+ self.settings = self._get_settings(self.get_path("settings"))
+ self._setup_saved_settings()
+ self._setup_user_id(self.user_id)
+ self._setup_encoding(self.encoding)
+ self._setup_severities(self.severities)
+ self._setup_status(self.active_status, self.inactive_status)
+
def _get_settings(self, settings_path):
- if self.rcs_name == None:
- # Use a temporary RCS to loading settings the first time
- RCS = rcs.rcs_by_name("None")
- RCS.root(self.root)
- else:
- RCS = self.rcs
-
- allow_no_rcs = not RCS.path_in_root(settings_path)
+ allow_no_rcs = not self.rcs.path_in_root(settings_path)
# allow_no_rcs=True should only be for the special case of
# configuring duplicate bugdir settings
try:
- settings = mapfile.map_load(RCS, settings_path, allow_no_rcs)
+ settings = mapfile.map_load(self.rcs, settings_path, allow_no_rcs)
except rcs.NoSuchFile:
settings = {"rcs_name": "None"}
return settings
+ def save_settings(self):
+ settings = self._get_saved_settings()
+ self._save_settings(self.get_path("settings"), settings)
+
def _save_settings(self, settings_path, settings):
- this_dir_path = os.path.realpath(self.get_path("settings"))
- if os.path.realpath(settings_path) == this_dir_path:
- if not os.path.exists(self.get_path()):
- # don't save settings until the bug directory has been
- # initialized. this initialization happens the first time
- # a bug directory is saved (BugDir.save()). If the user
- # is just working with a BugDir in memory, we don't want
- # to go cluttering up his file system with settings files.
- return
- if self._save_user_id == False:
- if "user-id" in settings:
- settings = copy.copy(settings)
- del settings["user-id"]
allow_no_rcs = not self.rcs.path_in_root(settings_path)
# allow_no_rcs=True should only be for the special case of
# configuring duplicate bugdir settings
@@ -304,23 +408,17 @@ that the Arch RCS backend *enforces* ids with this format.""")
duplicate_settings = self._get_settings(duplicate_settings_path)
if "rcs_name" in duplicate_settings:
duplicate_settings["rcs_name"] = "None"
- duplicate_settings["user-id"] = self.user_id
- self._save_settings(duplicate_settings_path, duplicate_settings)
+ duplicate_settings["user_id"] = self.user_id
+ if "disabled" in bug.status_values:
+ # Hack to support old versions of BE bugs
+ duplicate_settings["inactive_status"] = self.inactive_status
+ self._save_settings(duplicate_settings_path, duplicate_settings)
- return BugDir(duplicate_path, from_disk=True)
+ return BugDir(duplicate_path, from_disk=True, manipulate_encodings=self._manipulate_encodings)
def remove_duplicate_bugdir(self):
self.rcs.remove_duplicate_repo()
- def _bug_map_gen(self):
- map = {}
- for bug in self:
- map[bug.uuid] = bug
- for uuid in self.list_uuids():
- if uuid not in map:
- map[uuid] = None
- self._bug_map = map
-
def list_uuids(self):
uuids = []
if os.path.exists(self.get_path()):
@@ -338,6 +436,7 @@ that the Arch RCS backend *enforces* ids with this format.""")
def _clear_bugs(self):
while len(self) > 0:
self.pop()
+ self._bug_map_gen()
def _load_bug(self, uuid):
bg = bug.Bug(bugdir=self, uuid=uuid, from_disk=True)
@@ -420,7 +519,8 @@ def simple_bug_dir():
"""
dir = utility.Dir()
assert os.path.exists(dir.path)
- bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True)
+ bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True,
+ manipulate_encodings=False)
bugdir._dir_ref = dir # postpone cleanup since dir.__del__() removes dir.
bug_a = bugdir.new_bug("a", summary="Bug A")
bug_a.creator = "John Doe <jdoe@example.com>"
@@ -495,6 +595,37 @@ class BugDirTestCase(unittest.TestCase):
self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime))
self.bugdir.save()
self.versionTest()
+ def testComments(self):
+ self.bugdir.new_bug(uuid="a", summary="Ant")
+ bug = self.bugdir.bug_from_uuid("a")
+ comm = bug.comment_root
+ rep = comm.new_reply("Ants are small.")
+ rep.new_reply("And they have six legs.")
+ self.bugdir.save()
+ self.bugdir._clear_bugs()
+ bug = self.bugdir.bug_from_uuid("a")
+ bug.load_comments()
+ self.failUnless(len(bug.comment_root)==1, len(bug.comment_root))
+ for index,comment in enumerate(bug.comments()):
+ if index == 0:
+ repLoaded = comment
+ self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid)
+ self.failUnless(comment.sync_with_disk == True,
+ comment.sync_with_disk)
+ #load_settings()
+ self.failUnless(comment.content_type == "text/plain",
+ comment.content_type)
+ self.failUnless(repLoaded.settings["Content-type"]=="text/plain",
+ repLoaded.settings)
+ self.failUnless(repLoaded.body == "Ants are small.",
+ repLoaded.body)
+ elif index == 1:
+ self.failUnless(comment.in_reply_to == repLoaded.uuid,
+ repLoaded.uuid)
+ self.failUnless(comment.body == "And they have six legs.",
+ comment.body)
+ else:
+ self.failIf(True, "Invalid comment: %d\n%s" % (index, comment))
unitsuite = unittest.TestLoader().loadTestsFromTestCase(BugDirTestCase)
suite = unittest.TestSuite([unitsuite])#, doctest.DocTestSuite()])
diff --git a/libbe/bzr.py b/libbe/bzr.py
index a0ae715..38af6bb 100644
--- a/libbe/bzr.py
+++ b/libbe/bzr.py
@@ -55,7 +55,7 @@ class Bzr(RCS):
pass
def _rcs_get_file_contents(self, path, revision=None):
if revision == None:
- return file(os.path.join(self.rootdir, path), "rb").read()
+ return RCS._rcs_get_file_contents(self, path, revision)
else:
status,output,error = \
self._u_invoke_client("cat","-r",revision,path)
diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py
index 6d7ab01..6be7540 100644
--- a/libbe/cmdutil.py
+++ b/libbe/cmdutil.py
@@ -16,14 +16,15 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import optparse
import os
-import locale
from textwrap import TextWrapper
from StringIO import StringIO
+import sys
import doctest
import bugdir
import plugin
-import utility
+import encoding
+
class UserError(Exception):
def __init__(self, msg):
@@ -34,6 +35,18 @@ class UserErrorWrap(UserError):
UserError.__init__(self, str(exception))
self.exception = exception
+class UsageError(Exception):
+ pass
+
+class GetHelp(Exception):
+ pass
+
+class GetCompletions(Exception):
+ def __init__(self, completions=[]):
+ msg = "Get allowed completions"
+ Exception.__init__(self, msg)
+ self.completions = completions
+
def iter_commands():
for name, module in plugin.iter_plugins("becommands"):
yield name.replace("_", "-"), module
@@ -52,9 +65,11 @@ def get_command(command_name):
raise UserError("Unknown command %s" % command_name)
return cmd
+
def execute(cmd, args):
- encoding = locale.getpreferredencoding() or 'ascii'
- return get_command(cmd).execute([a.decode(encoding) for a in args])
+ enc = encoding.get_encoding()
+ get_command(cmd).execute([a.decode(enc) for a in args])
+ return 0
def help(cmd=None):
if cmd != None:
@@ -71,24 +86,30 @@ def help(cmd=None):
ret.append("be %s%*s %s" % (name, numExtraSpaces, "", desc))
return "\n".join(ret)
-class GetHelp(Exception):
- pass
-
-
-class UsageError(Exception):
- pass
-
+def completions(cmd):
+ parser = get_command(cmd).get_parser()
+ longopts = []
+ for opt in parser.option_list:
+ longopts.append(opt.get_opt_string())
+ return longopts
def raise_get_help(option, opt, value, parser):
raise GetHelp
-
+def raise_get_completions(option, opt, value, parser):
+ print "got completion arg"
+ raise GetCompletions(completions(sys.argv[1]))
+
class CmdOptionParser(optparse.OptionParser):
def __init__(self, usage):
optparse.OptionParser.__init__(self, usage)
+ self.disable_interspersed_args()
self.remove_option("-h")
self.add_option("-h", "--help", action="callback",
callback=raise_get_help, help="Print a help message")
+ self.add_option("--complete", action="callback",
+ callback=raise_get_completions,
+ help="Print a list of available completions")
def error(self, message):
raise UsageError(message)
@@ -102,6 +123,45 @@ class CmdOptionParser(optparse.OptionParser):
self.print_help(f)
return f.getvalue()
+def option_value_pairs(options, parser):
+ """
+ Iterate through OptionParser (option, value) pairs.
+ """
+ for option in [o.dest for o in parser.option_list if o.dest != None]:
+ value = getattr(options, option)
+ yield (option, value)
+
+def default_complete(options, args, parser, bugid_args={}):
+ """
+ A dud complete implementation for becommands to that the
+ --complete argument doesn't cause any problems. Use this
+ until you've set up a command-specific complete function.
+
+ bugid_args is an optional dict where the keys are positional
+ arguments taking bug shortnames and the values are functions for
+ filtering, since that's a common enough operation.
+ e.g. for "be open [options] BUGID"
+ bugid_args = {0: lambda bug : bug.active == False}
+ """
+ for option,value in option_value_pairs(options, parser):
+ if value == "--complete":
+ raise cmdutil.GetCompletions()
+ for pos,value in enumerate(args):
+ if value == "--complete":
+ if pos in bugid_args:
+ filter = bugid_args[pos]
+ bugshortnames = []
+ try:
+ bd = bugdir.BugDir(from_disk=True,
+ manipulate_encodings=False)
+ bd.load_all_bugs()
+ bugs = [bug for bug in bd if filter(bug) == True]
+ bugshortnames = [bd.bug_shortname(bug) for bug in bugs]
+ except bugdir.NoBugDir:
+ pass
+ raise GetCompletions(bugshortnames)
+ raise GetCompletions()
+
def underlined(instring):
"""Produces a version of a string that is underlined with '='
diff --git a/libbe/comment.py b/libbe/comment.py
index 87c1de0..e5c86c7 100644
--- a/libbe/comment.py
+++ b/libbe/comment.py
@@ -23,10 +23,23 @@ import textwrap
import doctest
from beuuid import uuid_gen
+from properties import Property, doc_property, local_property, \
+ defaulting_property, checked_property, cached_property, \
+ primed_property, change_hook_property, settings_property
+import settings_object
import mapfile
from tree import Tree
import utility
+
+class InvalidShortname(KeyError):
+ def __init__(self, shortname, shortnames):
+ msg = "Invalid shortname %s\n%s" % (shortname, shortnames)
+ KeyError.__init__(self, msg)
+ self.shortname = shortname
+ self.shortnames = shortnames
+
+
INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!"
def _list_to_root(comments, bug):
@@ -45,7 +58,8 @@ def _list_to_root(comments, bug):
assert comment.uuid != None
uuid_map[comment.uuid] = comment
for comm in comments:
- if comm.in_reply_to == None:
+ rep = comm.in_reply_to
+ if rep == None or rep == settings_object.EMPTY or rep == bug.uuid:
root_comments.append(comm)
else:
parentUUID = comm.in_reply_to
@@ -55,7 +69,11 @@ def _list_to_root(comments, bug):
dummy_root.extend(root_comments)
return dummy_root
-def loadComments(bug):
+def loadComments(bug, load_full=False):
+ """
+ Set load_full=True when you want to load the comment completely
+ from disk *now*, rather than waiting and lazy loading as required.
+ """
path = bug.get_path("comments")
if not os.path.isdir(path):
return Comment(bug, uuid=INVALID_UUID)
@@ -64,6 +82,9 @@ def loadComments(bug):
if uuid.startswith('.'):
continue
comm = Comment(bug, uuid, from_disk=True)
+ if load_full == True:
+ comm.load_settings()
+ dummy = comm.body # force the body to load
comments.append(comm)
return _list_to_root(comments, bug)
@@ -73,12 +94,89 @@ def saveComments(bug):
for comment in bug.comment_root.traverse():
comment.save()
-class Comment(Tree):
+
+class Comment(Tree, settings_object.SavedSettingsObject):
+ """
+ >>> c = Comment()
+ >>> c.uuid != None
+ True
+ >>> c.uuid = "some-UUID"
+ >>> print c.content_type
+ text/plain
+ """
+
+ settings_properties = []
+ required_saved_properties = []
+ _prop_save_settings = settings_object.prop_save_settings
+ _prop_load_settings = settings_object.prop_load_settings
+ def _versioned_property(settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties,
+ **kwargs):
+ if "settings_properties" not in kwargs:
+ kwargs["settings_properties"] = settings_properties
+ if "required_saved_properties" not in kwargs:
+ kwargs["required_saved_properties"]=required_saved_properties
+ return settings_object.versioned_property(**kwargs)
+
+ @_versioned_property(name="From",
+ doc="The author of the comment")
+ def From(): return {}
+
+ @_versioned_property(name="In-reply-to",
+ doc="UUID for parent comment or bug")
+ def in_reply_to(): return {}
+
+ @_versioned_property(name="Content-type",
+ doc="Mime type for comment body",
+ default="text/plain",
+ require_save=True)
+ def content_type(): return {}
+
+ @_versioned_property(name="Date",
+ doc="An RFC 2822 timestamp for comment creation")
+ def time_string(): return {}
+
+ def _get_time(self):
+ if self.time_string == None:
+ return None
+ return utility.str_to_time(self.time_string)
+ def _set_time(self, value):
+ self.time_string = utility.time_to_str(value)
+ time = property(fget=_get_time,
+ fset=_set_time,
+ doc="An integer version of .time_string")
+
+ def _get_comment_body(self):
+ if self.rcs != None and self.sync_with_disk == True:
+ import rcs
+ return self.rcs.get_file_contents(self.get_path("body"))
+ def _set_comment_body(self, value, force=False):
+ if (self.rcs != None and self.sync_with_disk == True) or force==True:
+ assert value != None, "Can't save empty comment"
+ self.rcs.set_file_contents(self.get_path("body"), value)
+
+ @Property
+ @change_hook_property(hook=_set_comment_body)
+ @cached_property(generator=_get_comment_body)
+ @local_property("body")
+ @doc_property(doc="The meat of the comment")
+ def body(): return {}
+
+ def _get_rcs(self):
+ if hasattr(self.bug, "rcs"):
+ return self.bug.rcs
+
+ @Property
+ @cached_property(generator=_get_rcs)
+ @local_property("rcs")
+ @doc_property(doc="A revision control system instance.")
+ def rcs(): return {}
+
def __init__(self, bug=None, uuid=None, from_disk=False,
in_reply_to=None, body=None):
"""
- Set from_disk=True to load an old bug.
- Set from_disk=False to create a new bug.
+ Set from_disk=True to load an old comment.
+ Set from_disk=False to create a new comment.
The uuid option is required when from_disk==True.
@@ -89,26 +187,19 @@ class Comment(Tree):
in_reply_to should be the uuid string of the parent comment.
"""
Tree.__init__(self)
+ settings_object.SavedSettingsObject.__init__(self)
self.bug = bug
- if bug != None:
- self.rcs = bug.rcs
- else:
- self.rcs = None
+ self.uuid = uuid
if from_disk == True:
- self.uuid = uuid
- self.load()
+ self.sync_with_disk = True
else:
- if uuid != None:
- self.uuid = uuid
- else:
+ self.sync_with_disk = False
+ if uuid == None:
self.uuid = uuid_gen()
- self.time = time.time()
+ self.time = int(time.time()) # only save to second precision
if self.rcs != None:
self.From = self.rcs.get_user_id()
- else:
- self.From = None
self.in_reply_to = in_reply_to
- self.content_type = "text/plain"
self.body = body
def traverse(self, *args, **kwargs):
@@ -118,41 +209,54 @@ class Comment(Tree):
continue
yield comment
- def _clean_string(self, value):
- """
- >>> comm = Comment()
- >>> comm._clean_string(None)
- ''
- >>> comm._clean_string("abc")
- 'abc'
- """
- if value == None:
+ def _setting_attr_string(self, setting):
+ value = getattr(self, setting)
+ if value == settings_object.EMPTY:
return ""
- return value
+ else:
+ return str(value)
def xml(self, indent=0, shortname=None):
+ """
+ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
+ >>> comm.uuid = "0123"
+ >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000"
+ >>> print comm.xml(indent=2, shortname="com-1")
+ <comment>
+ <name>com-1</name>
+ <uuid>0123</uuid>
+ <from></from>
+ <date>Thu, 01 Jan 1970 00:00:00 +0000</date>
+ <body>Some
+ insightful
+ remarks</body>
+ </comment>
+ """
if shortname == None:
shortname = self.uuid
- ret = """<comment>
- <name>%s</name>
- <from>%s</from>
- <date>%s</date>
- <body>%s</body>
-</comment>\n""" % (shortname,
- self._clean_string(self.From),
- utility.time_to_str(self.time),
- self.body.rstrip('\n'))
- return ret
+ lines = ["<comment>",
+ " <name>%s</name>" % (shortname,),
+ " <uuid>%s</uuid>" % self.uuid,]
+ if self.in_reply_to != None:
+ lines.append(" <in_reply_to>%s</in_reply_to>" % self.in_reply_to)
+ lines.extend([
+ " <from>%s</from>" % self._setting_attr_string("From"),
+ " <date>%s</date>" % self.time_string,
+ " <body>%s</body>" % (self.body or "").rstrip('\n'),
+ "</comment>\n"])
+ istring = ' '*indent
+ sep = '\n' + istring
+ return istring + sep.join(lines).rstrip('\n')
def string(self, indent=0, shortname=None):
"""
>>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
- >>> comm.time = utility.str_to_time("Thu, 20 Nov 2008 15:55:11 +0000")
+ >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000"
>>> print comm.string(indent=2, shortname="com-1")
--------- Comment ---------
Name: com-1
From:
- Date: Thu, 20 Nov 2008 15:55:11 +0000
+ Date: Thu, 01 Jan 1970 00:00:00 +0000
<BLANKLINE>
Some
insightful
@@ -163,12 +267,12 @@ class Comment(Tree):
lines = []
lines.append("--------- Comment ---------")
lines.append("Name: %s" % shortname)
- lines.append("From: %s" % self._clean_string(self.From))
- lines.append("Date: %s" % utility.time_to_str(self.time))
+ lines.append("From: %s" % (self._setting_attr_string("From")))
+ lines.append("Date: %s" % self.time_string)
lines.append("")
- #lines.append(textwrap.fill(self._clean_string(self.body),
+ #lines.append(textwrap.fill(self.body or "",
# width=(79-indent)))
- lines.extend(self._clean_string(self.body).splitlines())
+ lines.extend((self.body or "").splitlines())
# some comments shouldn't be wrapped...
istring = ' '*indent
@@ -179,7 +283,7 @@ class Comment(Tree):
"""
>>> comm = Comment(bug=None, body="Some insightful remarks")
>>> comm.uuid = "com-1"
- >>> comm.time = utility.str_to_time("Thu, 20 Nov 2008 15:55:11 +0000")
+ >>> comm.time_string = "Thu, 20 Nov 2008 15:55:11 +0000"
>>> comm.From = "Jane Doe <jdoe@example.com>"
>>> print comm
--------- Comment ---------
@@ -198,64 +302,68 @@ class Comment(Tree):
assert name in ["values", "body"]
return os.path.join(my_dir, name)
- def load(self):
- map = mapfile.map_load(self.rcs, self.get_path("values"))
- self.time = utility.str_to_time(map["Date"])
- self.From = map["From"]
- self.in_reply_to = map.get("In-reply-to")
- self.content_type = map.get("Content-type", "text/plain")
- self.body = self.rcs.get_file_contents(self.get_path("body"))
+ def load_settings(self):
+ self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
+ self._setup_saved_settings()
- def save(self):
- assert self.rcs != None
- map_file = {"Date": utility.time_to_str(self.time)}
- self._add_headers(map_file, ("From", "in_reply_to", "content_type"))
+ def save_settings(self):
+ parent_dir = os.path.dirname(self.get_path())
+ self.rcs.mkdir(parent_dir)
self.rcs.mkdir(self.get_path())
- mapfile.map_save(self.rcs, self.get_path("values"), map_file)
- self.rcs.set_file_contents(self.get_path("body"), self.body)
-
- def _add_headers(self, map, names):
- map_names = {}
- for name in names:
- map_names[name] = self._pyname_to_header(name)
- self._add_attrs(map, map_names)
-
- def _pyname_to_header(self, name):
- return name.capitalize().replace('_', '-')
-
- def _add_attrs(self, map, map_names):
- for name in map_names.keys():
- value = getattr(self, name)
- if value is not None:
- map[map_names[name]] = value
+ path = self.get_path("values")
+ mapfile.map_save(self.rcs, path, self._get_saved_settings())
+
+ def save(self):
+ assert self.body != None, "Can't save blank comment"
+ #if self.in_reply_to == None:
+ # raise Exception, str(self)+'\n'+str(self.settings)+'\n'+str(self._settings_loaded)
+ #assert self.in_reply_to != None, "Comment must be a reply to something"
+ self.save_settings()
+ self._set_comment_body(self.body, force=True)
def remove(self):
for comment in self.traverse():
path = comment.get_path()
self.rcs.recursive_remove(path)
- def add_reply(self, reply):
- if reply.time != None and self.time != None:
- assert reply.time >= self.time
+ def add_reply(self, reply, allow_time_inversion=False):
if self.uuid != INVALID_UUID:
reply.in_reply_to = self.uuid
self.append(reply)
+ #raise Exception, "adding reply \n%s\n%s" % (self, reply)
def new_reply(self, body=None):
"""
>>> comm = Comment(bug=None, body="Some insightful remarks")
>>> repA = comm.new_reply("Critique original comment")
>>> repB = repA.new_reply("Begin flamewar :p")
+ >>> repB.in_reply_to == repA.uuid
+ True
"""
reply = Comment(self.bug, body=body)
self.add_reply(reply)
+ #raise Exception, "new reply added (%s),\n%s\n%s\n\t--%s--" % (body, self, reply, reply.in_reply_to)
return reply
- def string_thread(self, name_map={}, indent=0,
+ def string_thread(self, string_method_name="string", name_map={},
+ indent=0, flatten=True,
auto_name_map=False, bug_shortname=None):
"""
- Return a sting displaying a thread of comments.
+ Return a string displaying a thread of comments.
bug_shortname is only used if auto_name_map == True.
+
+ string_method_name (defaults to "string") is the name of the
+ Comment method used to generate the output string for each
+ Comment in the thread. The method must take the arguments
+ indent and shortname.
+
+ SIDE-EFFECT: if auto_name_map==True, calls comment_shortnames()
+ which will sort the tree by comment.time. Avoid by calling
+ name_map = {}
+ for shortname,comment in comm.comment_shortnames(bug_shortname):
+ name_map[comment.uuid] = shortname
+ comm.sort(key=lambda c : c.From) # your sort
+ comm.string_thread(name_map=name_map)
>>> a = Comment(bug=None, uuid="a", body="Insightful remarks")
>>> a.time = utility.str_to_time("Thu, 20 Nov 2008 01:00:00 +0000")
@@ -269,7 +377,7 @@ class Comment(Tree):
>>> 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()
+ >>> print a.string_thread(flatten=True)
--------- Comment ---------
Name: a
From:
@@ -325,33 +433,23 @@ class Comment(Tree):
for shortname,comment in self.comment_shortnames(bug_shortname):
name_map[comment.uuid] = shortname
stringlist = []
- for depth,comment in self.thread(flatten=True):
+ for depth,comment in self.thread(flatten=flatten):
ind = 2*depth+indent
if comment.uuid in name_map:
sname = name_map[comment.uuid]
else:
sname = None
- stringlist.append(comment.string(indent=ind, shortname=sname))
+ string_fn = getattr(comment, string_method_name)
+ stringlist.append(string_fn(indent=ind, shortname=sname))
return '\n'.join(stringlist)
def xml_thread(self, name_map={}, indent=0,
auto_name_map=False, bug_shortname=None):
- if auto_name_map == True:
- name_map = {}
- for shortname,comment in self.comment_shortnames(bug_shortname):
- name_map[comment.uuid] = shortname
- stringlist = []
- for depth,comment in self.thread(flatten=True):
- ind = 2*depth+indent
- if comment.uuid in name_map:
- sname = name_map[comment.uuid]
- else:
- sname = None
- stringlist.append(comment.xml(indent=ind, shortname=sname))
- return '\n'.join(stringlist)
-
+ return self.string_thread(string_method_name="xml", name_map=name_map,
+ indent=indent, auto_name_map=auto_name_map,
+ bug_shortname=bug_shortname)
- def comment_shortnames(self, bug_shortname=""):
+ def comment_shortnames(self, bug_shortname=None):
"""
Iterate through (id, comment) pairs, in time order.
(This is a user-friendly id, not the comment uuid).
@@ -372,6 +470,8 @@ class Comment(Tree):
bug-1:3 c
bug-1:4 d
"""
+ if bug_shortname == None:
+ bug_shortname = ""
self.sort(key=lambda comm : comm.time)
for num,comment in enumerate(self.traverse()):
yield ("%s:%d" % (bug_shortname, num+1), comment)
@@ -393,7 +493,8 @@ class Comment(Tree):
for cur_name, comment in self.comment_shortnames(*args, **kwargs):
if comment_shortname == cur_name:
return comment
- raise KeyError(comment_shortname)
+ raise InvalidShortname(comment_shortname,
+ list(self.comment_shortnames(*args, **kwargs)))
def comment_from_uuid(self, uuid):
"""
diff --git a/libbe/config.py b/libbe/config.py
index 79c0d6f..94c700e 100644
--- a/libbe/config.py
+++ b/libbe/config.py
@@ -15,30 +15,40 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import ConfigParser
+import codecs
+import locale
import os.path
+import sys
import doctest
+default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding()
+
def path():
"""Return the path to the per-user config file"""
return os.path.expanduser("~/.bugs_everywhere")
-def set_val(name, value, section="DEFAULT"):
+def set_val(name, value, section="DEFAULT", encoding=None):
"""Set a value in the per-user config file
:param name: The name of the value to set
:param value: The new value to set (or None to delete the value)
:param section: The section to store the name/value in
"""
+ if encoding == None:
+ encoding = default_encoding
config = ConfigParser.ConfigParser()
- config.read(path())
+ 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)
- config.write(file(path(), "wb"))
- pass
+ f = codecs.open(path(), "w", encoding)
+ config.write(f)
+ f.close()
-def get_val(name, section="DEFAULT"):
+def get_val(name, section="DEFAULT", encoding=None):
"""
Get a value from the per-user config file
@@ -49,13 +59,17 @@ def get_val(name, section="DEFAULT"):
True
>>> set_val("junk", "random")
>>> get_val("junk")
- 'random'
+ u'random'
>>> set_val("junk", None)
>>> get_val("junk") is None
True
"""
+ if encoding == None:
+ encoding = default_encoding
config = ConfigParser.ConfigParser()
- config.read(path())
+ f = codecs.open(path(), "r", encoding)
+ config.readfp(f, path())
+ f.close()
try:
return config.get(section, name)
except ConfigParser.NoOptionError:
diff --git a/libbe/diff.py b/libbe/diff.py
index 86a91ca..17d6c50 100644
--- a/libbe/diff.py
+++ b/libbe/diff.py
@@ -15,9 +15,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Compare two bug trees"""
-from libbe import cmdutil, bugdir
+from libbe import cmdutil, bugdir, bug
from libbe.utility import time_to_str
-from libbe.bug import cmp_severity
import doctest
def diff(old_bugdir, new_bugdir):
@@ -41,17 +40,18 @@ def diff(old_bugdir, new_bugdir):
def diff_report(diff_data, bug_dir):
(removed, modified, added) = diff_data
def modified_cmp(left, right):
- return cmp_severity(left[1], right[1])
+ return bug.cmp_severity(left[1], right[1])
- added.sort(cmp_severity)
- removed.sort(cmp_severity)
+ added.sort(bug.cmp_severity)
+ removed.sort(bug.cmp_severity)
modified.sort(modified_cmp)
-
+ lines = []
+
if len(added) > 0:
- print "New bug reports:"
- for bug in added:
- print bug.string(shortlist=True)
- print ""
+ lines.append("New bug reports:")
+ for bg in added:
+ lines.extend(bg.string(shortlist=True).splitlines())
+ lines.append("")
if len(modified) > 0:
printed = False
@@ -61,15 +61,18 @@ def diff_report(diff_data, bug_dir):
continue
if not printed:
printed = True
- print "Modified bug reports:"
- print change_str
- print ""
+ lines.append("Modified bug reports:")
+ lines.extend(change_str.splitlines())
+ if printed == True:
+ lines.append("")
- if len(removed) > 0:
- print "Removed bug reports:"
- for bug in removed:
- print bug.string(shortlist=True)
- print ""
+ if len(removed) > 0:
+ lines.append("Removed bug reports:")
+ for bg in removed:
+ lines.extend(bg.string(shortlist=True).splitlines())
+ lines.append("")
+
+ return '\n'.join(lines)
def change_lines(old, new, attributes):
change_list = []
diff --git a/libbe/editor.py b/libbe/editor.py
new file mode 100644
index 0000000..4a63e5c
--- /dev/null
+++ b/libbe/editor.py
@@ -0,0 +1,103 @@
+# Bugs Everywhere, a distributed bugtracker
+# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
+# <abentley@panoramicfeedback.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301, USA
+
+import codecs
+import locale
+import os
+import sys
+import tempfile
+import doctest
+
+default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding()
+
+comment_marker = u"== Anything below this line will be ignored\n"
+
+class CantFindEditor(Exception):
+ def __init__(self):
+ Exception.__init__(self, "Can't find editor to get string from")
+
+def editor_string(comment=None, encoding=None):
+ """Invokes the editor, and returns the user_produced text as a string
+
+ >>> if "EDITOR" in os.environ:
+ ... del os.environ["EDITOR"]
+ >>> if "VISUAL" in os.environ:
+ ... del os.environ["VISUAL"]
+ >>> editor_string()
+ Traceback (most recent call last):
+ CantFindEditor: Can't find editor to get string from
+ >>> os.environ["EDITOR"] = "echo bar > "
+ >>> editor_string()
+ u'bar\\n'
+ >>> os.environ["VISUAL"] = "echo baz > "
+ >>> editor_string()
+ u'baz\\n'
+ >>> del os.environ["EDITOR"]
+ >>> del os.environ["VISUAL"]
+ """
+ if encoding == None:
+ encoding = default_encoding
+ for name in ('VISUAL', 'EDITOR'):
+ try:
+ editor = os.environ[name]
+ break
+ except KeyError:
+ pass
+ else:
+ raise CantFindEditor()
+ fhandle, fname = tempfile.mkstemp()
+ try:
+ if comment is not None:
+ os.write(fhandle, '\n'+comment_string(comment))
+ os.close(fhandle)
+ oldmtime = os.path.getmtime(fname)
+ os.system("%s %s" % (editor, fname))
+ f = codecs.open(fname, "r", encoding)
+ output = trimmed_string(f.read())
+ f.close()
+ if output.rstrip('\n') == "":
+ output = None
+ finally:
+ os.unlink(fname)
+ return output
+
+
+def comment_string(comment):
+ """
+ >>> comment_string('hello') == comment_marker+"hello"
+ True
+ """
+ return comment_marker + comment
+
+
+def trimmed_string(instring):
+ """
+ >>> trimmed_string("hello\\n"+comment_marker)
+ u'hello\\n'
+ >>> trimmed_string("hi!\\n" + comment_string('Booga'))
+ u'hi!\\n'
+ """
+ out = []
+ for line in instring.splitlines(True):
+ if line.startswith(comment_marker):
+ break
+ out.append(line)
+ return ''.join(out)
+
+suite = doctest.DocTestSuite()
diff --git a/libbe/encoding.py b/libbe/encoding.py
new file mode 100644
index 0000000..7f924eb
--- /dev/null
+++ b/libbe/encoding.py
@@ -0,0 +1,53 @@
+# Bugs Everywhere, a distributed bugtracker
+# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
+# <abentley@panoramicfeedback.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301, USA
+import codecs
+import locale
+import sys
+import doctest
+
+def get_encoding():
+ """
+ Guess a useful input/output/filesystem encoding... Maybe we need
+ seperate encodings for input/output and filesystem? Hmm...
+ """
+ encoding = locale.getpreferredencoding() or sys.getdefaultencoding()
+ if sys.platform != 'win32' or sys.version_info[:2] > (2, 3):
+ encoding = locale.getlocale(locale.LC_TIME)[1] or encoding
+ # Python 2.3 on windows doesn't know about 'XYZ' alias for 'cpXYZ'
+ return encoding
+
+def known_encoding(encoding):
+ """
+ >>> known_encoding("highly-unlikely-encoding")
+ False
+ >>> known_encoding(get_encoding())
+ True
+ """
+ try:
+ codecs.lookup(encoding)
+ return True
+ except LookupError:
+ return False
+
+def set_IO_stream_encodings(encoding):
+ sys.stdin = codecs.getreader(encoding)(sys.__stdin__)
+ sys.stdout = codecs.getwriter(encoding)(sys.__stdout__)
+ sys.stderr = codecs.getwriter(encoding)(sys.__stderr__)
+
+suite = doctest.DocTestSuite()
diff --git a/libbe/git.py b/libbe/git.py
index 046e72e..e57014f 100644
--- a/libbe/git.py
+++ b/libbe/git.py
@@ -69,7 +69,7 @@ class Git(RCS):
self._rcs_add(path)
def _rcs_get_file_contents(self, path, revision=None):
if revision == None:
- return file(self._u_abspath(path), "rb").read()
+ return RCS._rcs_get_file_contents(self, path, revision)
else:
arg = "%s:%s" % (revision,path)
status,output,error = self._u_invoke_client("show", arg)
@@ -85,7 +85,7 @@ class Git(RCS):
status,output,error = self._u_invoke_client('commit', '-a',
'-F', commitfile)
revision = None
- revline = re.compile("Created (.*)commit (.*):(.*)")
+ revline = re.compile("(.*) (.*)[:\]] (.*)")
match = revline.search(output)
assert match != None, output+error
assert len(match.groups()) == 3
diff --git a/libbe/hg.py b/libbe/hg.py
index 27cbb79..c00d7e2 100644
--- a/libbe/hg.py
+++ b/libbe/hg.py
@@ -58,14 +58,14 @@ class Hg(RCS):
pass
def _rcs_get_file_contents(self, path, revision=None):
if revision == None:
- return file(os.path.join(self.rootdir, path), "rb").read()
+ return RCS._rcs_get_file_contents(self, path, revision)
else:
status,output,error = \
self._u_invoke_client("cat","-r",revision,path)
return output
def _rcs_duplicate_repo(self, directory, revision=None):
if revision == None:
- RCS._rcs_duplicate_repo(self, directory, revision)
+ return RCS._rcs_duplicate_repo(self, directory, revision)
else:
self._u_invoke_client("archive", "--rev", revision, directory)
def _rcs_commit(self, commitfile):
diff --git a/libbe/mapfile.py b/libbe/mapfile.py
index 559d713..c36d454 100644
--- a/libbe/mapfile.py
+++ b/libbe/mapfile.py
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+import yaml
import os.path
import errno
import utility
@@ -29,17 +30,20 @@ class IllegalValue(Exception):
Exception.__init__(self, 'Illegal value "%s"' % value)
self.value = value
-def generate(map, context=3):
- """Generate a format-2 mapfile content string. This is a simpler
- format, but should merge better, because there's no chance of
- confusion for appends, and lines are unique for both key and
- value.
-
+def generate(map):
+ """Generate a YAML mapfile content string.
>>> generate({"q":"p"})
- '\\n\\n\\nq=p\\n\\n\\n\\n'
+ '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"
@@ -53,7 +57,6 @@ def generate(map, context=3):
Traceback (most recent call last):
IllegalValue: Illegal value "p\\n"
"""
- assert(context > 0)
keys = map.keys()
keys.sort()
for key in keys:
@@ -61,6 +64,7 @@ def generate(map, context=3):
assert not key.startswith('>')
assert('\n' not in key)
assert('=' not in key)
+ assert(':' not in key)
assert(len(key) > 0)
except AssertionError:
raise IllegalKey(key.encode('string_escape'))
@@ -69,20 +73,19 @@ def generate(map, context=3):
lines = []
for key in keys:
- for i in range(context):
- lines.append("")
- lines.append("%s=%s" % (key, map[key]))
- for i in range(context):
- lines.append("")
- return '\n'.join(lines) + '\n'
+ 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 format-2 mapfile string.
- >>> parse('\\n\\n\\nq=p\\n\\n\\n\\n')['q']
+ Parse a YAML mapfile string.
+ >>> parse('q: p\\n\\n')['q']
+ 'p'
+ >>> parse('q: \\'p\\'\\n\\n')['q']
'p'
- >>> parse('\\n\\nq=\\'p\\'\\n\\n\\n\\n')['q']
- "\'p\'"
>>> contents = generate({"a":"b", "c":"d", "e":"f"})
>>> dict = parse(contents)
>>> dict["a"]
@@ -92,15 +95,25 @@ def parse(contents):
>>> dict["e"]
'f'
"""
- result = {}
+ old_format = False
for line in contents.splitlines():
- line = line.rstrip('\n')
- if len(line) == 0:
- continue
- name,value = [field for field in line.split('=', 1)]
- assert not result.has_key(name)
- result[name] = value
- return result
+ if len(line.split("=")) == 2:
+ old_format = True
+ break
+ if old_format: # translate to YAML. Hack to deal with old BE bugs.
+ newlines = []
+ for line in contents.splitlines():
+ line = line.rstrip('\n')
+ if len(line) == 0:
+ continue
+ fields = line.split("=")
+ if len(fields) == 2:
+ key,value = fields
+ newlines.append('%s: "%s"' % (key, value.replace('"','\\"')))
+ else:
+ newlines.append(line)
+ contents = '\n'.join(newlines)
+ return yaml.load(contents)
def map_save(rcs, path, map, allow_no_rcs=False):
"""Save the map as a mapfile to the specified path"""
diff --git a/libbe/properties.py b/libbe/properties.py
new file mode 100644
index 0000000..a8e89fb
--- /dev/null
+++ b/libbe/properties.py
@@ -0,0 +1,479 @@
+# Bugs Everywhere - a distributed bugtracker
+# Copyright (C) 2008 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+This module provides a series of useful decorators for defining
+various types of properties. For example usage, consider the
+unittests at the end of the module.
+
+See
+ http://www.python.org/dev/peps/pep-0318/
+and
+ http://www.phyast.pitt.edu/~micheles/python/documentation.html
+for more information on decorators.
+"""
+
+import unittest
+
+
+class ValueCheckError (ValueError):
+ def __init__(self, name, value, allowed):
+ msg = "%s not in %s for %s" % (value, 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):
+ """
+ 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.
+ """
+ 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 = getattr(self, "_%s_value" % name, 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
+
+def defaulting_property(default=None, null=None):
+ """
+ Define a default value for get access to a property.
+ If the stored value is null, then default is returned.
+ """
+ def decorator(funcs):
+ if hasattr(funcs, "__call__"):
+ funcs = funcs()
+ fget = funcs.get("fget")
+ def _fget(self):
+ value = fget(self)
+ if value == null:
+ return default
+ return value
+ funcs["fget"] = _fget
+ 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):
+ """
+ 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,
+ whiose 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.
+ """
+ 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):
+ cache = getattr(self, "_%s_cache" % name, True)
+ value = fget(self)
+ if cache == True:
+ if value == initVal:
+ 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:
+ if value == initVal:
+ value = generator(self)
+ return value
+ funcs["fget"] = _fget
+ return funcs
+ return decorator
+
+def primed_property(primer, initVal=None):
+ """
+ Just like a generator_property, except that instead of returning a
+ new value and running fset to cache it, the primer performs some
+ background manipulation (e.g. loads data into instance.settings)
+ such that a _second_ pass through fget succeeds.
+
+ The 'cache' flag becomes a 'prime' flag, with priming taking place
+ whenever ._<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)
+ return value
+ funcs["fget"] = _fget
+ return funcs
+ return decorator
+
+def change_hook_property(hook):
+ """
+ Call the function hook(instance, old_value, new_value) whenever a
+ value different from the current value is set (instance is a a
+ reference to the class instance to which this property belongs).
+ This is useful for saving changes to disk, etc.
+ """
+ def decorator(funcs):
+ if hasattr(funcs, "__call__"):
+ funcs = funcs()
+ fget = funcs.get("fget")
+ fset = funcs.get("fset")
+ name = funcs.get("name", "<unknown>")
+ def _fset(self, value):
+ old_value = fget(self)
+ if value != old_value:
+ hook(self, old_value, value)
+ fset(self, value)
+ funcs["fset"] = _fset
+ return funcs
+ return decorator
+
+
+class DecoratorTests(unittest.TestCase):
+ def testLocalDoc(self):
+ class Test(object):
+ @Property
+ @doc_property("A fancy property")
+ def x():
+ return {}
+ self.failUnless(Test.x.__doc__ == "A fancy property",
+ Test.x.__doc__)
+ def testLocalProperty(self):
+ class Test(object):
+ @Property
+ @local_property(name="LOCAL")
+ def x():
+ return {}
+ t = Test()
+ self.failUnless(t.x == None, str(t.x))
+ t.x = 'z' # the first set initializes ._LOCAL_value
+ self.failUnless(t.x == 'z', str(t.x))
+ self.failUnless("_LOCAL_value" in dir(t), dir(t))
+ self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value)
+ def testSettingsProperty(self):
+ class Test(object):
+ @Property
+ @settings_property(name="attr")
+ def x():
+ return {}
+ def __init__(self):
+ self.settings = {}
+ t = Test()
+ self.failUnless(t.x == None, str(t.x))
+ t.x = 'z' # the first set initializes ._LOCAL_value
+ self.failUnless(t.x == 'z', str(t.x))
+ self.failUnless("attr" in t.settings, t.settings)
+ self.failUnless(t.settings["attr"] == 'z', t.settings["attr"])
+ def testDefaultingLocalProperty(self):
+ class Test(object):
+ @Property
+ @defaulting_property(default='y', null='x')
+ @local_property(name="DEFAULT")
+ def x(): return {}
+ t = Test()
+ self.failUnless(t.x == None, 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))
+ def testCheckedLocalProperty(self):
+ class Test(object):
+ @Property
+ @checked_property(allowed=['x', 'y', 'z'])
+ @local_property(name="CHECKED")
+ def x(): return {}
+ def __init__(self):
+ self._CHECKED_value = 'x'
+ t = Test()
+ self.failUnless(t.x == 'x', str(t.x))
+ try:
+ t.x = None
+ e = None
+ except ValueCheckError, e:
+ pass
+ self.failUnless(type(e) == ValueCheckError, type(e))
+ def testTwoCheckedLocalProperties(self):
+ class Test(object):
+ @Property
+ @checked_property(allowed=['x', 'y', 'z'])
+ @local_property(name="X")
+ def x(): return {}
+
+ @Property
+ @checked_property(allowed=['a', 'b', 'c'])
+ @local_property(name="A")
+ def a(): return {}
+ def __init__(self):
+ self._A_value = 'a'
+ self._X_value = 'x'
+ t = Test()
+ try:
+ t.x = 'a'
+ e = None
+ except ValueCheckError, e:
+ pass
+ self.failUnless(type(e) == ValueCheckError, type(e))
+ t.x = 'x'
+ t.x = 'y'
+ t.x = 'z'
+ try:
+ t.a = 'x'
+ e = None
+ except ValueCheckError, e:
+ pass
+ self.failUnless(type(e) == ValueCheckError, type(e))
+ t.a = 'a'
+ t.a = 'b'
+ t.a = 'c'
+ def testFnCheckedLocalProperty(self):
+ class Test(object):
+ @Property
+ @fn_checked_property(lambda v : v in ['x', 'y', 'z'])
+ @local_property(name="CHECKED")
+ def x(): return {}
+ def __init__(self):
+ self._CHECKED_value = 'x'
+ t = Test()
+ self.failUnless(t.x == 'x', str(t.x))
+ try:
+ t.x = None
+ e = None
+ except ValueCheckError, e:
+ pass
+ self.failUnless(type(e) == ValueCheckError, type(e))
+ def testCachedLocalProperty(self):
+ class Gen(object):
+ def __init__(self):
+ self.i = 0
+ def __call__(self, owner):
+ self.i += 1
+ return self.i
+ class Test(object):
+ @Property
+ @cached_property(generator=Gen(), initVal=None)
+ @local_property(name="CACHED")
+ def x(): return {}
+ t = Test()
+ self.failIf("_CACHED_cache" in dir(t), getattr(t, "_CACHED_cache", None))
+ self.failUnless(t.x == 1, t.x)
+ self.failUnless(t.x == 1, t.x)
+ self.failUnless(t.x == 1, t.x)
+ t.x = 8
+ self.failUnless(t.x == 8, t.x)
+ self.failUnless(t.x == 8, t.x)
+ t._CACHED_cache = False # Caching is off, but the stored value
+ val = t.x # is 8, not the initVal (None), so we
+ self.failUnless(val == 8, val) # get 8.
+ t._CACHED_value = None # Now we've set the stored value to None
+ val = t.x # so future calls to fget (like this)
+ self.failUnless(val == 2, val) # will call the generator every time...
+ val = t.x
+ self.failUnless(val == 3, val)
+ val = t.x
+ self.failUnless(val == 4, val)
+ t._CACHED_cache = True # We turn caching back on, and get
+ self.failUnless(t.x == 1, str(t.x)) # the original cached value.
+ del t._CACHED_cached_value # Removing that value forces a
+ self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call
+ self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which
+ self.failUnless(t.x == 5, str(t.x)) # we get the new cached value.
+ def testPrimedLocalProperty(self):
+ class Test(object):
+ def prime(self):
+ self.settings["PRIMED"] = "initialized"
+ @Property
+ @primed_property(primer=prime, initVal=None)
+ @settings_property(name="PRIMED")
+ def x(): return {}
+ def __init__(self):
+ self.settings={}
+ t = Test()
+ self.failIf("_PRIMED_prime" in dir(t), getattr(t, "_PRIMED_prime", None))
+ self.failUnless(t.x == "initialized", t.x)
+ t.x = 1
+ self.failUnless(t.x == 1, t.x)
+ t.x = None
+ self.failUnless(t.x == "initialized", t.x)
+ t._PRIMED_prime = True
+ t.x = 3
+ self.failUnless(t.x == "initialized", t.x)
+ t._PRIMED_prime = False
+ t.x = 3
+ self.failUnless(t.x == 3, t.x)
+ def testChangeHookLocalProperty(self):
+ class Test(object):
+ def _hook(self, old, new):
+ self.old = old
+ self.new = new
+
+ @Property
+ @change_hook_property(_hook)
+ @local_property(name="HOOKED")
+ def x(): return {}
+ t = Test()
+ t.x = 1
+ self.failUnless(t.old == None, t.old)
+ self.failUnless(t.new == 1, t.new)
+ t.x = 1
+ self.failUnless(t.old == None, t.old)
+ self.failUnless(t.new == 1, t.new)
+ t.x = 2
+ self.failUnless(t.old == 1, t.old)
+ self.failUnless(t.new == 2, t.new)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests)
+
diff --git a/libbe/rcs.py b/libbe/rcs.py
index 3519c3d..786f9dd 100644
--- a/libbe/rcs.py
+++ b/libbe/rcs.py
@@ -15,13 +15,14 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from subprocess import Popen, PIPE
+import codecs
import os
import os.path
-from socket import gethostname
import re
+from socket import gethostname
+import shutil
import sys
import tempfile
-import shutil
import unittest
import doctest
@@ -77,8 +78,9 @@ class PathNotInRoot(Exception):
self.root = root
class NoSuchFile(Exception):
- def __init__(self, pathname):
- Exception.__init__(self, "No such file: %s" % pathname)
+ def __init__(self, pathname, root="."):
+ path = os.path.abspath(os.path.join(root, pathname))
+ Exception.__init__(self, "No such file: %s" % path)
def new():
@@ -97,12 +99,13 @@ class RCS(object):
name = "None"
client = "" # command-line tool for _u_invoke_client
versioned = False
- def __init__(self, paranoid=False):
+ def __init__(self, paranoid=False, encoding=sys.getdefaultencoding()):
self.paranoid = paranoid
self.verboseInvoke = False
self.rootdir = None
self._duplicateBasedir = None
self._duplicateDirname = None
+ self.encoding = encoding
def __del__(self):
self.cleanup()
@@ -171,15 +174,15 @@ class RCS(object):
pass
def _rcs_get_file_contents(self, path, revision=None):
"""
- Get the file contents as they were in a given revision. Don't
- worry about decoding the contents, the RCS.get_file_contents()
- method will handle that.
-
+ Get the file contents as they were in a given revision.
Revision==None specifies the current revision.
"""
assert revision == None, \
"The %s RCS does not support revision specifiers" % self.name
- return file(os.path.join(self.rootdir, path), "rb").read()
+ f = codecs.open(os.path.join(self.rootdir, path), "r", self.encoding)
+ contents = f.read()
+ f.close()
+ return contents
def _rcs_duplicate_repo(self, directory, revision=None):
"""
Get the repository as it was in a given revision.
@@ -297,14 +300,18 @@ class RCS(object):
relpath = self._u_rel_path(path)
contents = self._rcs_get_file_contents(relpath,revision)
else:
- contents = file(path, "rb").read()
- return contents.decode("utf-8")
+ f = codecs.open(path, "r", self.encoding)
+ contents = f.read()
+ f.close()
+ return contents
def set_file_contents(self, path, contents, allow_no_rcs=False):
"""
Set the file contents under version control.
"""
add = not os.path.exists(path)
- file(path, "wb").write(contents.encode("utf-8"))
+ f = codecs.open(path, "w", self.encoding)
+ f.write(contents)
+ f.close()
if self._use_rcs(path, allow_no_rcs):
if add:
@@ -537,13 +544,13 @@ class RCS(object):
Split the commitfile created in self.commit() back into
summary and header lines.
"""
- f = file(commitfile, "rb")
+ f = codecs.open(commitfile, "r", self.encoding)
summary = f.readline()
body = f.read()
body.lstrip('\n')
if len(body) == 0:
body = None
- f.close
+ f.close()
return (summary, body)
diff --git a/libbe/restconvert.py b/libbe/restconvert.py
deleted file mode 100644
index 57148e4..0000000
--- a/libbe/restconvert.py
+++ /dev/null
@@ -1,130 +0,0 @@
-# Copyright (C) 2005, 2006 Aaron Bentley and Panometrics, Inc.
-# <abentley@panoramicfeedback.com>
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
-import re
-from StringIO import StringIO
-from docutils import nodes
-from docutils.statemachine import StringList
-from docutils.core import publish_file
-from docutils.parsers import rst
-from docutils.parsers.rst import directives
-from docutils.parsers.rst.states import Inliner, MarkupMismatch, unescape
-try :
- from xml.etree import ElementTree # Python 2.5 (and greater?)
-except ImportError :
- from elementtree import ElementTree
-import doctest
-
-def rest_xml(rest):
- warnings = StringIO()
- parser = rst.Parser(inliner=HelpLinkInliner())
- xmltext = publish_file(rest, writer_name="html", parser=parser,
- settings_overrides={"warning_stream": warnings,
- "halt_level": 5})
- warnings.seek(0)
- return ElementTree.parse(StringIO(xmltext)).getroot(), warnings.read()
-
-class HelpLinkInliner(Inliner):
- def __init__(self, roles=None):
- Inliner.__init__(self, roles)
- regex = re.compile('\[([^|]*)\|([^]]*)\]')
- self.implicit_dispatch.append((regex, self.help_reference))
-
- def parse(self, *args, **kwargs):
- self.more_messages = []
- nodes, messages = Inliner.parse(self, *args, **kwargs)
- return nodes, (messages + self.more_messages)
-
- def help_reference(self, match, lineno):
- from wizardhelp.controllers import iter_help_pages
- text,link = match.groups()
- rawtext = match.group(0)
- text, link, rawtext = [unescape(f, 1) for f in (text, link, rawtext)]
- if link not in list(iter_help_pages()):
- msg = self.reporter.warning('Broken link to "%s".' % link,
- line=lineno)
- self.more_messages.append(msg)
- ref = "/help/%s/" % link
- unescaped = text
- node = nodes.reference(rawtext, text, refuri=ref)
- node.set_class("helplink")
- return [node]
-
-
-def rst_directive(name=None, required_args=0, optional_args=0,
- final_arg_ws=False, options=None, content='forbidden'):
- """Decorator that simplifies creating ReST directives
-
- All arguments are optional. Name is, by default, determined from the
- function name.
-
- The possible values for content are 'forbidden', 'allowed' (but not
- required), and 'required' (a warning will be generated if not present).
- """
- content_rules = {'forbidden': (False, False), 'allowed': (True, False),
- 'required': (True, True)}
- content_allowed, content_required = content_rules[content]
-
- def decorator_factory(func):
- my_name = name
- if my_name is None:
- my_name = func.__name__
-
- def decorator(name, arguments, options, content, lineno,
- content_offset, block_text, state, state_machine):
- warn = state_machine.reporter.warning
- if not content and content_required:
- warn = state_machine.reporter.warning
- warning = warn('%s is empty' % my_name,
- nodes.literal_block(block_text, block_text),
- line=lineno)
- return [warning]
- return func(name, arguments, options, content, lineno,
- content_offset, block_text, state, state_machine)
-
- decorator.arguments = (required_args, optional_args, final_arg_ws)
- decorator.options = options
- decorator.content = content_allowed
- directives.register_directive(my_name, decorator)
- return decorator
- return decorator_factory
-
-
-@rst_directive(required_args=1, final_arg_ws=True, content='required')
-def foldout(name, arguments, options, content, lineno, content_offset,
- block_text, state, state_machine):
- """\
- Generate a foldout section.
-
- On the ReST side, this merely involves marking the items with suitable
- classes. A Kid match rule will be used to insert the appropriate
- Javascript magic.
- """
- text = '\n'.join(content)
- foldout_title = nodes.paragraph([arguments[0]])
- foldout_title.set_class('foldout-title')
- state.nested_parse(StringList([arguments[0]]), 0, foldout_title)
- foldout_body = nodes.compound(text)
- foldout_body.set_class('foldout-body')
- state.nested_parse(content, content_offset, foldout_body)
- foldout = nodes.compound(text)
- foldout += foldout_title
- foldout += foldout_body
- foldout.set_class('foldout')
- return [foldout]
-
-suite = doctest.DocTestSuite()
diff --git a/libbe/settings_object.py b/libbe/settings_object.py
new file mode 100644
index 0000000..1df3e6b
--- /dev/null
+++ b/libbe/settings_object.py
@@ -0,0 +1,353 @@
+# Bugs Everywhere - a distributed bugtracker
+# Copyright (C) 2008 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+This module provides a base class implementing settings-dict based
+property storage useful for BE objects with saved properties
+(e.g. BugDir, Bug, Comment). For example usage, consider the
+unittests at the end of the module.
+"""
+
+import doctest
+import unittest
+
+from properties import Property, doc_property, local_property, \
+ defaulting_property, checked_property, fn_checked_property, \
+ cached_property, primed_property, change_hook_property, \
+ settings_property
+
+
+class _Token (object):
+ """
+ `Control' value class for properties. We want values that only
+ mean something to the settings_object module.
+ """
+ pass
+
+class UNPRIMED (_Token):
+ "Property has not been primed."
+ pass
+
+class EMPTY (_Token):
+ """
+ Property has been primed but has no user-set value, so use
+ default/generator value.
+ """
+ pass
+
+
+def prop_save_settings(self, old, new):
+ """
+ The default action undertaken when a property changes.
+ """
+ if self.sync_with_disk==True:
+ self.save_settings()
+
+def prop_load_settings(self):
+ """
+ The default action undertaken when an UNPRIMED property is accessed.
+ """
+ if self.sync_with_disk==True and self._settings_loaded==False:
+ self.load_settings()
+ else:
+ self._setup_saved_settings(flag_as_loaded=False)
+
+# Some name-mangling routines for pretty printing setting names
+def setting_name_to_attr_name(self, name):
+ """
+ Convert keys to the .settings dict into their associated
+ SavedSettingsObject attribute names.
+ >>> print setting_name_to_attr_name(None,"User-id")
+ user_id
+ """
+ return name.lower().replace('-', '_')
+
+def attr_name_to_setting_name(self, name):
+ """
+ The inverse of setting_name_to_attr_name.
+ >>> print attr_name_to_setting_name(None, "user_id")
+ User-id
+ """
+ return name.capitalize().replace('_', '-')
+
+
+def versioned_property(name, doc,
+ default=None, generator=None,
+ change_hook=prop_save_settings,
+ primer=prop_load_settings,
+ allowed=None, check_fn=None,
+ settings_properties=[],
+ required_saved_properties=[],
+ require_save=False):
+ """
+ Combine the common decorators in a single function.
+
+ Use zero or one (but not both) of default or generator, since a
+ working default will keep the generator from functioning. Use the
+ default if you know what you want the default value to be at
+ 'coding time'. Use the generator if you can write a function to
+ determine a valid default at run time. If both default and
+ generator are None, then the property will be a defaulting
+ property which defaults to None.
+
+ allowed and check_fn have a similar relationship, although you can
+ use both of these if you want. allowed compares the proposed
+ value against a list determined at 'coding time' and check_fn
+ allows more flexible comparisons to take place at run time.
+
+ Set require_save to True if you want to save the default/generated
+ value for a property, to protect against future changes. E.g., we
+ currently expect all comments to be 'text/plain' but in the future
+ we may want to default to 'text/html'. If we don't want the old
+ comments to be interpreted as 'text/html', we would require that
+ the content type be saved.
+
+ change_hook, primer, settings_properties, and
+ required_saved_properties are only options to get their defaults
+ into our local scope. Don't mess with them.
+ """
+ settings_properties.append(name)
+ if require_save == True:
+ required_saved_properties.append(name)
+ def decorator(funcs):
+ fulldoc = doc
+ if default != None:
+ defaulting = defaulting_property(default=default, null=EMPTY)
+ fulldoc += "\n\nThis property defaults to %s" % default
+ if generator != None:
+ cached = cached_property(generator=generator, initVal=EMPTY)
+ 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)
+ primed = primed_property(primer=primer, initVal=UNPRIMED)
+ settings = settings_property(name=name, null=UNPRIMED)
+ docp = doc_property(doc=fulldoc)
+ deco = hooked(primed(settings(docp(funcs))))
+ if default != None:
+ deco = defaulting(deco)
+ if generator != None:
+ deco = cached(deco)
+ if default != None:
+ deco = defaulting(deco)
+ if allowed != None:
+ deco = checked(deco)
+ if check_fn != None:
+ deco = fn_checked(deco)
+ return Property(deco)
+ return decorator
+
+class SavedSettingsObject(object):
+
+ # Keep a list of properties that may be stored in the .settings dict.
+ #settings_properties = []
+
+ # A list of properties that we save to disk, even if they were
+ # never set (in which case we save the default value). This
+ # protects against future changes in default values.
+ #required_saved_properties = []
+
+ _setting_name_to_attr_name = setting_name_to_attr_name
+ _attr_name_to_setting_name = attr_name_to_setting_name
+
+ def __init__(self):
+ self._settings_loaded = False
+ self.sync_with_disk = False
+ self.settings = {}
+
+ def load_settings(self):
+ """Load the settings from disk."""
+ # Override. Must call ._setup_saved_settings() after loading.
+ self.settings = {}
+ self._setup_saved_settings()
+
+ def _setup_saved_settings(self, flag_as_loaded=True):
+ """
+ To be run after setting self.settings up from disk. Marks all
+ settings as primed.
+ """
+ for property in self.settings_properties:
+ if property not in self.settings:
+ self.settings[property] = EMPTY
+ elif self.settings[property] == UNPRIMED:
+ self.settings[property] = EMPTY
+ if flag_as_loaded == True:
+ self._settings_loaded = True
+
+ def save_settings(self):
+ """Load the settings from disk."""
+ # Override. Should save the dict output of ._get_saved_settings()
+ settings = self._get_saved_settings()
+ pass # write settings to disk....
+
+ def _get_saved_settings(self):
+ settings = {}
+ for k,v in self.settings.items():
+ if v != None and v != EMPTY:
+ settings[k] = v
+ for k in self.required_saved_properties:
+ settings[k] = getattr(self, self._setting_name_to_attr_name(k))
+ return settings
+
+ def clear_cached_setting(self, setting=None):
+ "If setting=None, clear *all* cached settings"
+ if setting != None:
+ if hasattr(self, "_%s_cached_value" % setting):
+ delattr(self, "_%s_cached_value" % setting)
+ else:
+ for setting in settings_properties:
+ self.clear_cached_setting(setting)
+
+
+class SavedSettingsObjectTests(unittest.TestCase):
+ def testSimpleProperty(self):
+ class Test(SavedSettingsObject):
+ settings_properties = []
+ required_saved_properties = []
+ @versioned_property(name="Content-type",
+ doc="A test property",
+ settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties)
+ def content_type(): return {}
+ def __init__(self):
+ SavedSettingsObject.__init__(self)
+ t = Test()
+ # access missing setting
+ self.failUnless(t._settings_loaded == False, t._settings_loaded)
+ self.failUnless(len(t.settings) == 0, len(t.settings))
+ self.failUnless(t.content_type == EMPTY, t.content_type)
+ # accessing t.content_type triggers the priming, which runs
+ # t._setup_saved_settings, which fills out t.settings with
+ # EMPTY data. t._settings_loaded is still false though, since
+ # the default priming does not do any of the `official' loading
+ # that occurs in t.load_settings.
+ self.failUnless(len(t.settings) == 1, len(t.settings))
+ self.failUnless(t.settings["Content-type"] == EMPTY,
+ t.settings["Content-type"])
+ self.failUnless(t._settings_loaded == False, t._settings_loaded)
+ # load settings creates an EMPTY value in the settings array
+ t.load_settings()
+ self.failUnless(t._settings_loaded == True, t._settings_loaded)
+ self.failUnless(t.settings["Content-type"] == EMPTY,
+ t.settings["Content-type"])
+ self.failUnless(t.content_type == EMPTY, t.content_type)
+ self.failUnless(len(t.settings) == 1, len(t.settings))
+ self.failUnless(t.settings["Content-type"] == EMPTY,
+ t.settings["Content-type"])
+ # now we set a value
+ t.content_type = None
+ self.failUnless(t.settings["Content-type"] == None,
+ t.settings["Content-type"])
+ self.failUnless(t.content_type == None, t.content_type)
+ self.failUnless(t.settings["Content-type"] == None,
+ t.settings["Content-type"])
+ # now we set another value
+ t.content_type = "text/plain"
+ self.failUnless(t.content_type == "text/plain", t.content_type)
+ self.failUnless(t.settings["Content-type"] == "text/plain",
+ t.settings["Content-type"])
+ self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"},
+ t._get_saved_settings())
+ # now we clear to the post-primed value
+ t.content_type = EMPTY
+ self.failUnless(t._settings_loaded == True, t._settings_loaded)
+ self.failUnless(t.settings["Content-type"] == EMPTY,
+ t.settings["Content-type"])
+ self.failUnless(t.content_type == EMPTY, t.content_type)
+ self.failUnless(len(t.settings) == 1, len(t.settings))
+ self.failUnless(t.settings["Content-type"] == EMPTY,
+ t.settings["Content-type"])
+ def testDefaultingProperty(self):
+ class Test(SavedSettingsObject):
+ settings_properties = []
+ required_saved_properties = []
+ @versioned_property(name="Content-type",
+ doc="A test property",
+ default="text/plain",
+ settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties)
+ def content_type(): return {}
+ def __init__(self):
+ SavedSettingsObject.__init__(self)
+ t = Test()
+ self.failUnless(t._settings_loaded == False, t._settings_loaded)
+ self.failUnless(t.content_type == "text/plain", t.content_type)
+ self.failUnless(t._settings_loaded == False, t._settings_loaded)
+ t.load_settings()
+ self.failUnless(t._settings_loaded == True, t._settings_loaded)
+ self.failUnless(t.content_type == "text/plain", t.content_type)
+ self.failUnless(t.settings["Content-type"] == EMPTY,
+ t.settings["Content-type"])
+ self.failUnless(t._get_saved_settings() == {}, t._get_saved_settings())
+ t.content_type = "text/html"
+ self.failUnless(t.content_type == "text/html",
+ t.content_type)
+ self.failUnless(t.settings["Content-type"] == "text/html",
+ t.settings["Content-type"])
+ self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"},
+ t._get_saved_settings())
+ def testRequiredDefaultingProperty(self):
+ class Test(SavedSettingsObject):
+ settings_properties = []
+ required_saved_properties = []
+ @versioned_property(name="Content-type",
+ doc="A test property",
+ default="text/plain",
+ settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties,
+ require_save=True)
+ def content_type(): return {}
+ def __init__(self):
+ SavedSettingsObject.__init__(self)
+ t = Test()
+ self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"},
+ t._get_saved_settings())
+ t.content_type = "text/html"
+ self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"},
+ t._get_saved_settings())
+ def testClassVersionedPropertyDefinition(self):
+ class Test(SavedSettingsObject):
+ settings_properties = []
+ required_saved_properties = []
+ def _versioned_property(settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties,
+ **kwargs):
+ if "settings_properties" not in kwargs:
+ kwargs["settings_properties"] = settings_properties
+ if "required_saved_properties" not in kwargs:
+ kwargs["required_saved_properties"]=required_saved_properties
+ return versioned_property(**kwargs)
+ @_versioned_property(name="Content-type",
+ doc="A test property",
+ default="text/plain",
+ require_save=True)
+ def content_type(): return {}
+ def __init__(self):
+ SavedSettingsObject.__init__(self)
+ t = Test()
+ self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"},
+ t._get_saved_settings())
+ t.content_type = "text/html"
+ self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"},
+ t._get_saved_settings())
+
+unitsuite=unittest.TestLoader().loadTestsFromTestCase(SavedSettingsObjectTests)
+suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
diff --git a/libbe/tree.py b/libbe/tree.py
index e6f144e..9e07ee3 100644
--- a/libbe/tree.py
+++ b/libbe/tree.py
@@ -39,6 +39,9 @@ class Tree(list):
>>> a.branch_len()
5
+ >>> a.sort(key=lambda node : -node.branch_len())
+ >>> "".join([node.n for node in a.traverse()])
+ 'acfhiebdg'
>>> a.sort(key=lambda node : node.branch_len())
>>> "".join([node.n for node in a.traverse()])
'abdgcefhi'
@@ -93,7 +96,7 @@ class Tree(list):
"""
list.sort(self, *args, **kwargs)
for child in self:
- child.sort()
+ child.sort(*args, **kwargs)
def traverse(self, depthFirst=True):
"""
diff --git a/libbe/utility.py b/libbe/utility.py
index 2c77fcf..30240a9 100644
--- a/libbe/utility.py
+++ b/libbe/utility.py
@@ -15,10 +15,11 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import calendar
-import time
+import codecs
import os
-import tempfile
import shutil
+import tempfile
+import time
import doctest
@@ -87,73 +88,5 @@ def str_to_time(str_time):
def handy_time(time_val):
return time.strftime("%a, %d %b %Y %H:%M", time.localtime(time_val))
-class CantFindEditor(Exception):
- def __init__(self):
- Exception.__init__(self, "Can't find editor to get string from")
-
-def editor_string(comment=None):
-
- """Invokes the editor, and returns the user_produced text as a string
-
- >>> if "EDITOR" in os.environ:
- ... del os.environ["EDITOR"]
- >>> if "VISUAL" in os.environ:
- ... del os.environ["VISUAL"]
- >>> editor_string()
- Traceback (most recent call last):
- CantFindEditor: Can't find editor to get string from
- >>> os.environ["EDITOR"] = "echo bar > "
- >>> editor_string()
- u'bar\\n'
- >>> os.environ["VISUAL"] = "echo baz > "
- >>> editor_string()
- u'baz\\n'
- >>> del os.environ["EDITOR"]
- >>> del os.environ["VISUAL"]
- """
- for name in ('VISUAL', 'EDITOR'):
- try:
- editor = os.environ[name]
- break
- except KeyError:
- pass
- else:
- raise CantFindEditor()
- fhandle, fname = tempfile.mkstemp()
- try:
- if comment is not None:
- os.write(fhandle, '\n'+comment_string(comment))
- os.close(fhandle)
- oldmtime = os.path.getmtime(fname)
- os.system("%s %s" % (editor, fname))
- output = trimmed_string(file(fname, "rb").read().decode("utf-8"))
- if output.rstrip('\n') == "":
- output = None
- finally:
- os.unlink(fname)
- return output
-
-
-def comment_string(comment):
- """
- >>> comment_string('hello')
- '== Anything below this line will be ignored ==\\nhello'
- """
- return '== Anything below this line will be ignored ==\n' + comment
-
-
-def trimmed_string(instring):
- """
- >>> trimmed_string("hello\\n== Anything below this line will be ignored")
- 'hello\\n'
- >>> trimmed_string("hi!\\n" + comment_string('Booga'))
- 'hi!\\n'
- """
- out = []
- for line in instring.splitlines(True):
- if line.startswith('== Anything below this line will be ignored'):
- break
- out.append(line)
- return ''.join(out)
suite = doctest.DocTestSuite()
diff --git a/test.py b/test.py
index bf57d1e..1f1ffcf 100644
--- a/test.py
+++ b/test.py
@@ -19,9 +19,12 @@ if len(sys.argv) > 1:
for submodname in sys.argv[1:]:
match = False
mod = plugin.get_plugin("libbe", submodname)
- if mod is not None and hasattr(mod, "suite"):
- suite.addTest(mod.suite)
- match = True
+ if mod is not None:
+ if hasattr(mod, "suite"):
+ suite.addTest(mod.suite)
+ match = True
+ else:
+ print "Module \"%s\" has no test suite" % submodname
mod = plugin.get_plugin("becommands", submodname)
if mod is not None:
suite.addTest(doctest.DocTestSuite(mod))
diff --git a/test_usage.sh b/test_usage.sh
index 43b5d4d..42c0f2f 100755
--- a/test_usage.sh
+++ b/test_usage.sh
@@ -11,7 +11,7 @@
# Note that this script uses the *installed* version of be, not the
# one in your working tree.
-set -e # exit imediately on failed command
+set -e # exit immediately on failed command
set -o pipefail # pipes fail if any stage fails
set -v # verbose, echo commands to stdout
@@ -95,6 +95,8 @@ echo "$OUT"
BUG=`echo "$OUT" | sed -n 's/Created bug with ID //p'`
echo "Working with bug: $BUG"
be comment $BUG "This is an argument"
+be set user_id "$ID" # get tired of guessing user id for none RCS
+be set # show settings
be comment $BUG:1 "No it isn't" # comment on the first comment
be show $BUG # show details on a given bug
be close $BUG # set bug status to 'closed'
@@ -102,7 +104,6 @@ be comment $BUG "It's closed, but I can still comment."
be open $BUG # set bug status to 'open'
be comment $BUG "Reopend, comment again"
be status $BUG fixed # set bug status to 'fixed'
-be show $BUG # show bug details & comments
be list # list all open bugs
be list --status fixed # list all fixed bugs
be assign $BUG # assign the bug to yourself
@@ -111,6 +112,11 @@ be assign $BUG 'Joe' # assign the bug to Joe
be list -a Joe -s fixed # list the fixed bugs assigned to Joe
be assign $BUG none # assign the bug to noone
be diff # see what has changed
+OUT=`be new 'also having too much fun'`
+BUGB=`echo "$OUT" | sed -n 's/Created bug with ID //p'`
+be comment $BUGB "Blissfully unaware of a similar bug"
+be merge $BUG $BUGB # join BUGB to BUG
+be show $BUG # show bug details & comments
be remove $BUG # decide that you don't like that bug after all
cd /
rm -rf $TESTDIR