diff options
129 files changed, 4577 insertions, 2077 deletions
diff --git a/.be/bugs/02223264-e28a-4720-9f20-1e7a27a7041d/values b/.be/bugs/02223264-e28a-4720-9f20-1e7a27a7041d/values index bb8f7f3..ac2fa4e 100644 --- a/.be/bugs/02223264-e28a-4720-9f20-1e7a27a7041d/values +++ b/.be/bugs/02223264-e28a-4720-9f20-1e7a27a7041d/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=fixed diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/body b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/body new file mode 100644 index 0000000..595381c --- /dev/null +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/body @@ -0,0 +1,34 @@ +When I try to do set-root on a git repository, I get: +# be set-root . +Traceback (most recent call last): + File "/usr/local/bin/be", line 55, in <module> + sys.exit(execute(sys.argv[1], sys.argv[2:])) + File "/usr/lib/python2.5/site-packages/libbe/cmdutil.py", line 105, in execute + File "/usr/lib/python2.5/site-packages/becommands/set_root.py", line 57, in execute + File "/usr/lib/python2.5/site-packages/libbe/bugdir.py", line 110, in create_bug_dir + File "/usr/lib/python2.5/site-packages/libbe/bugdir.py", line 70, in set_version + File "/usr/lib/python2.5/site-packages/libbe/git.py", line 51, in set_file_contents + File "/usr/lib/python2.5/site-packages/libbe/git.py", line 38, in add_id + File "/usr/lib/python2.5/site-packages/libbe/git.py", line 33, in invoke_client + File "/usr/lib/python2.5/site-packages/libbe/rcs.py", line 63, in invoke + File "/usr/lib/python2.5/subprocess.py", line 594, in __init__ + errread, errwrite) + File "/usr/lib/python2.5/subprocess.py", line 1147, in _execute_child + raise child_exception +OSError: [Errno 2] No such file or directory: '' + +because the cwd argument for Popen is set to '' (the empty string). + +The following patch fixes the issue: +--- libbe/git.py 2008-06-22 19:52:14.000000000 -0400 ++++ libbe/git.py 2008-06-23 00:53:39.000000000 -0400 +@@ -26,7 +26,7 @@ + return filename + + def invoke_client(*args, **kwargs): +- directory = kwargs['directory'] ++ directory = kwargs['directory'] or None + expect = kwargs.get('expect', (0, 1)) + cl_args = ["git"] + cl_args.extend(args) + diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values new file mode 100644 index 0000000..d55baa7 --- /dev/null +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Mon, 23 Jun 2008 05:02:22 +0000 + + + + + + +From=hubert + + + diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/body b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/body new file mode 100644 index 0000000..49fe1fb --- /dev/null +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/body @@ -0,0 +1,29 @@ +It looks like the problems with the git backend are more than just in the +site-init command. It looks like several places expect that git_dir_for_path +and git_repo_for_path return absolute paths, while in the current +implementation, it may not be the case. Here is an updated patch to fix this. +This replaces the previous patch that I gave in this bug. It seems to work for +me, but I haven't heavily tested it. + +--- libbe/git.py 2008-06-22 19:52:14.000000000 -0400 ++++ /libbe/git.py 2008-06-23 22:39:17.000000000 -0400 +@@ -102,11 +102,16 @@ + """Find the root of the deepest repository containing path.""" + # Assume that nothing funny is going on; in particular, that we aren't + # dealing with a bare repo. +- return os.path.dirname(git_dir_for_path(path)) ++ # "git rev-parse --show-cdup" gives the relative path to the top-level ++ # directory of the repository. We then join that to the requested path, ++ # and then use realpath to turn it into an absolute path and to get rid of ++ # ".." components. ++ return os.path.realpath(os.path.join(path,invoke_client("rev-parse", "--show-cdup", directory=path)[1].rstrip())) + + def git_dir_for_path(path): + """Find the git-dir of the deepest repo containing path.""" +- return invoke_client("rev-parse", "--git-dir", directory=path)[1].rstrip() ++ repo = git_repo_for_path(path) ++ return os.path.join(repo,invoke_client("rev-parse", "--git-dir", directory=repo)[1].rstrip()) + + def export(spec, bug_dir, revision_dir): + """Check out commit 'spec' from the git repo containing bug_dir into + diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values new file mode 100644 index 0000000..1350ffb --- /dev/null +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Tue, 24 Jun 2008 02:45:18 +0000 + + + + + + +From=hubert + + + diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/body b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/body new file mode 100644 index 0000000..ccc18ea --- /dev/null +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/body @@ -0,0 +1,10 @@ +Fixed another bug in git.strip_git(). lstrip() wasn't what I had thought. + +>>> "/a.b/.be/x/y".lstrip("/a.b/") +'e/x/y' + +So I went back to just droping the first N chars + +>>> "/a.b/.be/x/y"[len("/a.b/"):] +'.be/x/y' + diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values new file mode 100644 index 0000000..67b182a --- /dev/null +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Sun, 16 Nov 2008 20:36:20 +0000 + + + + + + +From=wking + + + diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/body b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/body new file mode 100644 index 0000000..c889a38 --- /dev/null +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/body @@ -0,0 +1 @@ +Fixed with a simpler patch. diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values new file mode 100644 index 0000000..4a2e108 --- /dev/null +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Thu, 13 Nov 2008 19:31:04 +0000 + + + + + + +From=wking + + + diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/body b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/body new file mode 100644 index 0000000..7c07a0f --- /dev/null +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/body @@ -0,0 +1,23 @@ +Oops, missed a case. I now see what Hubert was saying about absolute +paths :p. In git.strip_git(), the output of git_repo_for_path('.') +was being subtracted from an absolute path. Obviously, if the path +was returning '.', you'd get things like + +filename= +/home/wking/src/fun/testbe/.be/bugs/c3bf839b-88f9-4609-89a2-6a5b75c415b8/values + +stripping 2 chars ('.' and '/')], returns +ome/wking/src/fun/testbe/.be/bugs/c3bf839b-88f9-4609-89a2-6a5b75c415b8/values + + +Now we convert the git_repo_for_path output to an absolute path and get + +filename= +/home/wking/src/fun/testbe/.be/bugs/c3bf839b-88f9-4609-89a2-6a5b75c415b8/values +absRepoPath= +/home/wking/src/fun/testbe +absRepoSlashedDir= +/home/wking/src/fun/testbe/ +returns +.be/bugs/c3bf839b-88f9-4609-89a2-6a5b75c415b8/values + diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values new file mode 100644 index 0000000..cbf7142 --- /dev/null +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Thu, 13 Nov 2008 20:18:02 +0000 + + + + + + +From=wking + + + diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/values new file mode 100644 index 0000000..84e14f1 --- /dev/null +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/values @@ -0,0 +1,35 @@ + + + +creator=hubert + + + + + + +severity=minor + + + + + + +status=fixed + + + + + + +summary=set-root in git repository fails + + + + + + +time=Mon, 23 Jun 2008 04:57:22 +0000 + + + diff --git a/.be/bugs/0e0c806c-5443-4839-aa60-9615c8c10853/values b/.be/bugs/0e0c806c-5443-4839-aa60-9615c8c10853/values index 26c2c47..05f3eba 100644 --- a/.be/bugs/0e0c806c-5443-4839-aa60-9615c8c10853/values +++ b/.be/bugs/0e0c806c-5443-4839-aa60-9615c8c10853/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values b/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values deleted file mode 100644 index 68c357f..0000000 --- a/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values +++ /dev/null @@ -1,35 +0,0 @@ - - - -creator=abentley - - - - - - -severity=minor - - - - - - -status=disabled - - - - - - -summary=Oh, wait - - - - - - -time=Fri, 03 Feb 2006 21:35:52 +0000 - - - diff --git a/.be/bugs/14c65eab-b9f2-4d43-991d-2dac6c239fc4/values b/.be/bugs/14c65eab-b9f2-4d43-991d-2dac6c239fc4/values deleted file mode 100644 index 33cacf2..0000000 --- a/.be/bugs/14c65eab-b9f2-4d43-991d-2dac6c239fc4/values +++ /dev/null @@ -1,28 +0,0 @@ - - - -creator=abentley - - - - - - -severity=minor - - - - - - -status=closed - - - - - - -summary= - - - diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values index 5e923f7..74ffa83 100644 --- a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values +++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7/values @@ -1,6 +1,13 @@ +Content-type=text/plain + + + + + + Date=Sat, 01 Apr 2006 18:32:47 +0000 diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body new file mode 100644 index 0000000..d09a4be --- /dev/null +++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body @@ -0,0 +1 @@ +This seems to be taken care of. diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values new file mode 100644 index 0000000..6c7fb63 --- /dev/null +++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Fri, 14 Nov 2008 05:00:43 +0000 + + + + + + +From=wking + + + diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values index 3b96b7b..cf41641 100644 --- a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values +++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=fixed diff --git a/.be/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values b/.be/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values index a7c57ed..fe5568e 100644 --- a/.be/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values +++ b/.be/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8/values @@ -1,6 +1,13 @@ +Content-type=text/plain + + + + + + Date=Wed, 04 Jan 2006 21:03:54 +0000 diff --git a/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values b/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values index b528771..02f718a 100644 --- a/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values +++ b/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values b/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values index 2971ab4..08c3ae4 100644 --- a/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values +++ b/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values index 2f1cf4c..f88e71f 100644 --- a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values +++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f/values @@ -1,6 +1,13 @@ +Content-type=text/plain + + + + + + Date=Tue, 17 May 2005 13:42:52 +0000 diff --git a/.be/bugs/38bd9b8a-3325-4ee5-bb75-600dfb415285/values b/.be/bugs/38bd9b8a-3325-4ee5-bb75-600dfb415285/values deleted file mode 100644 index 33cacf2..0000000 --- a/.be/bugs/38bd9b8a-3325-4ee5-bb75-600dfb415285/values +++ /dev/null @@ -1,28 +0,0 @@ - - - -creator=abentley - - - - - - -severity=minor - - - - - - -status=closed - - - - - - -summary= - - - diff --git a/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values b/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values index 5a7b54e..4d1cded 100644 --- a/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values +++ b/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/body b/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/body new file mode 100644 index 0000000..dfcf82c --- /dev/null +++ b/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/body @@ -0,0 +1,29 @@ +I was having problems with `python test.py bugdir` with the Arch +backend. Commits were failing with `archive not registered'. + +Adding some trace information to arch.Arch._rcs_init() and +._rcs_cleanup() (the traceback module is great :p), I found +that the problem was coming from bugdir.BugDir.guess_rcs(). + +The Arch backend deletes any auto-created archives when it is cleaned +up (RCS.__del__ -> RCS.cleanup -> Arch._rcs_cleanup). This means that +whatever instance is used to init the archive in guess_rcs() must be +kept around. I had been doing: + * installed_rcs() -> Arch-instance-A + * Arch-instance-A.init() + * store Arch-instnance-A.name as bugdir.rcs_name + * future calls to bugdir.rcs get new instance Arch-instance-B + * eventually Arch-instance-A cleaned up + * archive dissapears & tests crash + +I switched things around so .rcs is the `master attribute' and +.rcs_name follows it. Now just save whichever rcs you used to init +your archive as .rcs. + +In order to implement the fix, I had to tweak the memory/file-system +interaction a bit. Instead of saving the settings *every*time* a +setting_property changed, we now save only if the .be file exists. +This file serves as a 'file-system-bugdir-active' flag. Before it is +created (e.g., by a .save()), the BugDir lives purely in memory, and +can freely go about configuring .rcs, .rcs_name, etc until it get's +to the point where it's ready to go to disk. diff --git a/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values b/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values new file mode 100644 index 0000000..b19c065 --- /dev/null +++ b/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Sat, 22 Nov 2008 18:53:20 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/values b/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/values new file mode 100644 index 0000000..96c0708 --- /dev/null +++ b/.be/bugs/496edad5-1484-413a-bc68-4b01274a65eb/values @@ -0,0 +1,35 @@ + + + +creator=W. Trevor King <wking@drexel.edu> + + + + + + +severity=minor + + + + + + +status=fixed + + + + + + +summary=Early del-cleanup with Arch backend + + + + + + +time=Sat, 22 Nov 2008 18:38:32 +0000 + + + diff --git a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values index 480386b..823e2bc 100644 --- a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values +++ b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values @@ -15,7 +15,7 @@ severity=minor -status=disabled +status=closed diff --git a/.be/bugs/65776f00-34d8-4b58-874d-333196a5e245/values b/.be/bugs/65776f00-34d8-4b58-874d-333196a5e245/values index 8f484de..79c65e2 100644 --- a/.be/bugs/65776f00-34d8-4b58-874d-333196a5e245/values +++ b/.be/bugs/65776f00-34d8-4b58-874d-333196a5e245/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=fixed diff --git a/.be/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values b/.be/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values index f20c01d..ba9e33e 100644 --- a/.be/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values +++ b/.be/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc/values @@ -1,6 +1,13 @@ +Content-type=text/plain + + + + + + Date=Fri, 27 Jan 2006 14:30:26 +0000 diff --git a/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values b/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values index 625495f..4622bc6 100644 --- a/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values +++ b/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values @@ -15,7 +15,7 @@ severity=serious -status=closed +status=fixed diff --git a/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values b/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values index 93689fb..921528e 100644 --- a/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values +++ b/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values b/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values index 8426c10..4cb1f35 100644 --- a/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values +++ b/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813/values @@ -1,6 +1,13 @@ +Content-type=text/plain + + + + + + Date=Thu, 24 Mar 2005 17:04:47 +0000 diff --git a/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values b/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values index ae4c276..51af41d 100644 --- a/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values +++ b/.be/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2/values @@ -1,6 +1,13 @@ +Content-type=text/plain + + + + + + Date=Thu, 24 Mar 2005 13:05:13 +0000 diff --git a/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/body b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/body new file mode 100644 index 0000000..c602969 --- /dev/null +++ b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/body @@ -0,0 +1 @@ +Fixed at least by commit 273, probably way before. diff --git a/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values new file mode 100644 index 0000000..ada2348 --- /dev/null +++ b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Mon, 24 Nov 2008 13:08:07 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values index 411922d..2bde2a3 100644 --- a/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values +++ b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc/values @@ -1,6 +1,13 @@ +Content-type=text/plain + + + + + + Date=Wed, 21 Dec 2005 21:53:47 +0000 diff --git a/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/values b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/values index 974ca50..685c112 100644 --- a/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/values +++ b/.be/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=fixed diff --git a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/body b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/body new file mode 100644 index 0000000..d10b444 --- /dev/null +++ b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/body @@ -0,0 +1,38 @@ +File "/home/wking/src/fun/be-bugfix/becommands/status.py", line 25, in becommands.status.execute +Failed example: + bd = bugdir.simple_bug_dir() +Exception raised: + Traceback (most recent call last): + File "/usr/lib/python2.5/doctest.py", line 1228, in __run + compileflags, 1) in test.globs + File "<doctest becommands.status.execute[1]>", line 1, in <module> + bd = bugdir.simple_bug_dir() + File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 293, in simple_bug_dir + bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True) + File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 99, in __init__ + rcs = self.guess_rcs(allow_rcs_init) + File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 165, in guess_rcs + rcs = installed_rcs() + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 53, in installed_rcs + return _get_matching_rcs(lambda rcs: rcs.installed()) + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 37, in _get_matching_rcs + if matchfn(rcs) == True: + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 53, in <lambda> + return _get_matching_rcs(lambda rcs: rcs.installed()) + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 180, in installed + self._rcs_help() + File "/home/wking/src/fun/be-bugfix/libbe/bzr.py", line 32, in _rcs_help + status,output,error = self._u_invoke_client("--help") + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 362, in _u_invoke_client + return self._u_invoke(cl_args, expect, cwd=directory) + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 355, in _u_invoke + raise CommandError(error, status) + CommandError: Command failed (1): 'import site' failed; use -v for traceback + bzr: ERROR: Couldn't import bzrlib and dependencies. + Please check bzrlib is on your PYTHONPATH. + + Traceback (most recent call last): + File "/usr/bin/bzr", line 64, in <module> + import bzrlib + ImportError: No module named bzrlib + diff --git a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values new file mode 100644 index 0000000..f109f3e --- /dev/null +++ b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Fri, 21 Nov 2008 18:41:47 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/body b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/body new file mode 100644 index 0000000..3d7d3aa --- /dev/null +++ b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/body @@ -0,0 +1,2 @@ +Aha, a final os.chdir('/') line is required to clean up after the +set_root.py doctest. diff --git a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values new file mode 100644 index 0000000..e0e3783 --- /dev/null +++ b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Fri, 21 Nov 2008 19:12:42 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/body b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/body new file mode 100644 index 0000000..1fe5ce3 --- /dev/null +++ b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/body @@ -0,0 +1,170 @@ +Hysteretic! test.py severity passes, then fails. + +Problem caused somewhere in set_root? Doctest? Bzr? + +libbe/plugin.py adds the BE-path to sys.path, but it is done by the +time the TestRunner fires up... Wierd. + +$ python test.py severity set_root severity +Doctest: becommands.severity.execute ... ok +Doctest: becommands.set_root.execute ... FAIL +Doctest: becommands.severity.execute ... FAIL + +====================================================================== +FAIL: Doctest: becommands.set_root.execute +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/usr/lib/python2.5/doctest.py", line 2128, in runTest + raise self.failureException(self.format_failure(new.getvalue())) +AssertionError: Failed doctest test for becommands.set_root.execute + File "/home/wking/src/fun/be-bugfix/becommands/set_root.py", line 22, in execute + +---------------------------------------------------------------------- +File "/home/wking/src/fun/be-bugfix/becommands/set_root.py", line 41, in becommands.set_root.execute +Failed example: + print rcs.name +Expected: + Arch +Got: + bzr +---------------------------------------------------------------------- +File "/home/wking/src/fun/be-bugfix/becommands/set_root.py", line 43, in becommands.set_root.execute +Failed example: + execute([]) +Expected: + Using Arch for revision control. + Directory initialized. +Got: + Using bzr for revision control. + Directory initialized. + + +====================================================================== +FAIL: Doctest: becommands.severity.execute +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/usr/lib/python2.5/doctest.py", line 2128, in runTest + raise self.failureException(self.format_failure(new.getvalue())) +AssertionError: Failed doctest test for becommands.severity.execute + File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 22, in execute + +---------------------------------------------------------------------- +File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 25, in becommands.severity.execute +Failed example: + bd = bugdir.simple_bug_dir() +Exception raised: + Traceback (most recent call last): + File "/usr/lib/python2.5/doctest.py", line 1228, in __run + compileflags, 1) in test.globs + File "<doctest becommands.severity.execute[1]>", line 1, in <module> + bd = bugdir.simple_bug_dir() + File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 293, in simple_bug_dir + bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True) + File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 99, in __init__ + rcs = self.guess_rcs(allow_rcs_init) + File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 165, in guess_rcs + rcs = installed_rcs() + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 53, in installed_rcs + return _get_matching_rcs(lambda rcs: rcs.installed()) + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 37, in _get_matching_rcs + if matchfn(rcs) == True: + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 53, in <lambda> + return _get_matching_rcs(lambda rcs: rcs.installed()) + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 180, in installed + self._rcs_help() + File "/home/wking/src/fun/be-bugfix/libbe/bzr.py", line 32, in _rcs_help + status,output,error = self._u_invoke_client("--help") + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 362, in _u_invoke_client + return self._u_invoke(cl_args, expect, cwd=directory) + File "/home/wking/src/fun/be-bugfix/libbe/rcs.py", line 355, in _u_invoke + raise CommandError(error, status) + CommandError: Command failed (1): 'import site' failed; use -v for traceback + bzr: ERROR: Couldn't import bzrlib and dependencies. + Please check bzrlib is on your PYTHONPATH. + + Traceback (most recent call last): + File "/usr/bin/bzr", line 64, in <module> + import bzrlib + ImportError: No module named bzrlib + +---------------------------------------------------------------------- +File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 26, in becommands.severity.execute +Failed example: + os.chdir(bd.root) +Exception raised: + Traceback (most recent call last): + File "/usr/lib/python2.5/doctest.py", line 1228, in __run + compileflags, 1) in test.globs + File "<doctest becommands.severity.execute[2]>", line 1, in <module> + os.chdir(bd.root) + NameError: name 'bd' is not defined +---------------------------------------------------------------------- +File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 27, in becommands.severity.execute +Failed example: + execute(["a"]) +Exception raised: + Traceback (most recent call last): + File "/usr/lib/python2.5/doctest.py", line 1228, in __run + compileflags, 1) in test.globs + File "<doctest becommands.severity.execute[3]>", line 1, in <module> + execute(["a"]) + File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 40, in execute + bd = bugdir.BugDir(loadNow=True) + File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 85, in __init__ + root = os.getcwd() + OSError: [Errno 2] No such file or directory +---------------------------------------------------------------------- +File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 29, in becommands.severity.execute +Failed example: + execute(["a", "wishlist"]) +Exception raised: + Traceback (most recent call last): + File "/usr/lib/python2.5/doctest.py", line 1228, in __run + compileflags, 1) in test.globs + File "<doctest becommands.severity.execute[4]>", line 1, in <module> + execute(["a", "wishlist"]) + File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 40, in execute + bd = bugdir.BugDir(loadNow=True) + File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 85, in __init__ + root = os.getcwd() + OSError: [Errno 2] No such file or directory +---------------------------------------------------------------------- +File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 30, in becommands.severity.execute +Failed example: + execute(["a"]) +Exception raised: + Traceback (most recent call last): + File "/usr/lib/python2.5/doctest.py", line 1228, in __run + compileflags, 1) in test.globs + File "<doctest becommands.severity.execute[5]>", line 1, in <module> + execute(["a"]) + File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 40, in execute + bd = bugdir.BugDir(loadNow=True) + File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 85, in __init__ + root = os.getcwd() + OSError: [Errno 2] No such file or directory +---------------------------------------------------------------------- +File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 32, in becommands.severity.execute +Failed example: + execute(["a", "none"]) +Expected: + Traceback (most recent call last): + UserError: Invalid severity level: none +Got: + Traceback (most recent call last): + File "/usr/lib/python2.5/doctest.py", line 1228, in __run + compileflags, 1) in test.globs + File "<doctest becommands.severity.execute[6]>", line 1, in <module> + execute(["a", "none"]) + File "/home/wking/src/fun/be-bugfix/becommands/severity.py", line 40, in execute + bd = bugdir.BugDir(loadNow=True) + File "/home/wking/src/fun/be-bugfix/libbe/bugdir.py", line 85, in __init__ + root = os.getcwd() + OSError: [Errno 2] No such file or directory + + +---------------------------------------------------------------------- +Ran 3 tests in 8.719s + +FAILED (failures=2) + diff --git a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values new file mode 100644 index 0000000..e5498c9 --- /dev/null +++ b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Fri, 21 Nov 2008 19:01:19 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/values b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/values new file mode 100644 index 0000000..38ad221 --- /dev/null +++ b/.be/bugs/8e83da06-26f1-4763-a972-dae7e7062233/values @@ -0,0 +1,35 @@ + + + +creator=W. Trevor King <wking@drexel.edu> + + + + + + +severity=minor + + + + + + +status=fixed + + + + + + +summary=test.py removes path to bzrlib + + + + + + +time=Fri, 21 Nov 2008 18:41:03 +0000 + + + diff --git a/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/body b/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/body new file mode 100644 index 0000000..6d75610 --- /dev/null +++ b/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/body @@ -0,0 +1 @@ +Would you do this instead of `be diff`? diff --git a/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values b/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values new file mode 100644 index 0000000..6f59e9c --- /dev/null +++ b/.be/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Mon, 24 Nov 2008 13:10: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 a282359..27ec173 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,6 +1,13 @@ +Content-type=text/plain + + + + + + Date=Fri, 31 Mar 2006 22:15:09 +0000 diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/values b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/values index 5ee35f9..c80f16e 100644 --- a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/values +++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=fixed diff --git a/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/body b/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/body new file mode 100644 index 0000000..05022e8 --- /dev/null +++ b/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/body @@ -0,0 +1 @@ +Fixed by 273. Probably around 253. diff --git a/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values b/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values new file mode 100644 index 0000000..6df7a97 --- /dev/null +++ b/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Mon, 24 Nov 2008 13:05:07 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/values b/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/values index b745597..d79a55a 100644 --- a/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/values +++ b/.be/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=fixed diff --git a/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values b/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values index ff8a30a..2fa1905 100644 --- a/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values +++ b/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values @@ -22,7 +22,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/b3c6da51-3a30-42c9-8c75-587c7a1705c5/values b/.be/bugs/b3c6da51-3a30-42c9-8c75-587c7a1705c5/values new file mode 100644 index 0000000..d1a7029 --- /dev/null +++ b/.be/bugs/b3c6da51-3a30-42c9-8c75-587c7a1705c5/values @@ -0,0 +1,35 @@ + + + +creator=W. Trevor King <wking@drexel.edu> + + + + + + +severity=critical + + + + + + +status=fixed + + + + + + +summary=Slow be commands due to bugdir loading, go back to lazy bug loading. + + + + + + +time=Sun, 23 Nov 2008 13:48:01 +0000 + + + diff --git a/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/body b/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/body new file mode 100644 index 0000000..a490992 --- /dev/null +++ b/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/body @@ -0,0 +1 @@ +Looks like j@oil21.org fixed this in 211.3.1. diff --git a/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values b/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values new file mode 100644 index 0000000..f94558c --- /dev/null +++ b/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Mon, 24 Nov 2008 13:23:43 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/values b/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/values index b465169..857c816 100644 --- a/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/values +++ b/.be/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=fixed diff --git a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/body b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/body new file mode 100644 index 0000000..cf9af30 --- /dev/null +++ b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/body @@ -0,0 +1,5 @@ +I rewrote test.py, so I suppose I'm the person who understands it +better now ;). The usage is now documented in the test.py lead +comment. The becommand tests now attempt to run with the first +*installed* versioning system, which should reduce cryptic errors. + diff --git a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values new file mode 100644 index 0000000..9b72e2c --- /dev/null +++ b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Wed, 19 Nov 2008 17:11:51 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/values b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/values index 5ecca35..483916d 100644 --- a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/values +++ b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=fixed diff --git a/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values b/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values index e854f0e..7e7f554 100644 --- a/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values +++ b/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body new file mode 100644 index 0000000..7f46872 --- /dev/null +++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body @@ -0,0 +1,17 @@ +Example: + +We're working happily in a versioned bugdir, and our RCS knows who we +are. We create a temporary repository copy from a previous revision +for diff generation. We set the RCS for the copy to "None", since we +didn't bother initializing our normal RCS in the snapshot copy. But +now the BugDir instantized on the copy doesn't know who we are! + +Solution: + +Track user id in the bugdir settings file. If you +bugdir.settings["user_id"], it will be saved and loaded. When loaded, +it will also set bugdir.user_id. If you set rcs.user_id, it will be +returned by rcs.get_user_id(), instead of returing the output of +rcs._rcs_get_user_id(). We should be caching the output of +_rcs_get_user_id() anyway. + diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values new file mode 100644 index 0000000..368afb3 --- /dev/null +++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Sat, 22 Nov 2008 21:43:29 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/body b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/body new file mode 100644 index 0000000..62c14e6 --- /dev/null +++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/body @@ -0,0 +1 @@ +This bug duplicates a403de79-8f39-41f2-b9ec-15053b175ee2 diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values new file mode 100644 index 0000000..5953360 --- /dev/null +++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/25c67b0b-1afd-4613-a787-e0f018614966/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Sun, 23 Nov 2008 12:37:57 +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 new file mode 100644 index 0000000..5aed729 --- /dev/null +++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values @@ -0,0 +1,35 @@ + + + +creator=W. Trevor King <wking@drexel.edu> + + + + + + +severity=minor + + + + + + +status=fixed + + + + + + +summary=Allow user id to be cached in settings for duplicate bugdirs + + + + + + +time=Sat, 22 Nov 2008 21:36:06 +0000 + + + diff --git a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body new file mode 100644 index 0000000..d7a57d9 --- /dev/null +++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body @@ -0,0 +1 @@ +I dunno, bugs everywhere is such a great mental image... ;) diff --git a/.be/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 new file mode 100644 index 0000000..cb5a094 --- /dev/null +++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Sat, 15 Nov 2008 23:56:51 +0000 + + + + + + +From=wking + + + diff --git a/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values b/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values index 8024d04..39b0fd7 100644 --- a/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values +++ b/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values @@ -22,7 +22,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/body b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/body new file mode 100644 index 0000000..2887d2b --- /dev/null +++ b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/body @@ -0,0 +1,10 @@ +Problem was due to + open-value-file + write-value-file + add/update-value-file +which should be (and now is) + open-value-file + write-value-file + close-value-file + add/update-value-file +since it was getting added before the changes we'd written were flushed out. diff --git a/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values new file mode 100644 index 0000000..92e7e86 --- /dev/null +++ b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Wed, 19 Nov 2008 01:12:37 +0000 + + + + + + +From=W. Trevor King <wking@example.com> + + + diff --git a/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/body b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/body new file mode 100644 index 0000000..2c49b6b --- /dev/null +++ b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/body @@ -0,0 +1,26 @@ +It looks like the mapfiles are not being 'git add'ed after changes. + +$ mkdir BEtest +$ cd BEtest +$ git init +$ be set-root . +$ be new 'new' +$ git status +# On branch master +# +# Initial commit +# +# Changes to be committed: +# (use "git rm --cached <file>..." to unstage) +# +# new file: .be/bugs/8f021d79-44f5-479f-af12-c37e2caf3ce1/values +# new file: .be/settings +# new file: .be/version +# +# Changed but not updated: +# (use "git add <file>..." to update what will be committed) +# +# modified: .be/bugs/8f021d79-44f5-479f-af12-c37e2caf3ce1/values +# modified: .be/settings +# + diff --git a/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values new file mode 100644 index 0000000..13df021 --- /dev/null +++ b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Mon, 17 Nov 2008 15:03:58 +0000 + + + + + + +From=wking + + + diff --git a/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/values b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/values new file mode 100644 index 0000000..c36e743 --- /dev/null +++ b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/values @@ -0,0 +1,35 @@ + + + +creator=wking + + + + + + +severity=serious + + + + + + +status=fixed + + + + + + +summary=BE not notifying git of some changed files + + + + + + +time=Mon, 17 Nov 2008 15:02:15 +0000 + + + diff --git a/.be/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c/values b/.be/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c/values index 2465211..06b20b7 100644 --- a/.be/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c/values +++ b/.be/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=fixed diff --git a/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values b/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values index 87a5ca5..ef82d6f 100644 --- a/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values +++ b/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values @@ -8,7 +8,7 @@ severity=fatal -status=closed +status=fixed diff --git a/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values b/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values index 233e336..bcf47f4 100644 --- a/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values +++ b/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed @@ -3,3 +3,4 @@ Aaron Bentley Oleg Romanyshyn Thomas Gerigk Marien Zwart +W. Trevor King diff --git a/Bugs-Everywhere-Web/README.txt b/Bugs-Everywhere-Web/README.txt index efde8ff..10774df 100644 --- a/Bugs-Everywhere-Web/README.txt +++ b/Bugs-Everywhere-Web/README.txt @@ -1,4 +1,42 @@ Bugs-Everywhere-Web This is a TurboGears (http://www.turbogears.org) project. It can be -started by running the start-beweb.py script.
\ No newline at end of file +started by running the start-beweb.py script. + +Configure by creating an appropriate beweb/config.py from +beweb/config.py.example. The server will edit the repositories that +it manages, so you should probably have it running on a seperate +branch than your working repository. You can then merge/push +as you require to keep the branches in sync. + +See + http://docs.turbogears.org/1.0/Configuration +For standard turbogears configuration information. + +Currently, you need to login for any methods with a +@identity.require() decorator. The only group in the current +implementation is 'editbugs'. Basically, anyone can browse around, +but only registered 'editbugs' members can change things. + +Anonymous actions: + * See project tree + * See buglist + * See comments +Editbugs required actions: + * Create new comments + * Reply to comments + * Update comment info + + +All login attempts will fail unless you have added some valid users. See + http://docs.turbogears.org/1.0/GettingStartedWithIdentity +For a good intro. For the impatient, try something like + Bugs-Everywhere-Web$ tg-admin toolbox + browse to 'CatWalk' -> 'User' -> 'Add User+' +or + Bugs-Everywhere-Web$ tg-admin sholl + >>> u = User(user_name=u'jdoe', email_address=u'jdoe@example.com', + display_name=u'Jane Doe', password=u'xxx') + >>> g = Group(group_name=u'editbugs', display_name=u'Edit Bugs') + >>> g.addUser(u) # BE-Web uses SQLObject +Exit the tg-admin shell with Ctrl-Z on MS Windows, Ctrl-D on other systems. diff --git a/Bugs-Everywhere-Web/beweb/controllers.py b/Bugs-Everywhere-Web/beweb/controllers.py index e3d555b..a0d0ff9 100644 --- a/Bugs-Everywhere-Web/beweb/controllers.py +++ b/Bugs-Everywhere-Web/beweb/controllers.py @@ -4,9 +4,7 @@ import cherrypy import turbogears from turbogears import controllers, expose, validate, redirect, identity -from libbe.bugdir import (tree_root, cmp_severity, new_bug, new_comment, - NoRootEntry) -from libbe import names +from libbe.bugdir import tree_root, NoRootEntry from config import projects from prest import PrestHandler, provide_action @@ -94,10 +92,7 @@ class Bug(PrestHandler): bug_tree = project_tree(project) bugs = list(bug_tree.list()) if sort_by is None: - def cmp_date(bug1, bug2): - return -cmp(bug1.time, bug2.time) - bugs.sort(cmp_date) - bugs.sort(cmp_severity) + bugs.sort() return {"project_id" : project, "project_name" : projects[project][0], "bugs" : bugs, @@ -108,11 +103,12 @@ class Bug(PrestHandler): @identity.require( identity.has_permission("editbugs")) @provide_action("action", "New bug") def new_bug(self, bug_data, bug, **kwargs): - bug = new_bug(project_tree(bug_data['project'])) + bug = project_tree(bug_data['project']).new_bug() bug.creator = identity.current.user.userId bug.save() raise cherrypy.HTTPRedirect(bug_url(bug_data['project'], bug.uuid)) + @identity.require( identity.has_permission("editbugs")) @provide_action("action", "Update") def update(self, bug_data, bug, status, severity, summary, assigned, action): diff --git a/Bugs-Everywhere-Web/beweb/templates/bugs.kid b/Bugs-Everywhere-Web/beweb/templates/bugs.kid index 376e77b..198aa94 100644 --- a/Bugs-Everywhere-Web/beweb/templates/bugs.kid +++ b/Bugs-Everywhere-Web/beweb/templates/bugs.kid @@ -1,6 +1,6 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <?python -from libbe.cmdutil import unique_name +from libbe.names import unique_name from beweb.controllers import bug_url, project_url, bug_list_url from beweb.model import people_map people = people_map() diff --git a/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid b/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid index 34fcb99..276f610 100644 --- a/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid +++ b/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid @@ -1,6 +1,6 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <?python -from libbe.bugdir import severity_levels, active_status, inactive_status, thread_comments +from libbe.bug import severity_values, status_values, thread_comments from libbe.utility import time_to_str from beweb.controllers import bug_list_url, comment_url from beweb.formatting import comment_body_xhtml, select_among @@ -20,14 +20,14 @@ people = people_map() <form method="post" action="."> <table> <tr><td>Status</td><td>Severity</td><td>Assigned To</td><td>Summary</td></tr> -<tr><td>${select_among("status", active_status+inactive_status, bug.status)}</td><td>${select_among("severity", severity_levels, bug.severity)}</td> +<tr><td>${select_among("status", status_values, bug.status)}</td><td>${select_among("severity", severity_values, bug.severity)}</td> <td>${select_among("assigned", people.keys()+[None], bug.assigned, people)}</td><td><input name="summary" value="${bug.summary}" size="80" /></td></tr> </table> <div py:def="show_comment(comment, children)" class="comment"> <insetbox> <table> <tr><td>From</td><td>${comment.From}</td></tr> - <tr><td>Date</td><td>${time_to_str(comment.date)}</td></tr> + <tr><td>Date</td><td>${time_to_str(comment.time)}</td></tr> </table> <div py:content="comment_body_xhtml(comment)" py:strip="True"></div> <a href="${comment_url(project_id, bug.uuid, comment.uuid)}">Edit</a> diff --git a/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid b/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid index 551db9d..2b522d4 100644 --- a/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid +++ b/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid @@ -1,6 +1,5 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <?python -from libbe.bugdir import severity_levels from libbe.utility import time_to_str from beweb.controllers import bug_list_url, bug_url ?> @@ -17,7 +16,7 @@ from beweb.controllers import bug_list_url, bug_url <form method="post"> <table> <tr><td>From</td><td>${comment.From}</td></tr> - <tr><td>Date</td><td>${time_to_str(comment.date)}</td></tr> + <tr><td>Date</td><td>${time_to_str(comment.time)}</td></tr> </table> <insetbox><textarea rows="15" cols="80" py:content="comment.body" name="comment_body" style="border-style: none"/></insetbox> <p><input type="submit" name="action" value="Update"/></p> diff --git a/Bugs-Everywhere-Web/beweb/templates/projects.kid b/Bugs-Everywhere-Web/beweb/templates/projects.kid index 09bde77..d5f9fd3 100644 --- a/Bugs-Everywhere-Web/beweb/templates/projects.kid +++ b/Bugs-Everywhere-Web/beweb/templates/projects.kid @@ -1,6 +1,6 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <?python -from libbe.bugdir import severity_levels +from libbe.bug import severity_values def select_among(name, options, default): output = ['<select name="%s">' % name] for option in options: @@ -17,7 +17,7 @@ $ be set-root $PROJECT_ROOT To create bugs, use "be new $DESCRIPTION". To comment on bugs, you can can use "be comment $BUG_ID". To close a bug, use "be close $BUG_ID". For more -commands, see "be help" +commands, see "be help". You can also look at the usage in test_usage.sh. Using BeWeb, the web UI ======================= diff --git a/README.dev b/README.dev new file mode 100644 index 0000000..bb39ba5 --- /dev/null +++ b/README.dev @@ -0,0 +1,28 @@ +Extending BE +============ + +To write a plugin, you simply create a new file in the becommands +directory. Take a look at one of the simpler plugins (e.g. open.py) +for an example of how that looks, and to start getting a feel for the +libbe interface. + +To fit into the current framework, your extension module should +provide the following elements: + __desc__ + A short string describing the purpose of your plugin + execute(args) + The entry function for your plugin. args is everything from + sys.argv after the name of your plugin (e.g. for the command + `be open abc', args=['abc']). + help() + Return the string to be output by `be help <yourplugin>', + `be <yourplugin> --help', etc. + +While that's all that's strictly necessary, many plugins (all the +current ones) use libbe.cmdutil.CmdOptionParser to provide a +consistent interface + get_parser() + Return an instance of CmdOptionParser("<usage string>"). You can + alter the parser (e.g. add some more options) before returning it. + +Again, you can just browse around in becommands to get a feel for things. @@ -17,50 +17,25 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -from libbe.cmdutil import * -from libbe.bugdir import tree_root, create_bug_dir -from libbe import names, plugin, cmdutil import sys -import os -import becommands.severity -import becommands.list -import becommands.show -import becommands.set_root -import becommands.new -import becommands.close -import becommands.open -import becommands.inprogress -__doc__ = """Bugs Everywhere - Distributed bug tracking - -Supported becommands - set-root: assign the root directory for bug tracking - new: Create a new bug - list: list bugs - show: show a particular bug - close: close a bug - open: re-open a bug - severity: %s - -Unimplemented becommands - comment: append a comment to a bug -""" % becommands.severity.__desc__ - +from libbe import cmdutil +__doc__ == cmdutil.help() if len(sys.argv) == 1 or sys.argv[1] in ('--help', '-h'): - cmdutil.print_command_list() + print cmdutil.help() else: try: try: - sys.exit(execute(sys.argv[1], sys.argv[2:])) + sys.exit(cmdutil.execute(sys.argv[1], sys.argv[2:])) except KeyError, e: - raise UserError("Unknown command \"%s\"" % e.args[0]) + raise cmdutil.UserError("Unknown command \"%s\"" % e.args[0]) except cmdutil.GetHelp: print cmdutil.help(sys.argv[1]) sys.exit(0) except cmdutil.UsageError: print cmdutil.help(sys.argv[1]) sys.exit(1) - except UserError, e: + except cmdutil.UserError, e: print e sys.exit(1) diff --git a/becommands/assign.py b/becommands/assign.py index 38ece52..cb732b3 100644 --- a/becommands/assign.py +++ b/becommands/assign.py @@ -15,45 +15,52 @@ # 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 bugdir, cmdutil, names +from libbe import cmdutil, bugdir __desc__ = __doc__ def execute(args): """ - >>> from libbe import tests, names >>> import os - >>> dir = tests.simple_bug_dir() - >>> os.chdir(dir.dir) - >>> dir.get_bug("a").assigned is None + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) + >>> bd.bug_from_shortname("a").assigned is None True + >>> execute(["a"]) - >>> dir.get_bug("a").assigned == names.creator() + >>> bd._clear_bugs() + >>> bd.bug_from_shortname("a").assigned == bd.user_id True + >>> execute(["a", "someone"]) - >>> dir.get_bug("a").assigned - u'someone' + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("a").assigned + someone + >>> execute(["a","none"]) - >>> dir.get_bug("a").assigned is None + >>> bd._clear_bugs() + >>> bd.bug_from_shortname("a").assigned is None True - >>> tests.clean_up() """ options, args = get_parser().parse_args(args) assert(len(args) in (0, 1, 2)) if len(args) == 0: - print help() - return - bug = cmdutil.get_bug(args[0]) + raise cmdutil.UserError("Please specify a bug id.") + if len(args) > 2: + help() + raise cmdutil.UserError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True) + bug = bd.bug_from_shortname(args[0]) if len(args) == 1: - bug.assigned = names.creator() + bug.assigned = bd.user_id elif len(args) == 2: if args[1] == "none": bug.assigned = None else: bug.assigned = args[1] - bug.save() + bd.save() def get_parser(): - parser = cmdutil.CmdOptionParser("be assign bug-id [assignee]") + parser = cmdutil.CmdOptionParser("be assign BUG-ID [ASSIGNEE]") return parser longhelp = """ diff --git a/becommands/close.py b/becommands/close.py index 52ab735..8d2ccdb 100644 --- a/becommands/close.py +++ b/becommands/close.py @@ -15,33 +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 """Close a bug""" -from libbe import cmdutil +from libbe import cmdutil, bugdir +__desc__ = __doc__ + def execute(args): """ - >>> from libbe import tests + >>> from libbe import bugdir >>> import os - >>> dir = tests.simple_bug_dir() - >>> os.chdir(dir.dir) - >>> dir.get_bug("a").status - u'open' + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("a").status + open >>> execute(["a"]) - >>> dir.get_bug("a").status - u'closed' - >>> tests.clean_up() + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("a").status + closed """ options, args = get_parser().parse_args(args) - if len(args) !=1: + if len(args) == 0: raise cmdutil.UserError("Please specify a bug id.") - bug = cmdutil.get_bug(args[0]) + if len(args) > 1: + help() + raise cmdutil.UserError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True) + bug = bd.bug_from_shortname(args[0]) bug.status = "closed" - bug.save() + bd.save() def get_parser(): - parser = cmdutil.CmdOptionParser("be close bug-id") + parser = cmdutil.CmdOptionParser("be close BUG-ID") return parser longhelp=""" -Close the bug identified by bug-id. +Close the bug identified by BUG-ID. """ def help(): diff --git a/becommands/comment.py b/becommands/comment.py index e3a1d93..172f818 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -15,39 +15,72 @@ # 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 bugdir, cmdutil, names, utility +from libbe import cmdutil, bugdir, utility import os +__desc__ = __doc__ + def execute(args): """ - >>> from libbe import tests, names - >>> import os, time - >>> dir = tests.simple_bug_dir() - >>> os.chdir(dir.dir) + >>> import time + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) >>> execute(["a", "This is a comment about a"]) - >>> comment = dir.get_bug("a").list_comments()[0] - >>> comment.body - u'This is a comment about a\\n' - >>> comment.From == names.creator() + >>> bd._clear_bugs() + >>> bug = bd.bug_from_shortname("a") + >>> bug.load_comments() + >>> comment = bug.comment_root[0] + >>> print comment.body + This is a comment about a + <BLANKLINE> + >>> comment.From == bd.user_id True - >>> comment.date <= int(time.time()) + >>> comment.time <= int(time.time()) True >>> comment.in_reply_to is None True + >>> if 'EDITOR' in os.environ: ... del os.environ["EDITOR"] >>> execute(["b"]) Traceback (most recent call last): UserError: No comment supplied, and EDITOR not specified. + >>> os.environ["EDITOR"] = "echo 'I like cheese' > " >>> execute(["b"]) - >>> dir.get_bug("b").list_comments()[0].body - u'I like cheese\\n' - >>> tests.clean_up() + >>> bd._clear_bugs() + >>> bug = bd.bug_from_shortname("b") + >>> bug.load_comments() + >>> comment = bug.comment_root[0] + >>> print comment.body + I like cheese + <BLANKLINE> """ options, args = get_parser().parse_args(args) - if len(args) < 1: - raise cmdutil.UsageError() - bug, parent_comment = cmdutil.get_bug_and_comment(args[0]) + if len(args) == 0: + raise cmdutil.UserError("Please specify a bug or comment id.") + if len(args) > 2: + help() + raise cmdutil.UserError("Too many arguments.") + + shortname = args[0] + if shortname.count(':') > 1: + raise cmdutil.UserError("Invalid id '%s'." % shortname) + elif shortname.count(':') == 1: + # Split shortname generated by Comment.comment_shortnames() + bugname = shortname.split(':')[0] + is_reply = True + else: + bugname = shortname + is_reply = False + + bd = bugdir.BugDir(from_disk=True) + bug = bd.bug_from_shortname(bugname) + bug.load_comments() + if is_reply: + 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") @@ -61,12 +94,9 @@ def execute(args): body = args[1] if not body.endswith('\n'): body+='\n' - - comment = bugdir.new_comment(bug, body) - if parent_comment is not None: - comment.in_reply_to = parent_comment.uuid - comment.save() - + + comment = parent.new_reply(body=body) + bd.save() def get_parser(): parser = cmdutil.CmdOptionParser("be comment ID COMMENT") diff --git a/becommands/diff.py b/becommands/diff.py index 82ebb2c..77194ff 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -16,25 +16,48 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Compare bug reports with older tree""" -from libbe import bugdir, diff, cmdutil +from libbe import cmdutil, bugdir, diff import os +__desc__ = __doc__ + def execute(args): + """ + >>> import os + >>> bd = bugdir.simple_bug_dir() + >>> original = bd.rcs.commit("Original status") + >>> bug = bd.bug_from_uuid("a") + >>> bug.status = "closed" + >>> bd.save() + >>> changed = bd.rcs.commit("Closed bug a") + >>> os.chdir(bd.root) + >>> if bd.rcs.versioned == True: + ... execute([original]) + ... else: + ... print "a:cm: Bug A\\nstatus: open -> closed\\n" + Modified bug reports: + a:cm: Bug A + status: open -> closed + <BLANKLINE> + """ options, args = get_parser().parse_args(args) if len(args) == 0: - spec = None - elif len(args) == 1: - spec = args[0] - else: - raise cmdutil.UsageError - tree = bugdir.tree_root(".") - if tree.rcs_name == "None": + 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) + if bd.rcs.versioned == False: print "This directory is not revision-controlled." else: - diff.diff_report(diff.reference_diff(tree, spec), tree) - + old_bd = bd.duplicate_bugdir(revision) + r,m,a = diff.diff(old_bd, bd) + diff.diff_report((r,m,a), bd) + bd.remove_duplicate_bugdir() def get_parser(): - parser = cmdutil.CmdOptionParser("be diff [specifier]") + parser = cmdutil.CmdOptionParser("be diff [SPECIFIER]") return parser longhelp=""" diff --git a/becommands/help.py b/becommands/help.py index aa4aa64..bf0b4fc 100644 --- a/becommands/help.py +++ b/becommands/help.py @@ -15,7 +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 """Print help for given subcommand""" -from libbe import bugdir, cmdutil, names, utility +from libbe import cmdutil, utility +__desc__ = __doc__ def execute(args): """ @@ -25,7 +26,7 @@ def execute(args): if len(args) > 1: raise cmdutil.UserError("Too many arguments.") if len(args) == 0: - cmdutil.print_command_list() + print cmdutil.help() else: try: print cmdutil.help(args[0]) diff --git a/becommands/list.py b/becommands/list.py index d745702..63e1cd6 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -15,106 +15,167 @@ # 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 bugdir, cmdutil, names +from libbe import cmdutil, bugdir +from libbe.bug import cmp_full, severity_values, status_values, \ + active_status_values, inactive_status_values import os +__desc__ = __doc__ + def execute(args): + """ + >>> import os + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) + >>> execute([]) + a:om: Bug A + >>> execute(["--status", "all"]) + a:om: Bug A + b:cm: Bug B + """ options, args = get_parser().parse_args(args) if len(args) > 0: - raise cmdutil.UsageError - active = True - severity = ("minor", "serious", "critical", "fatal") - if options.wishlist: - severity = ("wishlist",) - if options.closed: - active = False - tree = cmdutil.bug_tree() - current_id = names.creator() + help() + raise cmdutil.UserError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True) + bd.load_all_bugs() + # select status + if options.status != None: + if options.status == "all": + status = status_values + else: + status = options.status.split(',') + else: + status = [] + if options.active == True: + status.extend(list(active_status_values)) + if options.unconfirmed == True: + status.append("unconfirmed") + if options.open == True: + status.append("opened") + if options.test == True: + status.append("test") + if status == []: # set the default value + status = active_status_values + # select severity + if options.severity != None: + if options.severity == "all": + severity = severity_values + else: + severity = options.severity.split(',') + else: + severity = [] + if options.wishlist == True: + severity.extend("wishlist") + if options.important == True: + serious = severity_values.index("serious") + severity.append(list(severity_values[serious:])) + if severity == []: # set the default value + severity = severity_values + # select assigned + if options.assigned != None: + if options.assigned == "all": + assigned = "all" + else: + assigned = options.assigned.split(',') + else: + assigned = [] + if options.mine == True: + assigned.extend('-') + if assigned == []: # set the default value + assigned = "all" + for i in range(len(assigned)): + if assigned[i] == '-': + assigned[i] = bd.user_id + # select target + if options.target != None: + if options.target == "all": + target = "all" + else: + target = options.target.split(',') + else: + target = [] + if options.cur_target == True: + target.append(bd.target) + if target == []: # set the default value + target = "all" + def filter(bug): - if options.mine and bug.assigned != current_id: + if status != "all" and not bug.status in status: + return False + if severity != "all" and not bug.severity in severity: return False - if options.cur_target: - if tree.target is None or bug.target != tree.target: - return False - if active is not None: - if bug.active != active: - return False - if bug.severity not in severity: + if assigned != "all" and not bug.assigned in assigned: + return False + if target != "all" and not bug.target in target: return False return True - all_bugs = list(tree.list()) - bugs = [b for b in all_bugs if filter(b) ] + bugs = [b for b in bd if filter(b) ] if len(bugs) == 0: print "No matching bugs found" - my_target_bugs = [] - other_target_bugs = [] - unassigned_target_bugs = [] - my_bugs = [] - other_bugs = [] - unassigned_bugs = [] - if tree.target is not None: - for bug in bugs: - if bug.target != tree.target: - continue - if bug.assigned == current_id: - my_target_bugs.append(bug) - elif bug.assigned is None: - unassigned_target_bugs.append(bug) - else: - other_target_bugs.append(bug) - - for bug in bugs: - if tree.target is not None and bug.target == tree.target: - continue - if bug.assigned == current_id: - my_bugs.append(bug) - elif bug.assigned is None: - unassigned_bugs.append(bug) - else: - other_bugs.append(bug) - - def list_bugs(cur_bugs, title, no_target=False): - def cmp_date(bug1, bug2): - return -cmp(bug1.time, bug2.time) - cur_bugs.sort(cmp_date) - cur_bugs.sort(bugdir.cmp_severity) + def list_bugs(cur_bugs, title=None, no_target=False): + cur_bugs.sort(cmp_full) if len(cur_bugs) > 0: - print cmdutil.underlined(title) + if title != None: + print cmdutil.underlined(title) for bug in cur_bugs: - print cmdutil.bug_summary(bug, all_bugs, no_target=no_target, - shortlist=True) + print bug.string(shortlist=True) - list_bugs(my_target_bugs, - "Bugs assigned to you for target %s" % tree.target, - no_target=True) - list_bugs(unassigned_target_bugs, - "Unassigned bugs for target %s" % tree.target, no_target=True) - list_bugs(other_target_bugs, - "Bugs assigned to others for target %s" % tree.target, - no_target=True) - list_bugs(my_bugs, "Bugs assigned to you") - list_bugs(unassigned_bugs, "Unassigned bugs") - list_bugs(other_bugs, "Bugs assigned to others") - + list_bugs(bugs, no_target=False) def get_parser(): parser = cmdutil.CmdOptionParser("be list [options]") - parser.add_option("-w", "--wishlist", action="store_true", dest="wishlist", - help="List bugs with 'wishlist' severity") - parser.add_option("-c", "--closed", action="store_true", dest="closed", - help="List closed bugs") - parser.add_option("-m", "--mine", action="store_true", dest="mine", - help="List only bugs assigned to you") - parser.add_option("-t", "--cur-target", action="store_true", - dest="cur_target", - help="List only bugs for the current target") + parser.add_option("-s", "--status", metavar="STATUS", dest="status", + help="List options matching STATUS", default=None) + parser.add_option("-v", "--severity", metavar="SEVERITY", dest="severity", + help="List options matching SEVERITY", default=None) + parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", + 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"), + ("i", "important", "List bugs with >= 'serious' severity"), + ("A", "active", "List all active bugs"), + ("u", "unconfirmed", "List unconfirmed bugs"), + ("o", "open", "List open bugs"), + ("T", "test", "List bugs in testing"), + ("m", "mine", "List bugs assigned to you"), + ("c", "cur-target", "List bugs for the current target")) + 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=""" -This command lists bugs. Options are cumulative, so that -mc will list only -closed bugs assigned to you. -""" +This command lists bugs. There are several criteria that you can +search by: + * status + * severity + * assigned (who the bug is assigned to) + * target (bugfix deadline) +Allowed values for each criterion may be given in a comma seperated +list. The special string "all" may be used with any of these options +to match all values of the criterion. + +status + %s +severity + %s +assigned + free form, with the string '-' being a shortcut for yourself. +target + free form + +In addition, there are some shortcut options that set boolean flags. +The boolean options are ignored if the matching string option is used. +""" % (','.join(status_values), + ','.join(severity_values)) def help(): return get_parser().help_str() + longhelp diff --git a/becommands/new.py b/becommands/new.py index 7bd2382..caa1549 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -15,37 +15,36 @@ # 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 bugdir, cmdutil, names, utility +from libbe import cmdutil, bugdir +__desc__ = __doc__ + def execute(args): """ >>> import os, time - >>> from libbe import tests - >>> dir = tests.bug_arch_dir() - >>> os.chdir(dir.dir) - >>> names.uuid = lambda: "a" + >>> from libbe import bug + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) + >>> bug.uuid_gen = lambda: "X" >>> execute (["this is a test",]) - Created bug with ID a - >>> bug = list(dir.list())[0] + Created bug with ID X + >>> bd.load() + >>> bug = bd.bug_from_uuid("X") >>> bug.summary u'this is a test' - >>> bug.creator = os.environ["LOGNAME"] >>> bug.time <= int(time.time()) True >>> bug.severity u'minor' >>> bug.target == None True - >>> tests.clean_up() """ options, args = get_parser().parse_args(args) if len(args) != 1: raise cmdutil.UserError("Please supply a summary message") - dir = cmdutil.bug_tree() - bug = bugdir.new_bug(dir) - bug.summary = args[0] - bug.save() - bugs = (dir.list()) - print "Created bug with ID %s" % cmdutil.unique_name(bug, bugs) + bd = bugdir.BugDir(from_disk=True) + bug = bd.new_bug(summary=args[0]) + bd.save() + print "Created bug with ID %s" % bd.bug_shortname(bug) def get_parser(): parser = cmdutil.CmdOptionParser("be new SUMMARY") diff --git a/becommands/open.py b/becommands/open.py index f7c23c1..788a183 100644 --- a/becommands/open.py +++ b/becommands/open.py @@ -15,26 +15,31 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Re-open a bug""" -from libbe import cmdutil +from libbe import cmdutil, bugdir +__desc__ = __doc__ + def execute(args): """ - >>> from libbe import tests >>> import os - >>> dir = tests.simple_bug_dir() - >>> os.chdir(dir.dir) - >>> dir.get_bug("b").status - u'closed' + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("b").status + closed >>> execute(["b"]) - >>> dir.get_bug("b").status - u'open' - >>> tests.clean_up() + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("b").status + open """ options, args = get_parser().parse_args(args) - if len(args) !=1: + if len(args) == 0: raise cmdutil.UserError("Please specify a bug id.") - bug = cmdutil.get_bug(args[0]) + if len(args) > 1: + help() + raise cmdutil.UserError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True) + bug = bd.bug_from_shortname(args[0]) bug.status = "open" - bug.save() + bd.save() def get_parser(): parser = cmdutil.CmdOptionParser("be open BUG-ID") diff --git a/becommands/inprogress.py b/becommands/remove.py index 05da971..8f7c2c6 100644 --- a/becommands/inprogress.py +++ b/becommands/remove.py @@ -14,34 +14,45 @@ # You 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 -"""Bug fixing in progress""" -from libbe import cmdutil +"""Remove (delete) a bug and its comments""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + def execute(args): """ - >>> from libbe import tests + >>> from libbe import mapfile >>> import os - >>> dir = tests.simple_bug_dir() - >>> os.chdir(dir.dir) - >>> dir.get_bug("a").status - u'open' - >>> execute(["a"]) - >>> dir.get_bug("a").status - u'in-progress' - >>> tests.clean_up() + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("b").status + closed + >>> execute (["b"]) + Removed bug b + >>> bd._clear_bugs() + >>> try: + ... bd.bug_from_shortname("b") + ... except KeyError: + ... print "Bug not found" + Bug not found """ options, args = get_parser().parse_args(args) - if len(args) !=1: + if len(args) != 1: raise cmdutil.UserError("Please specify a bug id.") - bug = cmdutil.get_bug(args[0]) - bug.status = "in-progress" - bug.save() + bd = bugdir.BugDir(from_disk=True) + bug = bd.bug_from_shortname(args[0]) + bd.remove_bug(bug) + bd.save() + print "Removed bug %s" % bug.uuid def get_parser(): - parser = cmdutil.CmdOptionParser("be inprogress BUG-ID") + parser = cmdutil.CmdOptionParser("be remove BUG-ID") return parser longhelp=""" -Mark a bug as 'in-progress'. +Remove (delete) an existing bug. Use with caution: if you're not using a +revision control system, there may be no way to recover the lost information. +You should use this command, for example, to get rid of blank or otherwise +mangled bugs. """ def help(): diff --git a/becommands/set.py b/becommands/set.py index e359df1..287ceb4 100644 --- a/becommands/set.py +++ b/becommands/set.py @@ -15,43 +15,44 @@ # 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 +from libbe import cmdutil, bugdir +__desc__ = __doc__ + def execute(args): """ - >>> from libbe import tests >>> import os - >>> dir = tests.simple_bug_dir() - >>> os.chdir(dir.dir) - >>> execute(["a"]) + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) + >>> execute(["target"]) None - >>> execute(["a", "tomorrow"]) - >>> execute(["a"]) + >>> execute(["target", "tomorrow"]) + >>> execute(["target"]) tomorrow - >>> execute(["a", "none"]) - >>> execute(["a"]) + >>> execute(["target", "none"]) + >>> execute(["target"]) None - >>> tests.clean_up() """ options, args = get_parser().parse_args(args) if len(args) > 2: + help() raise cmdutil.UserError("Too many arguments.") - tree = cmdutil.bug_tree() + bd = bugdir.BugDir(from_disk=True) if len(args) == 0: - keys = tree.settings.keys() + keys = bd.settings.keys() keys.sort() for key in keys: - print "%16s: %s" % (key, tree.settings[key]) + print "%16s: %s" % (key, bd.settings[key]) elif len(args) == 1: - print tree.settings.get(args[0]) + print bd.settings.get(args[0]) else: if args[1] != "none": - tree.settings[args[0]] = args[1] + bd.settings[args[0]] = args[1] else: - del tree.settings[args[0]] - tree.save_settings() + del bd.settings[args[0]] + bd.save() def get_parser(): - parser = cmdutil.CmdOptionParser("be set [name] [value]") + parser = cmdutil.CmdOptionParser("be set [NAME] [VALUE]") return parser longhelp=""" diff --git a/becommands/set_root.py b/becommands/set_root.py index 2ae7e1a..e17bd87 100644 --- a/becommands/set_root.py +++ b/becommands/set_root.py @@ -15,58 +15,72 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Assign the root directory for bug tracking""" -from libbe import bugdir, cmdutil, rcs +import os.path +from libbe import cmdutil, bugdir +__desc__ = __doc__ def execute(args): """ - >>> from libbe import tests + >>> from libbe import utility, rcs >>> import os - >>> dir = tests.Dir() + >>> dir = utility.Dir() >>> try: - ... bugdir.tree_root(dir.name) + ... bugdir.BugDir(dir.path) ... except bugdir.NoBugDir, e: ... True True - >>> execute([dir.name]) + >>> execute([dir.path]) No revision control detected. Directory initialized. - >>> bd = bugdir.tree_root(dir.name) - >>> bd.root = dir.name - >>> dir = tests.arch_dir() - >>> os.chdir(dir.name) - >>> execute(['.']) + >>> del(dir) + + >>> dir = utility.Dir() + >>> os.chdir(dir.path) + >>> rcs = rcs.installed_rcs() + >>> rcs.init('.') + >>> print rcs.name + Arch + >>> execute([]) Using Arch for revision control. Directory initialized. - >>> bd = bugdir.tree_root(dir.name+"/{arch}") - >>> bd.root = dir.name + >>> rcs.cleanup() + >>> try: ... execute(['.']) ... except cmdutil.UserError, e: ... str(e).startswith("Directory already initialized: ") True - >>> tests.clean_up() >>> execute(['/highly-unlikely-to-exist']) Traceback (most recent call last): UserError: No such directory: /highly-unlikely-to-exist + >>> os.chdir('/') """ options, args = get_parser().parse_args(args) - if len(args) != 1: - raise cmdutil.UsageError - dir_rcs = rcs.detect(args[0]) + if len(args) > 1: + print help() + raise cmdutil.UserError, "Too many arguments" + 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: - bugdir.create_bug_dir(args[0], dir_rcs) + bd = bugdir.BugDir(basedir, from_disk=False, sink_to_existing_root=False, assert_new_BugDir=True) except bugdir.NoRootEntry: - raise cmdutil.UserError("No such directory: %s" % args[0]) + raise cmdutil.UserError("No such directory: %s" % basedir) except bugdir.AlreadyInitialized: - raise cmdutil.UserError("Directory already initialized: %s" % args[0]) - if dir_rcs.name is not "None": - print "Using %s for revision control." % dir_rcs.name + raise cmdutil.UserError("Directory already initialized: %s" % basedir) + bd.save() + if bd.rcs.name is not "None": + print "Using %s for revision control." % bd.rcs.name else: print "No revision control detected." print "Directory initialized." def get_parser(): - parser = cmdutil.CmdOptionParser("be set-root DIRECTORY") + parser = cmdutil.CmdOptionParser("be set-root [DIRECTORY]") return parser longhelp=""" @@ -74,6 +88,8 @@ This command initializes Bugs Everywhere support for the specified directory and all its subdirectories. It will auto-detect any supported revision control system. You can use "be set rcs_name" to change the rcs being used. +The directory defaults to your current working directory. + It is usually a good idea to put the Bugs Everywhere root at the source code root, but you can put it anywhere. If you run "be set-root" in a subdirectory, then only bugs created in that subdirectory (and its children) will appear diff --git a/becommands/severity.py b/becommands/severity.py index af99bf7..3adefaa 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -15,16 +15,15 @@ # 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 bugdir -from libbe import cmdutil +from libbe import cmdutil, bugdir +from libbe.bug import severity_values, severity_description __desc__ = __doc__ def execute(args): """ - >>> from libbe import tests >>> import os - >>> dir = tests.simple_bug_dir() - >>> os.chdir(dir.dir) + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) >>> execute(["a"]) minor >>> execute(["a", "wishlist"]) @@ -33,42 +32,43 @@ def execute(args): >>> execute(["a", "none"]) Traceback (most recent call last): UserError: Invalid severity level: none - >>> tests.clean_up() """ options, args = get_parser().parse_args(args) - assert(len(args) in (0, 1, 2)) - if len(args) == 0: + if len(args) not in (1,2): print help() return - bug = cmdutil.get_bug(args[0]) + bd = bugdir.BugDir(from_disk=True) + bug = bd.bug_from_shortname(args[0]) if len(args) == 1: print bug.severity elif len(args) == 2: try: bug.severity = args[1] - except bugdir.InvalidValue, e: + except ValueError, e: if e.name != "severity": raise raise cmdutil.UserError ("Invalid severity level: %s" % e.value) - bug.save() + bd.save() def get_parser(): - parser = cmdutil.CmdOptionParser("be severity bug-id [severity]") + parser = cmdutil.CmdOptionParser("be severity BUG-ID [SEVERITY]") return parser -longhelp=""" -Show or change a bug's severity level. +longhelp=[""" +Show or change a bug's severity level. If no severity is specified, the current value is printed. If a severity level is specified, it will be assigned to the bug. Severity levels are: -wishlist: A feature that could improve usefulness, but not a bug. - minor: The standard bug level. - serious: A bug that requires workarounds. -critical: A bug that prevents some features from working at all. - fatal: A bug that makes the package unusable. -""" +"""] +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(): return get_parser().help_str() + longhelp diff --git a/becommands/show.py b/becommands/show.py index 8e83a1f..abec813 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -15,33 +15,36 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Show a particular bug""" -from libbe import bugdir, cmdutil, utility -import os +from libbe import cmdutil, bugdir +__desc__ = __doc__ def execute(args): + """ + >>> import os + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) + >>> execute (["a",]) + ID : a + Short name : a + Severity : minor + Status : open + Assigned : + Target : + Creator : John Doe <jdoe@example.com> + Created : Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000) + Bug A + <BLANKLINE> + """ options, args = get_parser().parse_args(args) - if len(args) !=1: + if len(args) == 0: raise cmdutil.UserError("Please specify a bug id.") - bug_dir = cmdutil.bug_tree() - bug = cmdutil.get_bug(args[0], bug_dir) - print cmdutil.bug_summary(bug, list(bug_dir.list())).rstrip("\n") - if bug.time is None: - time_str = "(Unknown time)" - else: - time_str = "%s (%s)" % (utility.handy_time(bug.time), - utility.time_to_str(bug.time)) - print "Created: %s" % time_str - unique_name = cmdutil.unique_name(bug, bug_dir.list()) - comments = [] - name_map = {} - for c_name, comment in cmdutil.iter_comment_name(bug, unique_name): - name_map[comment.uuid] = c_name - comments.append(comment) - threaded = bugdir.thread_comments(comments) - cmdutil.print_threaded_comments(threaded, name_map) + bd = bugdir.BugDir(from_disk=True) + for bugid in args: + bug = bd.bug_from_shortname(bugid) + print bug.string(show_comments=True) def get_parser(): - parser = cmdutil.CmdOptionParser("be show bug-id") + parser = cmdutil.CmdOptionParser("be show BUG-ID [BUG-ID ...]") return parser longhelp=""" diff --git a/becommands/status.py b/becommands/status.py new file mode 100644 index 0000000..a30b3d6 --- /dev/null +++ b/becommands/status.py @@ -0,0 +1,73 @@ +# 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 +"""Show or change a bug's status""" +from libbe import cmdutil, bugdir +from libbe.bug import status_values, status_description +__desc__ = __doc__ + +def execute(args): + """ + >>> import os + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) + >>> execute(["a"]) + open + >>> execute(["a", "closed"]) + >>> execute(["a"]) + closed + >>> execute(["a", "none"]) + Traceback (most recent call last): + UserError: Invalid status: none + """ + options, args = get_parser().parse_args(args) + if len(args) not in (1,2): + print help() + return + bd = bugdir.BugDir(from_disk=True) + bug = bd.bug_from_shortname(args[0]) + if len(args) == 1: + print bug.status + elif len(args) == 2: + try: + bug.status = args[1] + except ValueError, e: + if e.name != "status": + raise + raise cmdutil.UserError ("Invalid status: %s" % e.value) + bd.save() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be status BUG-ID [STATUS]") + 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 +is specified, it will be assigned to the bug. + +Severity 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(): + return get_parser().help_str() + longhelp diff --git a/becommands/target.py b/becommands/target.py index 4b015b4..dce100f 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -15,16 +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 """Show or change a bug's target for fixing""" -from libbe import bugdir -from libbe import cmdutil +from libbe import cmdutil, bugdir __desc__ = __doc__ def execute(args): """ - >>> from libbe import tests >>> import os - >>> dir = tests.simple_bug_dir() - >>> os.chdir(dir.dir) + >>> bd = bugdir.simple_bug_dir() + >>> os.chdir(bd.root) >>> execute(["a"]) No target assigned. >>> execute(["a", "tomorrow"]) @@ -33,14 +31,14 @@ def execute(args): >>> execute(["a", "none"]) >>> execute(["a"]) No target assigned. - >>> tests.clean_up() """ options, args = get_parser().parse_args(args) assert(len(args) in (0, 1, 2)) if len(args) == 0: print help() return - bug = cmdutil.get_bug(args[0]) + bd = bugdir.BugDir(from_disk=True) + bug = bd.bug_from_shortname(args[0]) if len(args) == 1: if bug.target is None: print "No target assigned." @@ -51,16 +49,16 @@ def execute(args): bug.target = None else: bug.target = args[1] - bug.save() + bd.save() def get_parser(): - parser = cmdutil.CmdOptionParser("be target bug-id [target]") + parser = cmdutil.CmdOptionParser("be target BUG-ID [TARGET]") return parser longhelp=""" Show or change a bug's target for fixing. -If no target is specified, the current value is printed. If a target +If no target is specified, the current value is printed. If a target is specified, it will be assigned to the bug. Targets are freeform; any text may be specified. They will generally be diff --git a/becommands/template b/becommands/template deleted file mode 100644 index 3c871e6..0000000 --- a/becommands/template +++ /dev/null @@ -1,21 +0,0 @@ -"""Short description""" -from libbe import bugdir, cmdutil, names -import os -def execute(args): - options, args = get_parser().parse_args(args) - if len(args) > 0: - raise cmdutil.UsageError - - -def get_parser(): - parser = cmdutil.CmdOptionParser("be list [options]") -# parser.add_option("-w", "--wishlist", action="store_true", dest="wishlist", -# help="List bugs with 'wishlist' severity") - return parser - -longhelp=""" -This is for the longwinded description -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/upgrade.py b/becommands/upgrade.py deleted file mode 100644 index 3dcb4eb..0000000 --- a/becommands/upgrade.py +++ /dev/null @@ -1,111 +0,0 @@ -# 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 -"""Upgrade the bugs to the latest format""" -import os.path -import errno -from libbe import bugdir, rcs, cmdutil - -def execute(args): - options, args = get_parser().parse_args(args) - root = bugdir.tree_root(".", old_version=True) - for uuid in root.list_uuids(): - old_bug = OldBug(root.bugs_path, uuid) - - new_bug = bugdir.Bug(root.bugs_path, None) - new_bug.uuid = old_bug.uuid - new_bug.summary = old_bug.summary - new_bug.creator = old_bug.creator - new_bug.target = old_bug.target - new_bug.status = old_bug.status - new_bug.severity = old_bug.severity - - new_bug.save() - for uuid in root.list_uuids(): - old_bug = OldBug(root.bugs_path, uuid) - old_bug.delete() - - bugdir.set_version(root.dir) - -def file_property(name, valid=None): - def getter(self): - value = self._get_value(name) - if valid is not None: - if value not in valid: - raise InvalidValue(name, value) - return value - def setter(self, value): - if valid is not None: - if value not in valid and value is not None: - raise InvalidValue(name, value) - return self._set_value(name, value) - return property(getter, setter) - - -class OldBug(object): - def __init__(self, path, uuid): - self.path = os.path.join(path, uuid) - self.uuid = uuid - - def get_path(self, file): - return os.path.join(self.path, file) - - summary = file_property("summary") - creator = file_property("creator") - target = file_property("target") - status = file_property("status", valid=("open", "closed")) - severity = file_property("severity", valid=("wishlist", "minor", "serious", - "critical", "fatal")) - def delete(self): - self.summary = None - self.creator = None - self.target = None - self.status = None - self.severity = None - self._set_value("name", None) - - def _get_active(self): - return self.status == "open" - - active = property(_get_active) - - def _get_value(self, name): - try: - return file(self.get_path(name), "rb").read().rstrip("\n") - except IOError, e: - if e.errno == errno.EEXIST: - return None - - def _set_value(self, name, value): - if value is None: - try: - rcs.unlink(self.get_path(name)) - except OSError, e: - if e.errno != 2: - raise - else: - rcs.set_file_contents(self.get_path(name), "%s\n" % value) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be upgrade") - return parser - -longhelp=""" -Upgrade the bug storage to the latest format. -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/libbe/arch.py b/libbe/arch.py index 038325a..fd953a4 100644 --- a/libbe/arch.py +++ b/libbe/arch.py @@ -14,196 +14,265 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -from subprocess import Popen, PIPE import os +import shutil +import time +import re +import unittest +import doctest + import config -import errno +from beuuid import uuid_gen +from rcs import RCS, RCStestCase, CommandError + client = config.get_val("arch_client") if client is None: client = "tla" config.set_val("arch_client", client) -def invoke(args): - try : - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) - except OSError, e : - strerror = "%s\nwhile executing %s" % (e.args[1], args) - raise Exception("Command failed: %s" % strerror) - output = q.stdout.read() - error = q.stderr.read() - status = q.wait() - if status >= 0: - return status, output, error - raise Exception("Command failed: %s" % error) - - -def invoke_client(*args, **kwargs): - cl_args = [client] - cl_args.extend(args) - status,output,error = invoke(cl_args) - if status not in (0,): - raise Exception("Command failed: %s" % error) - return output - -def get_user_id(): - try: - return invoke_client('my-id') - except Exception, e: - if 'no arch user id set' in e.args[0]: - return None +def new(): + return Arch() + +class Arch(RCS): + name = "Arch" + client = client + versioned = True + _archive_name = None + _archive_dir = None + _tmp_archive = False + _project_name = None + _tmp_project = False + _arch_paramdir = os.path.expanduser("~/.arch-params") + def _rcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _rcs_detect(self, path): + """Detect whether a directory is revision-controlled using Arch""" + if self._u_search_parent_directories(path, "{arch}") != None : + return True + return False + def _rcs_init(self, path): + self._create_archive(path) + self._create_project(path) + self._add_project_code(path) + def _create_archive(self, path): + # Create a new archive + # http://regexps.srparish.net/tutorial-tla/new-archive.html#Creating_a_New_Archive + assert self._archive_name == None + id = self.get_user_id() + name, email = self._u_parse_id(id) + if email == None: + email = "%s@example.com" % name + trailer = "%s-%s" % ("bugs-everywhere-auto", uuid_gen()[0:8]) + self._archive_name = "%s--%s" % (email, trailer) + self._archive_dir = "/tmp/%s" % trailer + self._tmp_archive = True + self._u_invoke_client("make-archive", self._archive_name, + self._archive_dir, directory=path) + def _invoke_client(self, *args, **kwargs): + """ + Invoke the client on our archive. + """ + assert self._archive_name != None + command = args[0] + if len(args) > 1: + tailargs = args[1:] else: - raise - - -def set_user_id(value): - invoke_client('my-id', value) - - -def ensure_user_id(): - if get_user_id() is None: - set_user_id('nobody <nobody@example.com>') - - -def write_tree_settings(contents, path): - file(os.path.join(path, "{arch}", "=tagging-method"), "wb").write(contents) - -def init_tree(path): - invoke_client("init-tree", "-d", path) - -def temp_arch_tree(type="easy"): - import tempfile - ensure_user_id() - path = tempfile.mkdtemp() - init_tree(path) - if type=="easy": - write_tree_settings("source ^.*$\n", path) - elif type=="tricky": - write_tree_settings("source ^$\n", path) - else: - assert (type=="impossible") - add_dir_rule("precious ^\.boo$", path, path) - return path - -def list_added(root): - assert os.path.exists(root) - assert os.access(root, os.X_OK) - root = os.path.realpath(root) - inv_str = invoke_client("inventory", "--source", '--both', '--all', root) - return [os.path.join(root, p) for p in inv_str.split('\n')] - -def tree_root(filename): - assert os.path.exists(filename) - if not os.path.isdir(filename): - dirname = os.path.dirname(filename) - else: - dirname = filename - return invoke_client("tree-root", dirname).rstrip('\n') - -def rel_filename(filename, root): - filename = os.path.realpath(filename) - root = os.path.realpath(root) - assert(filename.startswith(root)) - return filename[len(root)+1:] + tailargs = [] + arglist = [command, "-A", self._archive_name] + arglist.extend(tailargs) + args = tuple(arglist) + return self._u_invoke_client(*args, **kwargs) + def _remove_archive(self): + assert self._tmp_archive == True + assert self._archive_dir != None + assert self._archive_name != None + os.remove(os.path.join(self._arch_paramdir, + "=locations", self._archive_name)) + shutil.rmtree(self._archive_dir) + self._tmp_archive = False + self._archive_dir = False + self._archive_name = False + def _create_project(self, path): + """ + Create a temporary Arch project in the directory PATH. This + project will be removed by + __del__->cleanup->_rcs_cleanup->_remove_project + """ + # http://mwolson.org/projects/GettingStartedWithArch.html + # http://regexps.srparish.net/tutorial-tla/new-project.html#Starting_a_New_Project + category = "bugs-everywhere" + branch = "mainline" + version = "0.1" + self._project_name = "%s--%s--%s" % (category, branch, version) + self._invoke_client("archive-setup", self._project_name, + directory=path) + self._tmp_project = True + def _remove_project(self): + assert self._tmp_project == True + assert self._project_name != None + assert self._archive_dir != None + shutil.rmtree(os.path.join(self._archive_dir, self._project_name)) + self._tmp_project = False + self._project_name = False + def _archive_project_name(self): + assert self._archive_name != None + assert self._project_name != None + return "%s/%s" % (self._archive_name, self._project_name) + def _adjust_naming_conventions(self, path): + """ + By default, Arch restricts source code filenames to + ^[_=a-zA-Z0-9].*$ + See + http://regexps.srparish.net/tutorial-tla/naming-conventions.html + Since our bug directory '.be' doesn't satisfy these conventions, + we need to adjust them. + + The conventions are specified in + project-root/{arch}/=tagging-method + """ + tagpath = os.path.join(path, "{arch}", "=tagging-method") + lines_out = [] + for line in file(tagpath, "rb"): + line.decode("utf-8") + 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")) + + def _add_project_code(self, path): + # http://mwolson.org/projects/GettingStartedWithArch.html + # http://regexps.srparish.net/tutorial-tla/new-source.html + # http://regexps.srparish.net/tutorial-tla/importing-first.html + self._invoke_client("init-tree", self._project_name, + directory=path) + self._adjust_naming_conventions(path) + self._invoke_client("import", "--summary", "Began versioning", + directory=path) + def _rcs_cleanup(self): + if self._tmp_project == True: + self._remove_project() + if self._tmp_archive == True: + self._remove_archive() + + def _rcs_root(self, path): + if not os.path.isdir(path): + dirname = os.path.dirname(path) + else: + dirname = path + status,output,error = self._u_invoke_client("tree-root", dirname) + root = output.rstrip('\n') + + self._get_archive_project_name(root) + + return root + + def _get_archive_name(self, root): + status,output,error = self._u_invoke_client("archives") + lines = output.split('\n') + # e.g. output: + # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52 + # /tmp/BEtestXXXXXX/rootdir + # (+ repeats) + for archive,location in zip(lines[::2], lines[1::2]): + if os.path.realpath(location) == os.path.realpath(root): + self._archive_name = archive + assert self._archive_name != None + + def _get_archive_project_name(self, root): + # get project names + status,output,error = self._u_invoke_client("tree-version", directory=root) + # e.g output + # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52/be--mainline--0.1 + archive_name,project_name = output.rstrip('\n').split('/') + self._archive_name = archive_name + self._project_name = project_name + def _rcs_get_user_id(self): + try: + status,output,error = self._u_invoke_client('my-id') + return output.rstrip('\n') + except Exception, e: + if 'no arch user id set' in e.args[0]: + return None + else: + raise + def _rcs_set_user_id(self, value): + self._u_invoke_client('my-id', value) + def _rcs_add(self, path): + self._u_invoke_client("add-id", path) + realpath = os.path.realpath(self._u_abspath(path)) + pathAdded = realpath in self._list_added(self.rootdir) + if self.paranoid and not pathAdded: + self._force_source(path) + def _list_added(self, root): + assert os.path.exists(root) + assert os.access(root, os.X_OK) + root = os.path.realpath(root) + status,output,error = self._u_invoke_client("inventory", "--source", + "--both", "--all", root) + inv_str = output.rstrip('\n') + return [os.path.join(root, p) for p in inv_str.split('\n')] + def _add_dir_rule(self, rule, dirname, root): + inv_path = os.path.join(dirname, '.arch-inventory') + file(inv_path, "ab").write(rule) + if os.path.realpath(inv_path) not in self._list_added(root): + paranoid = self.paranoid + self.paranoid = False + self.add(inv_path) + self.paranoid = paranoid + def _force_source(self, path): + rule = "source %s\n" % self._u_rel_path(path) + self._add_dir_rule(rule, os.path.dirname(path), self.rootdir) + if os.path.realpath(path) not in self._list_added(self.rootdir): + raise CantAddFile(path) + def _rcs_remove(self, path): + if not '.arch-ids' in path: + self._u_invoke_client("delete-id", path) + def _rcs_update(self, path): + pass + def _rcs_get_file_contents(self, path, revision=None): + if revision == None: + return file(self._u_abspath(path), "rb").read() + else: + status,output,error = \ + self._invoke_client("file-find", path, revision) + path = output.rstrip('\n') + return file(self._u_abspath(path), "rb").read() + def _rcs_duplicate_repo(self, directory, revision=None): + if revision == None: + RCS._rcs_duplicate_repo(self, directory, revision) + else: + status,output,error = \ + self._u_invoke_client("get", revision,directory) + def _rcs_commit(self, commitfile): + summary,body = self._u_parse_commitfile(commitfile) + #status,output,error = self._invoke_client("make-log") + if body == None: + status,output,error \ + = self._u_invoke_client("commit","--summary",summary) + else: + status,output,error \ + = self._u_invoke_client("commit","--summary",summary, + "--log-message",body) + revision = None + revline = re.compile("[*] committed (.*)") + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 1 + revpath = match.groups()[0] + assert not " " in revpath, revpath + assert revpath.startswith(self._archive_project_name()+'--') + revision = revpath[len(self._archive_project_name()+'--'):] + return revpath class CantAddFile(Exception): def __init__(self, file): self.file = file Exception.__init__(self, "Can't automatically add file %s" % file) +class ArchTestCase(RCStestCase): + Class = Arch -def add_dir_rule(rule, dirname, root): - inv_filename = os.path.join(dirname, '.arch-inventory') - file(inv_filename, "ab").write(rule) - if os.path.realpath(inv_filename) not in list_added(root): - add_id(inv_filename, paranoid=False) - -def force_source(filename, root): - rule = "source %s\n" % rel_filename(filename, root) - add_dir_rule(rule, os.path.dirname(filename), root) - if os.path.realpath(filename) not in list_added(root): - raise CantAddFile(filename) - -def add_id(filename, paranoid=False): - invoke_client("add-id", filename) - root = tree_root(filename) - if paranoid and os.path.realpath(filename) not in list_added(root): - force_source(filename, root) - - -def delete_id(filename): - invoke_client("delete-id", filename) - -def test_helper(type): - t = temp_arch_tree(type) - dirname = os.path.join(t, ".boo") - return dirname, t - -def mkdir(path, paranoid=False): - """ - >>> import shutil - >>> dirname,t = test_helper("easy") - >>> mkdir(dirname, paranoid=False) - >>> assert os.path.realpath(dirname) in list_added(t) - >>> assert not os.path.exists(os.path.join(t, ".arch-inventory")) - >>> shutil.rmtree(t) - >>> dirname,t = test_helper("tricky") - >>> mkdir(dirname, paranoid=True) - >>> assert os.path.realpath(dirname) in list_added(t) - >>> assert os.path.exists(os.path.join(t, ".arch-inventory")) - >>> shutil.rmtree(t) - >>> dirname,t = test_helper("impossible") - >>> try: - ... mkdir(dirname, paranoid=True) - ... except CantAddFile, e: - ... print "Can't add file" - Can't add file - >>> shutil.rmtree(t) - """ - os.mkdir(path) - add_id(path, paranoid=paranoid) - -def set_file_contents(path, contents): - add = not os.path.exists(path) - file(path, "wb").write(contents) - if add: - add_id(path) - - -def path_in_reference(bug_dir, spec): - if spec is not None: - return invoke_client("file-find", bug_dir, spec).rstrip('\n') - return invoke_client("file-find", bug_dir).rstrip('\n') - - -def unlink(path): - try: - os.unlink(path) - delete_id(path) - except OSError, e: - if e.errno != 2: - raise - - -def detect(path): - """Detect whether a directory is revision-controlled using Arch""" - path = os.path.realpath(path) - old_path = None - while True: - if os.path.exists(os.path.join(path, "{arch}")): - return True - if path == old_path: - return False - old_path = path - path = os.path.join('..', path) - -def precommit(directory): - pass - -def commit(directory, summary, body=None): - pass - -def postcommit(directory): - pass - - -name = "Arch" +unitsuite = unittest.TestLoader().loadTestsFromTestCase(ArchTestCase) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/beuuid.py b/libbe/beuuid.py new file mode 100644 index 0000000..e2435ea --- /dev/null +++ b/libbe/beuuid.py @@ -0,0 +1,62 @@ +# 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 +""" +Backwards compatibility support for Python 2.4. Once people give up +on 2.4 ;), the uuid call should be merged into bugdir.py +""" + +import unittest + +try: + from uuid import uuid4 # Python >= 2.5 + def uuid_gen(): + id = uuid4() + idstr = id.urn + start = "urn:uuid:" + assert idstr.startswith(start) + return idstr[len(start):] +except ImportError: + import os + import sys + from subprocess import Popen, PIPE + + def uuid_gen(): + # Shell-out to system uuidgen + args = ['uuidgen', 'r'] + try: + if sys.platform != "win32": + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) + else: + # win32 don't have os.execvp() so have to run command in a shell + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, + shell=True, cwd=cwd) + except OSError, e : + strerror = "%s\nwhile executing %s" % (e.args[1], args) + raise OSError, strerror + output, error = q.communicate() + status = q.wait() + if status != 0: + strerror = "%s\nwhile executing %s" % (status, args) + raise Exception, strerror + return output.rstrip('\n') + +class UUIDtestCase(unittest.TestCase): + def testUUID_gen(self): + id = uuid_gen() + self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) + +suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) diff --git a/libbe/bug.py b/libbe/bug.py new file mode 100644 index 0000000..c75c968 --- /dev/null +++ b/libbe/bug.py @@ -0,0 +1,351 @@ +# 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 +import os +import os.path +import errno +import time +import doctest + +from beuuid import uuid_gen +import mapfile +import comment +import utility + + +### Define and describe valid bug categories +# Use a tuple of (category, description) tuples since we don't have +# ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/ + +# in order of increasing severity +severity_level_def = ( + ("wishlist","A feature that could improve usefullness, but not a bug."), + ("minor","The standard bug level."), + ("serious","A bug that requires workarounds."), + ("critical","A bug that prevents some features from working at all."), + ("fatal","A bug that makes the package unusable.")) + +# in order of increasing resolution +# roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html +active_status_def = ( + ("unconfirmed","A possible bug which lacks independent existance confirmation."), + ("open","A working bug that has not been assigned to a developer."), + ("assigned","A working bug that has been assigned to a developer."), + ("test","The code has been adjusted, but the fix is still being tested.")) +inactive_status_def = ( + ("closed", "The bug is no longer relevant."), + ("fixed", "The bug should no longer occur."), + ("wontfix","It's not a bug, it's a feature."), + ("disabled", "?")) + + +### 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_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) +status_index = {} +for i in range(len(status_values)): + status_index[status_values[i]] = i + + +def checked_property(name, valid): + """ + Provide access to an attribute name, testing for valid values. + """ + 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) + + +class Bug(object): + severity = checked_property("severity", severity_values) + status = checked_property("status", status_values) + + def _get_active(self): + return self.status in active_status_values + + active = property(_get_active) + + def __init__(self, bugdir=None, uuid=None, from_disk=False, + load_comments=False, summary=None): + self.bugdir = bugdir + if bugdir != None: + self.rcs = bugdir.rcs + else: + self.rcs = None + if from_disk == True: + self._comments_loaded = False + self.uuid = uuid + self.load(load_comments=load_comments) + else: + # Note: defaults should match those in Bug.load() + self._comments_loaded = True + if uuid != None: + self.uuid = uuid + else: + self.uuid = uuid_gen() + self.summary = summary + 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) + + def __repr__(self): + return "Bug(uuid=%r)" % self.uuid + + def string(self, shortlist=False, show_comments=False): + if self.bugdir == None: + shortname = self.uuid + else: + shortname = self.bugdir.bug_shortname(self) + if shortlist == False: + if self.time == None: + timestring = "" + else: + htime = utility.handy_time(self.time) + ftime = utility.time_to_str(self.time) + timestring = "%s (%s)" % (htime, ftime) + info = [("ID", self.uuid), + ("Short name", shortname), + ("Severity", self.severity), + ("Status", self.status), + ("Assigned", self.assigned), + ("Target", self.target), + ("Creator", self.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') + else: + statuschar = self.status[0] + severitychar = self.severity[0] + chars = "%c%c" % (statuschar, severitychar) + bugout = "%s:%s: %s" % (shortname, chars, self.summary.rstrip('\n')) + + if show_comments == True: + if self._comments_loaded == False: + self.load_comments() + comout = self.comment_root.string_thread(auto_name_map=True, + bug_shortname=shortname) + output = bugout + '\n' + comout.rstrip('\n') + else : + output = bugout + return output + + def __str__(self): + return self.string(shortlist=True) + + def __cmp__(self, other): + return cmp_full(self, other) + + def get_path(self, name=None): + my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid) + if name is None: + return my_dir + assert name in ["values", "comments"] + return os.path.join(my_dir, name) + + def load(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 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): + 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) + + if self._comments_loaded: + 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 new_comment(self, body=None): + comm = comment.comment_root.new_reply(body=body) + return comm + + def comment_from_shortname(self, shortname, *args, **kwargs): + return self.comment_root.comment_from_shortname(shortname, *args, **kwargs) + + def comment_from_uuid(self, uuid): + return self.comment_root.comment_from_uuid(uuid) + + +# the general rule for bug sorting is that "more important" bugs are +# less than "less important" bugs. This way sorting a list of bugs +# will put the most important bugs first in the list. When relative +# importance is unclear, the sorting follows some arbitrary convention +# (i.e. dictionary order). + +def cmp_severity(bug_1, bug_2): + """ + Compare the severity levels of two bugs, with more severe bugs + comparing as less. + >>> bugA = Bug() + >>> bugB = Bug() + >>> bugA.severity = bugB.severity = "wishlist" + >>> cmp_severity(bugA, bugB) == 0 + True + >>> bugB.severity = "minor" + >>> cmp_severity(bugA, bugB) > 0 + True + >>> bugA.severity = "critical" + >>> cmp_severity(bugA, bugB) < 0 + True + """ + if not hasattr(bug_2, "severity") : + return 1 + return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity]) + +def cmp_status(bug_1, bug_2): + """ + Compare the status levels of two bugs, with more 'open' bugs + comparing as less. + >>> bugA = Bug() + >>> bugB = Bug() + >>> bugA.status = bugB.status = "open" + >>> cmp_status(bugA, bugB) == 0 + True + >>> bugB.status = "closed" + >>> cmp_status(bugA, bugB) < 0 + True + >>> bugA.status = "fixed" + >>> cmp_status(bugA, bugB) > 0 + True + """ + if not hasattr(bug_2, "status") : + return 1 + val_2 = status_index[bug_2.status] + return cmp(status_index[bug_1.status], status_index[bug_2.status]) + +def cmp_attr(bug_1, bug_2, attr, invert=False): + """ + Compare a general attribute between two bugs using the conventional + comparison rule for that attribute type. If invert == True, sort + *against* that convention. + >>> attr="severity" + >>> bugA = Bug() + >>> bugB = Bug() + >>> bugA.severity = "critical" + >>> bugB.severity = "wishlist" + >>> cmp_attr(bugA, bugB, attr) < 0 + True + >>> cmp_attr(bugA, bugB, attr, invert=True) > 0 + True + >>> bugB.severity = "critical" + >>> cmp_attr(bugA, bugB, attr) == 0 + True + """ + if not hasattr(bug_2, attr) : + return 1 + if invert == True : + return -cmp(getattr(bug_1, attr), getattr(bug_2, attr)) + else : + return cmp(getattr(bug_1, attr), getattr(bug_2, attr)) + +# alphabetical rankings (a < z) +cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator") +cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned") +# chronological rankings (newer < older) +cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True) + +def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned, + cmp_time,cmp_creator)): + for comparison in cmp_list : + val = comparison(bug_1, bug_2) + if val != 0 : + return val + return 0 + +class InvalidValue(ValueError): + def __init__(self, name, value): + msg = "Cannot assign value %s to %s" % (value, name) + Exception.__init__(self, msg) + self.name = name + self.value = value + +suite = doctest.DocTestSuite() diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 427ed38..7e4cf3e 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -16,13 +16,17 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import os.path -import cmdutil import errno -import names -import mapfile import time +import copy +import unittest +import doctest + +import mapfile +import bug import utility -from rcs import rcs_by_name +import rcs + class NoBugDir(Exception): def __init__(self, path): @@ -30,48 +34,6 @@ class NoBugDir(Exception): Exception.__init__(self, msg) self.path = path - -def iter_parent_dirs(cur_dir): - cur_dir = os.path.realpath(cur_dir) - old_dir = None - while True: - yield cur_dir - old_dir = cur_dir - cur_dir = os.path.normpath(os.path.join(cur_dir, '..')) - if old_dir == cur_dir: - break; - - -def tree_root(dir, old_version=False): - for rootdir in iter_parent_dirs(dir): - versionfile=os.path.join(rootdir, ".be", "version") - if os.path.exists(versionfile): - if not old_version: - test_version(versionfile) - return BugDir(os.path.join(rootdir, ".be")) - elif not os.path.exists(rootdir): - raise NoRootEntry(rootdir) - old_rootdir = rootdir - rootdir=os.path.join('..', rootdir) - - raise NoBugDir(dir) - -class BadTreeVersion(Exception): - def __init__(self, version): - Exception.__init__(self, "Unsupported tree version: %s" % version) - self.version = version - -def test_version(path): - tree_version = file(path, "rb").read() - if tree_version != TREE_VERSION_STRING: - raise BadTreeVersion(tree_version) - -def set_version(path, rcs): - rcs.set_file_contents(os.path.join(path, "version"), TREE_VERSION_STRING) - - -TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n" - class NoRootEntry(Exception): def __init__(self, path): self.path = path @@ -83,363 +45,456 @@ class AlreadyInitialized(Exception): Exception.__init__(self, "Specified root is already initialized: %s" % path) -def create_bug_dir(path, rcs): - """ - >>> import no_rcs, tests - >>> create_bug_dir('/highly-unlikely-to-exist', no_rcs) - Traceback (most recent call last): - NoRootEntry: Specified root does not exist: /highly-unlikely-to-exist - >>> test_dir = os.path.dirname(tests.bug_arch_dir().dir) - >>> try: - ... create_bug_dir(test_dir, no_rcs) - ... except AlreadyInitialized, e: - ... print "Already Initialized" - Already Initialized - """ - root = os.path.join(path, ".be") - try: - rcs.mkdir(root, paranoid=True) - except OSError, e: - if e.errno == errno.ENOENT: - raise NoRootEntry(path) - elif e.errno == errno.EEXIST: - raise AlreadyInitialized(path) - else: - raise - rcs.mkdir(os.path.join(root, "bugs")) - set_version(root, rcs) - map_save(rcs, os.path.join(root, "settings"), {"rcs_name": rcs.name}) - return BugDir(os.path.join(path, ".be")) - - -def setting_property(name, valid=None): - def getter(self): - value = self.settings.get(name) - if valid is not None: - if value not in valid: - raise InvalidValue(name, value) - return value - - def setter(self, value): - if valid is not None: - if value not in valid and value is not None: - raise InvalidValue(name, value) - if value is None: - del self.settings[name] - else: - self.settings[name] = value - self.save_settings() - return property(getter, setter) - - -class BugDir: - def __init__(self, dir): - self.dir = dir - self.bugs_path = os.path.join(self.dir, "bugs") - try: - self.settings = map_load(os.path.join(self.dir, "settings")) - except NoSuchFile: - self.settings = {"rcs_name": "None"} - - rcs_name = setting_property("rcs_name", ("None", "bzr", "git", "Arch", "hg")) - _rcs = None - - target = setting_property("target") - - def save_settings(self): - map_save(self.rcs, os.path.join(self.dir, "settings"), self.settings) - - def get_rcs(self): - if self._rcs is not None and self.rcs_name == self._rcs.name: - return self._rcs - self._rcs = rcs_by_name(self.rcs_name) - return self._rcs - - rcs = property(get_rcs) - - def get_reference_bugdir(self, spec): - return BugDir(self.rcs.path_in_reference(self.dir, spec)) - - def list(self): - for uuid in self.list_uuids(): - yield self.get_bug(uuid) - - def bug_map(self): - bugs = {} - for bug in self.list(): - bugs[bug.uuid] = bug - return bugs - - def get_bug(self, uuid): - return Bug(self.bugs_path, uuid, self.rcs_name) - - def list_uuids(self): - for uuid in os.listdir(self.bugs_path): - if (uuid.startswith('.')): - continue - yield uuid - - def new_bug(self, uuid=None): - if uuid is None: - uuid = names.uuid() - path = os.path.join(self.bugs_path, uuid) - self.rcs.mkdir(path) - bug = Bug(self.bugs_path, None, self.rcs_name) - bug.uuid = uuid - return bug - -class InvalidValue(Exception): +class InvalidValue(ValueError): def __init__(self, name, value): msg = "Cannot assign value %s to %s" % (value, name) Exception.__init__(self, msg) self.name = name self.value = value +class MultipleBugMatches(ValueError): + def __init__(self, shortname, matches): + msg = ("More than one bug matches %s. " + "Please be more specific.\n%s" % shortname, matches) + ValueError.__init__(self, msg) + self.shortname = shortnamename + self.matches = matches -def checked_property(name, valid): - def getter(self): - value = self.__getattribute__("_"+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 self.__setattr__("_"+name, value) - return property(getter, setter) +TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n" -severity_levels = ("wishlist", "minor", "serious", "critical", "fatal") -active_status = ("open", "in-progress", "waiting", "new", "verified") -inactive_status = ("closed", "disabled", "fixed", "wontfix", "waiting") -severity_value = {} -for i in range(len(severity_levels)): - severity_value[severity_levels[i]] = i +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 + + 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) + + return property(getter, setter, doc=doc) -class Bug(object): - status = checked_property("status", (None,)+active_status+inactive_status) - severity = checked_property("severity", (None, "wishlist", "minor", - "serious", "critical", "fatal")) - def __init__(self, path, uuid, rcs_name): - self.path = path - self.uuid = uuid - if uuid is not None: - dict = map_load(self.get_path("values")) +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, + etc. Once this sub-directory has been created (possibly by + another BugDir instance) any changes to the BugDir in memory will + be flushed to the file system automatically. However, the BugDir + will only load information from the file system when it loads new + bugs/comments that it doesn't already have in memory, or when it + explicitly asked to do so (e.g. .load() or __init__(from_disk=True)). + """ + def __init__(self, root=None, sink_to_existing_root=True, + assert_new_BugDir=False, allow_rcs_init=False, + from_disk=False, rcs=None): + list.__init__(self) + self._save_user_id = False + self.settings = {} + if root == None: + root = os.getcwd() + if sink_to_existing_root == True: + self.root = self._find_root(root) else: - dict = {} - - self.rcs_name = rcs_name - - self.summary = dict.get("summary") - self.creator = dict.get("creator") - self.target = dict.get("target") - self.status = dict.get("status") - self.severity = dict.get("severity") - self.assigned = dict.get("assigned") - self.time = dict.get("time") - if self.time is not None: - self.time = utility.str_to_time(self.time) - - def __repr__(self): - return "Bug(uuid=%r)" % self.uuid - - def get_path(self, file): - return os.path.join(self.path, self.uuid, file) + if not os.path.exists(root): + raise NoRootEntry(root) + self.root = root + if from_disk == True: + self.load() + else: + 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() + + def _find_root(self, path): + """ + Search for an existing bug database dir and it's ancestors and + return a BugDir rooted there. + """ + if not os.path.exists(path): + raise NoRootEntry(path) + versionfile = utility.search_parent_directories(path, os.path.join(".be", "version")) + if versionfile != None: + beroot = os.path.dirname(versionfile) + root = os.path.dirname(beroot) + return root + else: + beroot = utility.search_parent_directories(path, ".be") + if beroot == None: + raise NoBugDir(path) + return beroot + + def get_version(self, path=None): + if self.rcs_name == None: + # Use a temporary RCS to check the version for the first time + RCS = rcs.rcs_by_name("None") + RCS.root(self.root) + else: + RCS = self.rcs - def _get_active(self): - return self.status in active_status + if path == None: + path = self.get_path("version") + tree_version = RCS.get_file_contents(path) + return tree_version - active = property(_get_active) + def set_version(self): + self.rcs.set_file_contents(self.get_path("version"), + TREE_VERSION_STRING) - def add_attr(self, map, name): - value = self.__getattribute__(name) - if value is not None: - map[name] = value + 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.""") - def save(self): - 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) - path = self.get_path("values") - map_save(rcs_by_name(self.rcs_name), path, map) + _rcs = None def _get_rcs(self): - return rcs_by_name(self.rcs_name) - - rcs = property(_get_rcs) - - def new_comment(self): - if not os.path.exists(self.get_path("comments")): - self.rcs.mkdir(self.get_path("comments")) - comm = Comment(None, self) - comm.uuid = names.uuid() - return comm - - def get_comment(self, uuid): - return Comment(uuid, self) - - def iter_comment_ids(self): - path = self.get_path("comments") - if not os.path.isdir(path): - return - try: - for uuid in os.listdir(path): - if (uuid.startswith('.')): - continue - yield uuid - except OSError, e: - if e.errno != errno.ENOENT: - raise - return + return self._rcs - def list_comments(self): - comments = [Comment(id, self) for id in self.iter_comment_ids()] - comments.sort(cmp_date) - return comments - -def cmp_date(comm1, comm2): - return cmp(comm1.date, comm2.date) - -def new_bug(dir, uuid=None): - bug = dir.new_bug(uuid) - bug.creator = names.creator() - bug.severity = "minor" - bug.status = "open" - bug.time = time.time() - return bug - -def new_comment(bug, body=None): - comm = bug.new_comment() - comm.From = names.creator() - comm.date = time.time() - comm.body = body - return comm - -def add_headers(obj, map, names): - map_names = {} - for name in names: - map_names[name] = pyname_to_header(name) - add_attrs(obj, map, names, map_names) - -def add_attrs(obj, map, names, map_names=None): - if map_names is None: - map_names = {} - for name in names: - map_names[name] = name - - for name in names: - value = obj.__getattribute__(name) - if value is not None: - map[map_names[name]] = value - - -class Comment(object): - def __init__(self, uuid, bug): - object.__init__(self) - self.uuid = uuid - self.bug = bug - if self.uuid is not None and self.bug is not None: - mapfile = map_load(self.get_path("values")) - self.date = utility.str_to_time(mapfile["Date"]) - self.From = mapfile["From"] - self.in_reply_to = mapfile.get("In-reply-to") - self.content_type = mapfile.get("Content-type", "text/plain") - self.body = file(self.get_path("body")).read().decode("utf-8") + 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: + return my_dir + assert args[0] in ["version", "settings", "bugs"], str(args) + return os.path.join(my_dir, *args) + + def _guess_rcs(self, allow_rcs_init=False): + deepdir = self.get_path() + if not os.path.exists(deepdir): + deepdir = os.path.dirname(deepdir) + new_rcs = rcs.detect_rcs(deepdir) + install = False + if new_rcs.name == "None": + if allow_rcs_init == True: + new_rcs = rcs.installed_rcs() + new_rcs.init(self.root) + self.rcs = new_rcs + return new_rcs + + def load(self): + version = self.get_version() + if version != TREE_VERSION_STRING: + raise NotImplementedError, \ + "BugDir cannot handle version '%s' yet." % version else: - self.date = None - self.From = None - self.in_reply_to = None - self.content_type = "text/plain" - self.body = None - - def save(self): - map_file = {"Date": utility.time_to_str(self.date)} - add_headers(self, map_file, ("From", "in_reply_to", "content_type")) - if not os.path.exists(self.get_path(None)): - self.bug.rcs.mkdir(self.get_path(None)) - map_save(self.bug.rcs, self.get_path("values"), map_file) - self.bug.rcs.set_file_contents(self.get_path("body"), - self.body.encode('utf-8')) + if not os.path.exists(self.get_path()): + raise NoBugDir(self.get_path()) + self.settings = self._get_settings(self.get_path("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() - def get_path(self, name): - my_dir = os.path.join(self.bug.get_path("comments"), self.uuid) - if name is None: - return my_dir - return os.path.join(my_dir, name) - - -def thread_comments(comments): - child_map = {} - top_comments = [] - for comment in comments: - child_map[comment.uuid] = [] - for comment in comments: - if comment.in_reply_to is None or comment.in_reply_to not in child_map: - top_comments.append(comment) - continue - child_map[comment.in_reply_to].append(comment) - - def recurse_children(comment): - child_list = [] - for child in child_map[comment.uuid]: - child_list.append(recurse_children(child)) - return (comment, child_list) - return [recurse_children(c) for c in top_comments] - - -def pyname_to_header(name): - return name.capitalize().replace('_', '-') - - -def map_save(rcs, path, map): - """Save the map as a mapfile to the specified path""" - add = not os.path.exists(path) - output = file(path, "wb") - mapfile.generate(output, map) - if add: - rcs.add_id(path) - -class NoSuchFile(Exception): - def __init__(self, pathname): - Exception.__init__(self, "No such file: %s" % pathname) - - -def map_load(path): - try: - return mapfile.parse(file(path, "rb")) - except IOError, e: - if e.errno != errno.ENOENT: - raise e - raise NoSuchFile(path) + def load_all_bugs(self): + "Warning: this could take a while." + self._clear_bugs() + for uuid in self.list_uuids(): + self._load_bug(uuid) + def save(self): + self.rcs.mkdir(self.get_path()) + self.set_version() + self._save_settings(self.get_path("settings"), self.settings) + self.rcs.mkdir(self.get_path("bugs")) + for bug in self: + bug.save() + + 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=True should only be for the special case of + # configuring duplicate bugdir settings + + try: + settings = mapfile.map_load(RCS, settings_path, allow_no_rcs) + except rcs.NoSuchFile: + settings = {"rcs_name": "None"} + return 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 + mapfile.map_save(self.rcs, settings_path, settings, allow_no_rcs) + + def duplicate_bugdir(self, revision): + duplicate_path = self.rcs.duplicate_repo(revision) + + # setup revision RCS as None, since the duplicate may not be + # initialized for versioning + duplicate_settings_path = os.path.join(duplicate_path, + ".be", "settings") + duplicate_settings = self._get_settings(duplicate_settings_path) + if "rcs_name" in duplicate_settings: + duplicate_settings["rcs_name"] = "None" + duplicate_settings["user-id"] = self.user_id + self._save_settings(duplicate_settings_path, duplicate_settings) + + return BugDir(duplicate_path, from_disk=True) + + 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 -class MockBug: - def __init__(self, severity): - self.severity = severity + def list_uuids(self): + uuids = [] + if os.path.exists(self.get_path()): + # list the uuids on disk + for uuid in os.listdir(self.get_path("bugs")): + if not (uuid.startswith('.')): + uuids.append(uuid) + yield uuid + # and the ones that are still just in memory + for bug in self: + if bug.uuid not in uuids: + uuids.append(bug.uuid) + yield bug.uuid + + def _clear_bugs(self): + while len(self) > 0: + self.pop() + + def _load_bug(self, uuid): + bg = bug.Bug(bugdir=self, uuid=uuid, from_disk=True) + self.append(bg) + self._bug_map_gen() + return bg + + def new_bug(self, uuid=None, summary=None): + bg = bug.Bug(bugdir=self, uuid=uuid, summary=summary) + self.append(bg) + self._bug_map_gen() + return bg + + def remove_bug(self, bug): + self.remove(bug) + bug.remove() + + def bug_shortname(self, bug): + """ + Generate short names from uuids. Picks the minimum number of + characters (>=3) from the beginning of the uuid such that the + short names are unique. + + Obviously, as the number of bugs in the database grows, these + short names will cease to be unique. The complete uuid should be + used for long term reference. + """ + chars = 3 + for uuid in self._bug_map.keys(): + if bug.uuid == uuid: + continue + while (bug.uuid[:chars] == uuid[:chars]): + chars+=1 + return bug.uuid[:chars] + + def bug_from_shortname(self, shortname): + """ + >>> bd = simple_bug_dir() + >>> bug_a = bd.bug_from_shortname('a') + >>> print type(bug_a) + <class 'libbe.bug.Bug'> + >>> print bug_a + a:om: Bug A + """ + matches = [] + self._bug_map_gen() + for uuid in self._bug_map.keys(): + if uuid.startswith(shortname): + matches.append(uuid) + if len(matches) > 1: + raise MultipleBugMatches(shortname, matches) + if len(matches) == 1: + return self.bug_from_uuid(matches[0]) + raise KeyError("No bug matches %s" % shortname) + + def bug_from_uuid(self, uuid): + if not self.has_bug(uuid): + raise KeyError("No bug matches %s\n bug map: %s\n root: %s" \ + % (uuid, self._bug_map, self.root)) + if self._bug_map[uuid] == None: + self._load_bug(uuid) + return self._bug_map[uuid] + + def has_bug(self, bug_uuid): + if bug_uuid not in self._bug_map: + self._bug_map_gen() + if bug_uuid not in self._bug_map: + return False + return True + -def cmp_severity(bug_1, bug_2): +def simple_bug_dir(): """ - Compare the severity levels of two bugs, with more sever bugs comparing - as less. - - >>> cmp_severity(MockBug(None), MockBug(None)) - 0 - >>> cmp_severity(MockBug("wishlist"), MockBug(None)) < 0 - True - >>> cmp_severity(MockBug(None), MockBug("wishlist")) > 0 - True - >>> cmp_severity(MockBug("critical"), MockBug("wishlist")) < 0 - True + For testing + >>> bugdir = simple_bug_dir() + >>> ls = list(bugdir.list_uuids()) + >>> ls.sort() + >>> print ls + ['a', 'b'] """ - val_1 = severity_value.get(bug_1.severity) - val_2 = severity_value.get(bug_2.severity) - return -cmp(val_1, val_2) + dir = utility.Dir() + assert os.path.exists(dir.path) + bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True) + 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>" + bug_a.time = 0 + bug_b = bugdir.new_bug("b", summary="Bug B") + bug_b.creator = "Jane Doe <jdoe@example.com>" + bug_b.time = 0 + bug_b.status = "closed" + bugdir.save() + return bugdir + + +class BugDirTestCase(unittest.TestCase): + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + def setUp(self): + self.dir = utility.Dir() + self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, + allow_rcs_init=True) + self.rcs = self.bugdir.rcs + def tearDown(self): + self.rcs.cleanup() + self.dir.cleanup() + def fullPath(self, path): + return os.path.join(self.dir.path, path) + def assertPathExists(self, path): + fullpath = self.fullPath(path) + self.failUnless(os.path.exists(fullpath)==True, + "path %s does not exist" % fullpath) + self.assertRaises(AlreadyInitialized, BugDir, + self.dir.path, assertNewBugDir=True) + def versionTest(self): + if self.rcs.versioned == False: + return + original = self.bugdir.rcs.commit("Began versioning") + bugA = self.bugdir.bug_from_uuid("a") + bugA.status = "fixed" + self.bugdir.save() + new = self.rcs.commit("Fixed bug a") + dupdir = self.bugdir.duplicate_bugdir(original) + self.failUnless(dupdir.root != self.bugdir.root, + "%s, %s" % (dupdir.root, self.bugdir.root)) + bugAorig = dupdir.bug_from_uuid("a") + self.failUnless(bugA != bugAorig, + "\n%s\n%s" % (bugA.string(), bugAorig.string())) + bugAorig.status = "fixed" + self.failUnless(bug.cmp_status(bugA, bugAorig)==0, + "%s, %s" % (bugA.status, bugAorig.status)) + self.failUnless(bug.cmp_severity(bugA, bugAorig)==0, + "%s, %s" % (bugA.severity, bugAorig.severity)) + self.failUnless(bug.cmp_assigned(bugA, bugAorig)==0, + "%s, %s" % (bugA.assigned, bugAorig.assigned)) + self.failUnless(bug.cmp_time(bugA, bugAorig)==0, + "%s, %s" % (bugA.time, bugAorig.time)) + self.failUnless(bug.cmp_creator(bugA, bugAorig)==0, + "%s, %s" % (bugA.creator, bugAorig.creator)) + self.failUnless(bugA == bugAorig, + "\n%s\n%s" % (bugA.string(), bugAorig.string())) + self.bugdir.remove_duplicate_bugdir() + self.failUnless(os.path.exists(dupdir.root)==False, str(dupdir.root)) + def testRun(self): + self.bugdir.new_bug(uuid="a", summary="Ant") + self.bugdir.new_bug(uuid="b", summary="Cockroach") + self.bugdir.new_bug(uuid="c", summary="Praying mantis") + length = len(self.bugdir) + self.failUnless(length == 3, "%d != 3 bugs" % length) + uuids = list(self.bugdir.list_uuids()) + self.failUnless(len(uuids) == 3, "%d != 3 uuids" % len(uuids)) + self.failUnless(uuids == ["a","b","c"], str(uuids)) + bugA = self.bugdir.bug_from_uuid("a") + bugAprime = self.bugdir.bug_from_shortname("a") + self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime)) + self.bugdir.save() + self.versionTest() + +unitsuite = unittest.TestLoader().loadTestsFromTestCase(BugDirTestCase) +suite = unittest.TestSuite([unitsuite])#, doctest.DocTestSuite()]) diff --git a/libbe/bzr.py b/libbe/bzr.py index ddda334..a0ae715 100644 --- a/libbe/bzr.py +++ b/libbe/bzr.py @@ -15,114 +15,84 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os -import tempfile - -import config -from rcs import invoke, CommandError - -def invoke_client(*args, **kwargs): - directory = kwargs['directory'] - expect = kwargs.get('expect', (0, 1)) - cl_args = ["bzr"] - cl_args.extend(args) - status,output,error = invoke(cl_args, expect, cwd=directory) - return status, output - -def add_id(filename, paranoid=False): - invoke_client("add", filename, directory='.') - -def delete_id(filename): - invoke_client("remove", filename, directory='.') - -def mkdir(path, paranoid=False): - os.mkdir(path) - add_id(path) - -def set_file_contents(path, contents): - add = not os.path.exists(path) - file(path, "wb").write(contents) - if add: - add_id(path) - -def lookup_revision(revno, directory): - return invoke_client("lookup-revision", str(revno), - directory=directory)[1].rstrip('\n') - -def export(revno, directory, revision_dir): - invoke_client("export", "-r", str(revno), revision_dir, directory=directory) - -def find_or_make_export(revno, directory): - revision_id = lookup_revision(revno, directory) - home = os.path.expanduser("~") - revision_root = os.path.join(home, ".bzrrevs") - if not os.path.exists(revision_root): - os.mkdir(revision_root) - revision_dir = os.path.join(revision_root, revision_id) - if not os.path.exists(revision_dir): - export(revno, directory, revision_dir) - return revision_dir - -def bzr_root(path): - return invoke_client("root", path, directory=None)[1].rstrip('\r') - -def path_in_reference(bug_dir, spec): - if spec is None: - spec = int(invoke_client("revno", directory=bug_dir)[1]) - rel_bug_dir = bug_dir[len(bzr_root(bug_dir)):] - export_root = find_or_make_export(spec, directory=bug_dir) - return os.path.join(export_root, rel_bug_dir) - - -def unlink(path): - try: - os.unlink(path) - delete_id(path) - except OSError, e: - if e.errno != 2: - raise - - -def detect(path): - """Detect whether a directory is revision-controlled using bzr""" - path = os.path.realpath(path) - old_path = None - while True: - if os.path.exists(os.path.join(path, ".bzr")): +import re +import unittest +import doctest + +from rcs import RCS, RCStestCase, CommandError + +def new(): + return Bzr() + +class Bzr(RCS): + name = "bzr" + client = "bzr" + versioned = True + def _rcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _rcs_detect(self, path): + if self._u_search_parent_directories(path, ".bzr") != None : return True - if path == old_path: - return False - old_path = path - path = os.path.dirname(path) - -def precommit(directory): - pass - -def commit(directory, summary, body=None): - if body is not None: - summary += '\n' + body - descriptor, filename = tempfile.mkstemp() - try: - temp_file = os.fdopen(descriptor, 'wb') - temp_file.write(summary) - temp_file.close() - invoke_client('commit', '--unchanged', '--file', filename, - directory=directory) - finally: - os.unlink(filename) - -def postcommit(directory): - try: - invoke_client('merge', directory=directory) - except CommandError, e: - if ('No merge branch known or specified' in e.err_str or - 'No merge location known or specified' in e.err_str): - pass + return False + def _rcs_root(self, path): + """Find the root of the deepest repository containing path.""" + status,output,error = self._u_invoke_client("root", path) + return output.rstrip('\n') + def _rcs_init(self, path): + self._u_invoke_client("init", directory=path) + def _rcs_get_user_id(self): + status,output,error = self._u_invoke_client("whoami") + return output.rstrip('\n') + def _rcs_set_user_id(self, value): + self._u_invoke_client("whoami", value) + def _rcs_add(self, path): + self._u_invoke_client("add", path) + def _rcs_remove(self, path): + # --force to also remove unversioned files. + self._u_invoke_client("remove", "--force", path) + def _rcs_update(self, path): + pass + def _rcs_get_file_contents(self, path, revision=None): + if revision == None: + return file(os.path.join(self.rootdir, path), "rb").read() + else: + status,output,error = \ + self._u_invoke_client("cat","-r",revision,path) + return output + def _rcs_duplicate_repo(self, directory, revision=None): + if revision == None: + RCS._rcs_duplicate_repo(self, directory, revision) else: - status = invoke_client('revert', '--no-backup', + self._u_invoke_client("branch", "--revision", revision, + ".", directory) + def _rcs_commit(self, commitfile): + status,output,error = self._u_invoke_client("commit", "--unchanged", + "--file", commitfile) + revision = None + revline = re.compile("Committed revision (.*)[.]") + match = revline.search(error) + assert match != None, output+error + assert len(match.groups()) == 1 + revision = match.groups()[0] + return revision + def postcommit(self): + try: + self._u_invoke_client('merge') + except CommandError, e: + if ('No merge branch known or specified' in e.err_str or + 'No merge location known or specified' in e.err_str): + pass + else: + self._u_invoke_client('revert', '--no-backup', directory=directory) - status = invoke_client('resolve', '--all', directory=directory) - raise - if len(invoke_client('status', directory=directory)[1]) > 0: - commit(directory, 'Merge from upstream') - -name = "bzr" + self._u_invoke_client('resolve', '--all', directory=directory) + raise + if len(self._u_invoke_client('status', directory=directory)[1]) > 0: + self.commit('Merge from upstream') + +class BzrTestCase(RCStestCase): + Class = Bzr + +unitsuite = unittest.TestLoader().loadTestsFromTestCase(BzrTestCase) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index 079601e..6d7ab01 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -14,25 +14,16 @@ # You 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 bugdir -import plugin -import locale -import os import optparse +import os +import locale from textwrap import TextWrapper from StringIO import StringIO -import utility +import doctest -def unique_name(bug, bugs): - chars = 1 - for some_bug in bugs: - if bug.uuid == some_bug.uuid: - continue - while (bug.uuid[:chars] == some_bug.uuid[:chars]): - chars+=1 - if chars < 3: - chars = 3 - return bug.uuid[:chars] +import bugdir +import plugin +import utility class UserError(Exception): def __init__(self, msg): @@ -43,45 +34,6 @@ class UserErrorWrap(UserError): UserError.__init__(self, str(exception)) self.exception = exception -def get_bug(spec, bug_dir=None): - matches = [] - try: - if bug_dir is None: - bug_dir = bugdir.tree_root('.') - except bugdir.NoBugDir, e: - raise UserErrorWrap(e) - bugs = list(bug_dir.list()) - for bug in bugs: - if bug.uuid.startswith(spec): - matches.append(bug) - if len(matches) > 1: - raise UserError("More than one bug matches %s. Please be more" - " specific." % spec) - if len(matches) == 1: - return matches[0] - - matches = [] - if len(matches) == 0: - raise UserError("No bug matches %s" % spec) - return matches[0] - -def bug_summary(bug, bugs, no_target=False, shortlist=False): - target = bug.target - if target is None or no_target: - target = "" - else: - target = " Target: %s" % target - if bug.assigned is None: - assigned = "" - else: - assigned = " Assigned: %s" % bug.assigned - if shortlist == False: - return " ID: %s\n Severity: %s\n%s%s\n Creator: %s \n%s\n" % \ - (unique_name(bug, bugs), bug.severity, assigned, target, - bug.creator, bug.summary) - else: - return "%4s: %s\n" % (unique_name(bug, bugs), bug.summary) - def iter_commands(): for name, module in plugin.iter_plugins("becommands"): yield name.replace("_", "-"), module @@ -104,9 +56,20 @@ def execute(cmd, args): encoding = locale.getpreferredencoding() or 'ascii' return get_command(cmd).execute([a.decode(encoding) for a in args]) -def help(cmd): - return get_command(cmd).help() - +def help(cmd=None): + if cmd != None: + return get_command(cmd).help() + else: + cmdlist = [] + for name, module in iter_commands(): + cmdlist.append((name, module.__desc__)) + longest_cmd_len = max([len(name) for name,desc in cmdlist]) + ret = ["Bugs Everywhere - Distributed bug tracking\n", + "Supported commands"] + for name, desc in cmdlist: + numExtraSpaces = longest_cmd_len-len(name) + ret.append("be %s%*s %s" % (name, numExtraSpaces, "", desc)) + return "\n".join(ret) class GetHelp(Exception): pass @@ -119,34 +82,6 @@ class UsageError(Exception): def raise_get_help(option, opt, value, parser): raise GetHelp - -def iter_comment_name(bug, unique_name): - """Iterate through id, comment pairs, in date order. - (This is a user-friendly id, not the comment uuid) - """ - def key(comment): - return comment.date - for num, comment in enumerate(sorted(bug.list_comments(), key=key)): - yield ("%s:%d" % (unique_name, num+1), comment) - - -def comment_from_name(bug, unique_name, name): - """Use a comment name to look up a comment""" - for cur_name, comment in iter_comment_name(bug, unique_name): - if name == cur_name: - return comment - raise KeyError(name) - - -def get_bug_and_comment(identifier, bug_dir=None): - ids = identifier.split(':') - bug = get_bug(ids[0], bug_dir) - if len(ids) == 2: - comment = comment_from_name(bug, ids[0], identifier) - else: - comment = None - return bug, comment - class CmdOptionParser(optparse.OptionParser): def __init__(self, usage): @@ -163,10 +98,9 @@ class CmdOptionParser(optparse.OptionParser): self._long_opt.iterkeys()]) def help_str(self): - fs = utility.FileString() - self.print_help(fs) - return fs.str - + f = StringIO() + self.print_help(f) + return f.getvalue() def underlined(instring): """Produces a version of a string that is underlined with '=' @@ -178,53 +112,6 @@ def underlined(instring): return "%s\n%s" % (instring, "="*len(instring)) -def print_threaded_comments(comments, name_map, indent=""): - """Print a threaded display of comments""" - tw = TextWrapper(initial_indent = indent, subsequent_indent = indent, - width=80) - for comment, children in comments: - s = StringIO() - print >> s, "--------- Comment ---------" - print >> s, "Name: %s" % name_map[comment.uuid] - print >> s, "From: %s" % comment.From - print >> s, "Date: %s\n" % utility.time_to_str(comment.date) - print >> s, comment.body.rstrip('\n') - - s.seek(0) - for line in s: - print tw.fill(line).rstrip('\n') - print_threaded_comments(children, name_map, indent=indent+" ") - - -def bug_tree(dir=None): - """Retrieve the bug tree specified by the user. If no directory is - specified, the current working directory is used. - - :param dir: The directory to search for the bug tree in. - - >>> bug_tree() is not None - True - >>> bug_tree("/") - Traceback (most recent call last): - UserErrorWrap: The directory "/" has no bug directory. - """ - if dir is None: - dir = os.getcwd() - try: - return bugdir.tree_root(dir) - except bugdir.NoBugDir, e: - raise UserErrorWrap(e) - -def print_command_list(): - cmdlist = [] - print """Bugs Everywhere - Distributed bug tracking - -Supported commands""" - for name, module in iter_commands(): - cmdlist.append((name, module.__doc__)) - for name, desc in cmdlist: - print "be %s\n %s" % (name, desc) - def _test(): import doctest import sys @@ -232,3 +119,5 @@ def _test(): if __name__ == "__main__": _test() + +suite = doctest.DocTestSuite() diff --git a/libbe/comment.py b/libbe/comment.py new file mode 100644 index 0000000..c89fd9d --- /dev/null +++ b/libbe/comment.py @@ -0,0 +1,386 @@ +# 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 os +import os.path +import time +import textwrap +import doctest + +from beuuid import uuid_gen +import mapfile +from tree import Tree +import utility + +INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!" + +def _list_to_root(comments, bug): + """ + Convert a raw list of comments to single (dummy) root comment. We + use a dummy root comment, because there can be several comment + threads rooted on the same parent bug. To simplify comment + interaction, we condense these threads into a single thread with a + Comment dummy root. + + No Comment method should use the dummy comment. + """ + root_comments = [] + uuid_map = {} + for comment in comments: + assert comment.uuid != None + uuid_map[comment.uuid] = comment + for comm in comments: + if comm.in_reply_to == None: + root_comments.append(comm) + else: + parentUUID = comm.in_reply_to + parent = uuid_map[parentUUID] + parent.add_reply(comm) + dummy_root = Comment(bug, uuid=INVALID_UUID) + dummy_root.extend(root_comments) + return dummy_root + +def loadComments(bug): + path = bug.get_path("comments") + if not os.path.isdir(path): + return Comment(bug, uuid=INVALID_UUID) + comments = [] + for uuid in os.listdir(path): + if uuid.startswith('.'): + continue + comm = Comment(bug, uuid, from_disk=True) + comments.append(comm) + return _list_to_root(comments, bug) + +def saveComments(bug): + path = bug.get_path("comments") + bug.rcs.mkdir(path) + for comment in bug.comment_root.traverse(): + comment.save() + +class Comment(Tree): + 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. + + The uuid option is required when from_disk==True. + + The in_reply_to and body options are only used if + from_disk==False (the default). When from_disk==True, they are + loaded from the bug database. + + in_reply_to should be the uuid string of the parent comment. + """ + Tree.__init__(self) + self.bug = bug + if bug != None: + self.rcs = bug.rcs + else: + self.rcs = None + if from_disk == True: + self.uuid = uuid + self.load() + else: + if uuid != None: + self.uuid = uuid + else: + self.uuid = uuid_gen() + self.time = time.time() + 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): + """Avoid working with the possible dummy root comment""" + for comment in Tree.traverse(self, *args, **kwargs): + if comment.uuid == INVALID_UUID: + continue + yield comment + + def _clean_string(self, value): + """ + >>> comm = Comment() + >>> comm._clean_string(None) + '' + >>> comm._clean_string("abc") + 'abc' + """ + if value == None: + return "" + return value + + 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") + >>> print comm.string(indent=2, shortname="com-1") + --------- Comment --------- + Name: com-1 + From: + Date: Thu, 20 Nov 2008 15:55:11 +0000 + <BLANKLINE> + Some + insightful + remarks + """ + if shortname == None: + shortname = self.uuid + 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("") + #lines.append(textwrap.fill(self._clean_string(self.body), + # width=(79-indent))) + lines.extend(self._clean_string(self.body).splitlines()) + # some comments shouldn't be wrapped... + + istring = ' '*indent + sep = '\n' + istring + return istring + sep.join(lines).rstrip('\n') + + def __str__(self): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> comm.uuid = "com-1" + >>> comm.time = utility.str_to_time("Thu, 20 Nov 2008 15:55:11 +0000") + >>> comm.From = "Jane Doe <jdoe@example.com>" + >>> print comm + --------- Comment --------- + Name: com-1 + From: Jane Doe <jdoe@example.com> + Date: Thu, 20 Nov 2008 15:55:11 +0000 + <BLANKLINE> + Some insightful remarks + """ + return self.string() + + def get_path(self, name=None): + my_dir = os.path.join(self.bug.get_path("comments"), self.uuid) + if name is None: + return my_dir + assert name in ["values", "body"] + return os.path.join(my_dir, name) + + def load(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 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")) + 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 + + 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 + if self.uuid != INVALID_UUID: + reply.in_reply_to = self.uuid + self.append(reply) + + def new_reply(self, body=None): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> repA = comm.new_reply("Critique original comment") + >>> repB = repA.new_reply("Begin flamewar :p") + """ + reply = Comment(self.bug, body=body) + self.add_reply(reply) + return reply + + def string_thread(self, name_map={}, indent=0, + auto_name_map=False, bug_shortname=None): + """ + Return a sting displaying a thread of comments. + bug_shortname is only used if auto_name_map == True. + + >>> a = Comment(bug=None, uuid="a", body="Insightful remarks") + >>> a.time = utility.str_to_time("Thu, 20 Nov 2008 01:00:00 +0000") + >>> b = a.new_reply("Critique original comment") + >>> b.uuid = "b" + >>> b.time = utility.str_to_time("Thu, 20 Nov 2008 02:00:00 +0000") + >>> c = b.new_reply("Begin flamewar :p") + >>> c.uuid = "c" + >>> c.time = utility.str_to_time("Thu, 20 Nov 2008 03:00:00 +0000") + >>> d = a.new_reply("Useful examples") + >>> d.uuid = "d" + >>> d.time = utility.str_to_time("Thu, 20 Nov 2008 04:00:00 +0000") + >>> a.sort(key=lambda comm : comm.time) + >>> print a.string_thread() + --------- Comment --------- + Name: a + From: + Date: Thu, 20 Nov 2008 01:00:00 +0000 + <BLANKLINE> + Insightful remarks + --------- Comment --------- + Name: b + From: + Date: Thu, 20 Nov 2008 02:00:00 +0000 + <BLANKLINE> + Critique original comment + --------- Comment --------- + Name: c + From: + Date: Thu, 20 Nov 2008 03:00:00 +0000 + <BLANKLINE> + Begin flamewar :p + --------- Comment --------- + Name: d + From: + Date: Thu, 20 Nov 2008 04:00:00 +0000 + <BLANKLINE> + Useful examples + >>> print a.string_thread(auto_name_map=True, bug_shortname="bug-1") + --------- Comment --------- + Name: bug-1:1 + From: + Date: Thu, 20 Nov 2008 01:00:00 +0000 + <BLANKLINE> + Insightful remarks + --------- Comment --------- + Name: bug-1:2 + From: + Date: Thu, 20 Nov 2008 02:00:00 +0000 + <BLANKLINE> + Critique original comment + --------- Comment --------- + Name: bug-1:3 + From: + Date: Thu, 20 Nov 2008 03:00:00 +0000 + <BLANKLINE> + Begin flamewar :p + --------- Comment --------- + Name: bug-1:4 + From: + Date: Thu, 20 Nov 2008 04:00:00 +0000 + <BLANKLINE> + Useful examples + """ + if auto_name_map == True: + name_map = {} + for shortname,comment in self.comment_shortnames(bug_shortname): + name_map[comment.uuid] = shortname + stringlist = [] + for depth,comment in self.thread(flatten=True): + 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)) + return '\n'.join(stringlist) + + def comment_shortnames(self, bug_shortname=""): + """ + Iterate through (id, comment) pairs, in time order. + (This is a user-friendly id, not the comment uuid). + + SIDE-EFFECT : will sort the comment tree by comment.time + + >>> a = Comment(bug=None, uuid="a") + >>> b = a.new_reply() + >>> b.uuid = "b" + >>> c = b.new_reply() + >>> c.uuid = "c" + >>> d = a.new_reply() + >>> d.uuid = "d" + >>> for id,name in a.comment_shortnames("bug-1"): + ... print id, name.uuid + bug-1:1 a + bug-1:2 b + bug-1:3 c + bug-1:4 d + """ + self.sort(key=lambda comm : comm.time) + for num,comment in enumerate(self.traverse()): + yield ("%s:%d" % (bug_shortname, num+1), comment) + + def comment_from_shortname(self, comment_shortname, *args, **kwargs): + """ + Use a comment shortname to look up a comment. + >>> a = Comment(bug=None, uuid="a") + >>> b = a.new_reply() + >>> b.uuid = "b" + >>> c = b.new_reply() + >>> c.uuid = "c" + >>> d = a.new_reply() + >>> d.uuid = "d" + >>> comm = a.comment_from_shortname("bug-1:3", bug_shortname="bug-1") + >>> id(comm) == id(c) + True + """ + for cur_name, comment in self.comment_shortnames(*args, **kwargs): + if comment_shortname == cur_name: + return comment + raise KeyError(comment_shortname) + + def comment_from_uuid(self, uuid): + """ + Use a comment shortname to look up a comment. + >>> a = Comment(bug=None, uuid="a") + >>> b = a.new_reply() + >>> b.uuid = "b" + >>> c = b.new_reply() + >>> c.uuid = "c" + >>> d = a.new_reply() + >>> d.uuid = "d" + >>> comm = a.comment_from_uuid("d") + >>> id(comm) == id(d) + True + """ + for comment in self.traverse(): + if comment.uuid == uuid: + return comment + raise KeyError(uuid) + +suite = doctest.DocTestSuite() diff --git a/libbe/config.py b/libbe/config.py index ecc40ce..79c0d6f 100644 --- a/libbe/config.py +++ b/libbe/config.py @@ -16,6 +16,8 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import ConfigParser import os.path +import doctest + def path(): """Return the path to the per-user config file""" return os.path.expanduser("~/.bugs_everywhere") @@ -58,3 +60,5 @@ def get_val(name, section="DEFAULT"): return config.get(section, name) except ConfigParser.NoOptionError: return None + +suite = doctest.DocTestSuite() diff --git a/libbe/diff.py b/libbe/diff.py index c1dc429..86a91ca 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -17,60 +17,60 @@ """Compare two bug trees""" from libbe import cmdutil, bugdir from libbe.utility import time_to_str +from libbe.bug import cmp_severity +import doctest -def diff(old_tree, new_tree): - old_bug_map = old_tree.bug_map() - new_bug_map = new_tree.bug_map() +def diff(old_bugdir, new_bugdir): added = [] removed = [] modified = [] - for old_bug in old_bug_map.itervalues(): - new_bug = new_bug_map.get(old_bug.uuid) - if new_bug is None : - removed.append(old_bug) - else: + for uuid in old_bugdir.list_uuids(): + old_bug = old_bugdir.bug_from_uuid(uuid) + try: + new_bug = new_bugdir.bug_from_uuid(uuid) if old_bug != new_bug: modified.append((old_bug, new_bug)) - for new_bug in new_bug_map.itervalues(): - if not old_bug_map.has_key(new_bug.uuid): + except KeyError: + removed.append(old_bug) + for uuid in new_bugdir.list_uuids(): + if not old_bugdir.has_bug(uuid): + new_bug = new_bugdir.bug_from_uuid(uuid) added.append(new_bug) return (removed, modified, added) - -def reference_diff(bugdir, spec=None): - return diff(bugdir.get_reference_bugdir(spec), bugdir) - def diff_report(diff_data, bug_dir): (removed, modified, added) = diff_data - bugs = list(bug_dir.list()) def modified_cmp(left, right): - return bugdir.cmp_severity(left[1], right[1]) + return cmp_severity(left[1], right[1]) - added.sort(bugdir.cmp_severity) - removed.sort(bugdir.cmp_severity) + added.sort(cmp_severity) + removed.sort(cmp_severity) modified.sort(modified_cmp) - if len(added) > 0: + if len(added) > 0: print "New bug reports:" for bug in added: - print cmdutil.bug_summary(bug, bugs, no_target=True) + print bug.string(shortlist=True) + print "" if len(modified) > 0: printed = False for old_bug, new_bug in modified: - change_str = bug_changes(old_bug, new_bug, bugs) + change_str = bug_changes(old_bug, new_bug, bug_dir) if change_str is None: continue if not printed: printed = True print "Modified bug reports:" print change_str + print "" if len(removed) > 0: print "Removed bug reports:" for bug in removed: - print cmdutil.bug_summary(bug, bugs, no_target=True) - + print bug.string(shortlist=True) + print "" + def change_lines(old, new, attributes): change_list = [] for attr in attributes: @@ -87,24 +87,27 @@ def bug_changes(old, new, bugs): change_list = change_lines(old, new, ("time", "creator", "severity", "target", "summary", "status", "assigned")) - old_comment_ids = list(old.iter_comment_ids()) - new_comment_ids = list(new.iter_comment_ids()) + old_comment_ids = [c.uuid for c in old.comments()] + new_comment_ids = [c.uuid for c in new.comments()] change_strings = ["%s: %s -> %s" % f for f in change_list] for comment_id in new_comment_ids: if comment_id not in old_comment_ids: - summary = comment_summary(new.get_comment(comment_id), "new") + summary = comment_summary(new.comment_from_uuid(comment_id), "new") change_strings.append(summary) for comment_id in old_comment_ids: if comment_id not in new_comment_ids: - summary = comment_summary(new.get_comment(comment_id), "removed") + summary = comment_summary(new.comment_from_uuid(comment_id), + "removed") change_strings.append(summary) if len(change_strings) == 0: return None - return "%s%s\n" % (cmdutil.bug_summary(new, bugs, shortlist=True), - "\n".join(change_strings)) + return "%s\n %s" % (new.string(shortlist=True), + " \n".join(change_strings)) def comment_summary(comment, status): return "%8s comment from %s on %s" % (status, comment.From, - time_to_str(comment.date)) + time_to_str(comment.time)) + +suite = doctest.DocTestSuite() diff --git a/libbe/git.py b/libbe/git.py index 398585f..046e72e 100644 --- a/libbe/git.py +++ b/libbe/git.py @@ -14,133 +14,86 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os -import tempfile - -from rcs import invoke - -def strip_git(filename): - # Find the base path of the GIT tree, in order to strip that leading - # path from arguments to git -- it doesn't like absolute paths. - if os.path.isabs(filename): - filename = filename[len(git_repo_for_path('.'))+1:] - return filename - -def invoke_client(*args, **kwargs): - directory = kwargs['directory'] - expect = kwargs.get('expect', (0, 1)) - cl_args = ["git"] - cl_args.extend(args) - status,output,error = invoke(cl_args, expect, cwd=directory) - return status, output - -def add_id(filename, paranoid=False): - filename = strip_git(filename) - invoke_client("add", filename, directory=git_repo_for_path('.')) - -def delete_id(filename): - filename = strip_git(filename) - invoke_client("rm", filename, directory=git_repo_for_path('.')) - -def mkdir(path, paranoid=False): - os.mkdir(path) - -def set_file_contents(path, contents): - add = not os.path.exists(path) - file(path, "wb").write(contents) - if add: - add_id(path) - -def detect(path): - """Detect whether a directory is revision-controlled using GIT""" - path = os.path.realpath(path) - old_path = None - while True: - if os.path.exists(os.path.join(path, ".git")): +import re +import unittest +import doctest + +from rcs import RCS, RCStestCase, CommandError + +def new(): + return Git() + +class Git(RCS): + name="git" + client="git" + versioned=True + def _rcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _rcs_detect(self, path): + if self._u_search_parent_directories(path, ".git") != None : return True - if path == old_path: - return False - old_path = path - path = os.path.dirname(path) - -def precommit(directory): - pass - -def commit(directory, summary, body=None): - if body is not None: - summary += '\n' + body - descriptor, filename = tempfile.mkstemp() - try: - temp_file = os.fdopen(descriptor, 'wb') - temp_file.write(summary) - temp_file.close() - invoke_client('commit', '-a', '-F', filename, directory=directory) - finally: - os.unlink(filename) - -def postcommit(directory): - pass - - -# In order to diff the bug database, you need a way to check out arbitrary -# previous revisions and a mechanism for locating the bug_dir in the revision -# you've checked out. -# -# Copying the Mercurial implementation, this feature is implemented by four -# functions: -# -# git_dir_for_path : find '.git' for a git tree. -# -# export : check out a commit 'spec' from git-repo 'bug_dir' into a dir -# 'revision_dir' -# -# find_or_make_export : check out a commit 'spec' from git repo 'directory' to -# any location you please and return the path to the checkout -# -# path_in_reference : return a path to the bug_dir of the commit 'spec' - -def git_repo_for_path(path): - """Find the root of the deepest repository containing path.""" - # Assume that nothing funny is going on; in particular, that we aren't - # dealing with a bare repo. - return os.path.dirname(git_dir_for_path(path)) - -def git_dir_for_path(path): - """Find the git-dir of the deepest repo containing path.""" - return invoke_client("rev-parse", "--git-dir", directory=path)[1].rstrip() - -def export(spec, bug_dir, revision_dir): - """Check out commit 'spec' from the git repo containing bug_dir into - 'revision_dir'.""" - if not os.path.exists(revision_dir): - os.makedirs(revision_dir) - invoke_client("init", directory=revision_dir) - invoke_client("pull", git_dir_for_path(bug_dir), directory=revision_dir) - invoke_client("checkout", '-f', spec, directory=revision_dir) - -def find_or_make_export(spec, directory): - """Checkout 'spec' from the repo at 'directory' by hook or by crook and - return the path to the working copy.""" - home = os.path.expanduser("~") - revision_root = os.path.join(home, ".be_revs") - if not os.path.exists(revision_root): - os.mkdir(revision_root) - revision_dir = os.path.join(revision_root, spec) - if not os.path.exists(revision_dir): - export(spec, directory, revision_dir) - return revision_dir - -def path_in_reference(bug_dir, spec): - """Check out 'spec' and return the path to its bug_dir.""" - spec = spec or 'HEAD' - spec = invoke_client('rev-parse', spec, directory=bug_dir)[1].rstrip() - # This is a really hairy computation. - # The theory is that we can't possibly be working out of a bare repo; - # hence, we get the rel_bug_dir by chopping off dirname(git_dir_for_path(bug_dir)) - # + '/'. - rel_bug_dir = strip_git(bug_dir) - export_root = find_or_make_export(spec, directory=bug_dir) - return os.path.join(export_root, rel_bug_dir) - - -name = "git" - + return False + def _rcs_root(self, path): + """Find the root of the deepest repository containing path.""" + # Assume that nothing funny is going on; in particular, that we aren't + # dealing with a bare repo. + if os.path.isdir(path) != True: + path = os.path.dirname(path) + status,output,error = self._u_invoke_client("rev-parse", "--git-dir", + directory=path) + gitdir = os.path.join(path, output.rstrip('\n')) + dirname = os.path.abspath(os.path.dirname(gitdir)) + return dirname + def _rcs_init(self, path): + self._u_invoke_client("init", directory=path) + def _rcs_get_user_id(self): + status,output,error = self._u_invoke_client("config", "user.name") + name = output.rstrip('\n') + status,output,error = self._u_invoke_client("config", "user.email") + email = output.rstrip('\n') + return self._u_create_id(name, email) + def _rcs_set_user_id(self, value): + name,email = self._u_parse_id(value) + if email != None: + self._u_invoke_client("config", "user.email", email) + self._u_invoke_client("config", "user.name", name) + def _rcs_add(self, path): + if os.path.isdir(path): + return + self._u_invoke_client("add", path) + def _rcs_remove(self, path): + if not os.path.isdir(self._u_abspath(path)): + self._u_invoke_client("rm", "-f", path) + def _rcs_update(self, path): + self._rcs_add(path) + def _rcs_get_file_contents(self, path, revision=None): + if revision == None: + return file(self._u_abspath(path), "rb").read() + else: + arg = "%s:%s" % (revision,path) + status,output,error = self._u_invoke_client("show", arg) + return output + def _rcs_duplicate_repo(self, directory, revision=None): + if revision==None: + RCS._rcs_duplicate_repo(self, directory, revision) + else: + #self._u_invoke_client("archive", revision, directory) # makes tarball + self._u_invoke_client("clone", "--no-checkout",".",directory) + self._u_invoke_client("checkout", revision, directory=directory) + def _rcs_commit(self, commitfile): + status,output,error = self._u_invoke_client('commit', '-a', + '-F', commitfile) + revision = None + revline = re.compile("Created (.*)commit (.*):(.*)") + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 3 + revision = match.groups()[1] + return revision + +class GitTestCase(RCStestCase): + Class = Git + +unitsuite = unittest.TestLoader().loadTestsFromTestCase(GitTestCase) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/hg.py b/libbe/hg.py index 35de8e0..27cbb79 100644 --- a/libbe/hg.py +++ b/libbe/hg.py @@ -14,102 +14,73 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os -import tempfile - -import config -from rcs import invoke, CommandError - -def invoke_client(*args, **kwargs): - directory = kwargs['directory'] - expect = kwargs.get('expect', (0, 1)) - cl_args = ["hg"] - cl_args.extend(args) - status,output,error = invoke(cl_args, expect, cwd=directory) - return status, output - -def add_id(filename, paranoid=False): - invoke_client("add", filename, directory='.') - -def delete_id(filename): - invoke_client("rm", filename, directory='.') - -def mkdir(path, paranoid=False): - os.mkdir(path) - -def set_file_contents(path, contents): - add = not os.path.exists(path) - file(path, "wb").write(contents) - if add: - add_id(path) - -def lookup_revision(revno, directory): - return invoke_client('log', '--rev', str(revno), '--template={node}', - directory=directory)[1].rstrip('\n') - -def export(revno, directory, revision_dir): - invoke_client("archive", "--rev", str(revno), revision_dir, - directory=directory) - -def find_or_make_export(revno, directory): - revision_id = lookup_revision(revno, directory) - home = os.path.expanduser("~") - revision_root = os.path.join(home, ".be_revs") - if not os.path.exists(revision_root): - os.mkdir(revision_root) - revision_dir = os.path.join(revision_root, revision_id) - if not os.path.exists(revision_dir): - export(revno, directory, revision_dir) - return revision_dir - -def hg_root(path): - return invoke_client("root", "-R", path, directory=None)[1].rstrip('\r') - -def path_in_reference(bug_dir, spec): - if spec is None: - spec = int(invoke_client('tip', '--template="{rev}"', - directory=bug_dir)[1]) - rel_bug_dir = bug_dir[len(hg_root(bug_dir)):] - export_root = find_or_make_export(spec, directory=bug_dir) - return os.path.join(export_root, rel_bug_dir) - - -def unlink(path): - try: - os.unlink(path) - delete_id(path) - except OSError, e: - if e.errno != 2: - raise - - -def detect(path): - """Detect whether a directory is revision-controlled using Mercurial""" - path = os.path.realpath(path) - old_path = None - while True: - if os.path.exists(os.path.join(path, ".hg")): +import re +import unittest +import doctest + +from rcs import RCS, RCStestCase, CommandError, SettingIDnotSupported + +def new(): + return Hg() + +class Hg(RCS): + name="hg" + client="hg" + versioned=True + def _rcs_help(self): + status,output,error = self._u_invoke_client("--help") + return output + def _rcs_detect(self, path): + """Detect whether a directory is revision-controlled using Mercurial""" + if self._u_search_parent_directories(path, ".hg") != None: return True - if path == old_path: - return False - old_path = path - path = os.path.dirname(path) - -def precommit(directory): - pass - -def commit(directory, summary, body=None): - if body is not None: - summary += '\n' + body - descriptor, filename = tempfile.mkstemp() - try: - temp_file = os.fdopen(descriptor, 'wb') - temp_file.write(summary) - temp_file.close() - invoke_client('commit', '--logfile', filename, directory=directory) - finally: - os.unlink(filename) - -def postcommit(directory): - pass - -name = "hg" + return False + def _rcs_root(self, path): + status,output,error = self._u_invoke_client("root", directory=path) + return output.rstrip('\n') + def _rcs_init(self, path): + self._u_invoke_client("init", directory=path) + def _rcs_get_user_id(self): + status,output,error = self._u_invoke_client("showconfig","ui.username") + return output.rstrip('\n') + def _rcs_set_user_id(self, value): + """ + Supported by the Config Extension, but that is not part of + standard Mercurial. + http://www.selenic.com/mercurial/wiki/index.cgi/ConfigExtension + """ + raise SettingIDnotSupported + def _rcs_add(self, path): + self._u_invoke_client("add", path) + def _rcs_remove(self, path): + self._u_invoke_client("rm", path) + def _rcs_update(self, path): + pass + def _rcs_get_file_contents(self, path, revision=None): + if revision == None: + return file(os.path.join(self.rootdir, path), "rb").read() + else: + status,output,error = \ + self._u_invoke_client("cat","-r",revision,path) + return output + def _rcs_duplicate_repo(self, directory, revision=None): + if revision == None: + RCS._rcs_duplicate_repo(self, directory, revision) + else: + self._u_invoke_client("archive", "--rev", revision, directory) + def _rcs_commit(self, commitfile): + self._u_invoke_client('commit', '--logfile', commitfile) + status,output,error = self._u_invoke_client('identify') + revision = None + revline = re.compile("(.*) tip") + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 1 + revision = match.groups()[0] + return revision + +class HgTestCase(RCStestCase): + Class = Hg + +unitsuite = unittest.TestLoader().loadTestsFromTestCase(HgTestCase) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/mapfile.py b/libbe/mapfile.py index 6a304fd..559d713 100644 --- a/libbe/mapfile.py +++ b/libbe/mapfile.py @@ -14,7 +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 os.path +import errno import utility +import doctest + class IllegalKey(Exception): def __init__(self, key): Exception.__init__(self, 'Illegal key "%s"' % key) @@ -25,28 +29,27 @@ class IllegalValue(Exception): Exception.__init__(self, 'Illegal value "%s"' % value) self.value = value -def generate(f, map, context=3): - """Generate a format-2 mapfile. 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, 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. - >>> f = utility.FileString() - >>> generate(f, {"q":"p"}) - >>> f.str + >>> generate({"q":"p"}) '\\n\\n\\nq=p\\n\\n\\n\\n' - >>> generate(f, {"q=":"p"}) + >>> generate({"q=":"p"}) Traceback (most recent call last): IllegalKey: Illegal key "q=" - >>> generate(f, {"q\\n":"p"}) + >>> generate({"q\\n":"p"}) Traceback (most recent call last): IllegalKey: Illegal key "q\\n" - >>> generate(f, {"":"p"}) + >>> generate({"":"p"}) Traceback (most recent call last): IllegalKey: Illegal key "" - >>> generate(f, {">q":"p"}) + >>> generate({">q":"p"}) Traceback (most recent call last): IllegalKey: Illegal key ">q" - >>> generate(f, {"q":"p\\n"}) + >>> generate({"q":"p\\n"}) Traceback (most recent call last): IllegalValue: Illegal value "p\\n" """ @@ -64,107 +67,48 @@ def generate(f, map, context=3): if "\n" in map[key]: raise IllegalValue(map[key].encode('string_escape')) + lines = [] for key in keys: for i in range(context): - f.write("\n") - f.write("%s=%s\n" % (key.encode("utf-8"), map[key].encode("utf-8"))) + lines.append("") + lines.append("%s=%s" % (key, map[key])) for i in range(context): - f.write("\n") + lines.append("") + return '\n'.join(lines) + '\n' -def parse(f): +def parse(contents): """ - Parse a format-2 mapfile. + Parse a format-2 mapfile string. >>> parse('\\n\\n\\nq=p\\n\\n\\n\\n')['q'] - u'p' + 'p' >>> parse('\\n\\nq=\\'p\\'\\n\\n\\n\\n')['q'] - u"\'p\'" - >>> f = utility.FileString() - >>> generate(f, {"a":"b", "c":"d", "e":"f"}) - >>> dict = parse(f) + "\'p\'" + >>> contents = generate({"a":"b", "c":"d", "e":"f"}) + >>> dict = parse(contents) >>> dict["a"] - u'b' + 'b' >>> dict["c"] - u'd' + 'd' >>> dict["e"] - u'f' + 'f' """ - f = utility.get_file(f) result = {} - for line in f: + for line in contents.splitlines(): line = line.rstrip('\n') if len(line) == 0: continue - name,value = [f.decode('utf-8') for f in line.split('=', 1)] - assert not result.has_key('name') + name,value = [field for field in line.split('=', 1)] + assert not result.has_key(name) result[name] = value return result +def map_save(rcs, path, map, allow_no_rcs=False): + """Save the map as a mapfile to the specified path""" + contents = generate(map) + rcs.set_file_contents(path, contents, allow_no_rcs) -def split_diff3(this, other, f): - """Split a file or string with diff3 conflicts into two files. - - :param this: The THIS file to write. May be a utility.FileString - :param other: The OTHER file to write. May be a utility.FileString - :param f: The file or string to split. - :return: True if there were conflicts - - >>> split_diff3(utility.FileString(), utility.FileString(), - ... "a\\nb\\nc\\nd\\n") - False - >>> this = utility.FileString() - >>> other = utility.FileString() - >>> split_diff3(this, other, "<<<<<<< values1\\nstatus=closed\\n=======\\nstatus=closedd\\n>>>>>>> values2\\n") - True - >>> this.str - 'status=closed\\n' - >>> other.str - 'status=closedd\\n' - """ - f = utility.get_file(f) - this_active = True - other_active = True - conflicts = False - for line in f: - if line.startswith("<<<<<<<"): - conflicts = True - this_active = True - other_active = False - elif line.startswith("======="): - this_active = False - other_active = True - elif line.startswith(">>>>>>>"): - this_active = True - other_active = True - else: - if this_active: - this.write(line) - if other_active: - other.write(line) - return conflicts - -def split_diff3_str(f): - """Split a file/string with diff3 conflicts into two strings. If there - were no conflicts, one string is returned. +def map_load(rcs, path, allow_no_rcs=False): + contents = rcs.get_file_contents(path, allow_no_rcs=allow_no_rcs) + return parse(contents) - >>> result = split_diff3_str("<<<<<<< values1\\nstatus=closed\\n=======\\nstatus=closedd\\n>>>>>>> values2\\n") - >>> len(result) - 2 - >>> result[0] != result[1] - True - >>> result = split_diff3_str("<<<<<<< values1\\nstatus=closed\\n=======\\nstatus=closed\\n>>>>>>> values2\\n") - >>> len(result) - 2 - >>> result[0] == result[1] - True - >>> result = split_diff3_str("a\\nb\\nc\\nd\\n") - >>> len(result) - 1 - >>> result[0] - 'a\\nb\\nc\\nd\\n' - """ - this = utility.FileString() - other = utility.FileString() - if split_diff3(this, other, f): - return (this.str, other.str) - else: - return (this.str,) +suite = doctest.DocTestSuite() diff --git a/libbe/names.py b/libbe/names.py deleted file mode 100644 index d2e077a..0000000 --- a/libbe/names.py +++ /dev/null @@ -1,37 +0,0 @@ -# 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 - -import os -import sys - - -def uuid(): - # this code borrowed from standard commands module - # but adapted to win32 - pipe = os.popen('uuidgen', 'r') - text = pipe.read() - sts = pipe.close() - if sts not in (0, None): - raise "Failed to run uuidgen" - if text[-1:] == '\n': text = text[:-1] - return text - -def creator(): - if sys.platform != "win32": - return os.environ["LOGNAME"] - else: - return os.environ["USERNAME"] diff --git a/libbe/no_rcs.py b/libbe/no_rcs.py deleted file mode 100644 index 1b3b005..0000000 --- a/libbe/no_rcs.py +++ /dev/null @@ -1,51 +0,0 @@ -# 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 -import os -import config -from os import unlink - -def add_id(filename, paranoid=False): - """Compatibility function""" - pass - -def delete_id(filename): - """Compatibility function""" - pass - -def mkdir(path, paranoid=False): - os.mkdir(path) - -def set_file_contents(path, contents): - add = not os.path.exists(path) - file(path, "wb").write(contents) - if add: - add_id(path) - -def detect(path): - """Compatibility function""" - return True - -def precommit(directory): - pass - -def commit(directory, summary, body=None): - pass - -def postcommit(directory): - pass - -name = "None" diff --git a/libbe/plugin.py b/libbe/plugin.py index 4016ca1..0964fba 100644 --- a/libbe/plugin.py +++ b/libbe/plugin.py @@ -17,6 +17,8 @@ import os import os.path import sys +import doctest + def my_import(mod_name): module = __import__(mod_name) components = mod_name.split('.') @@ -34,6 +36,8 @@ def iter_plugins(prefix): modfiles = os.listdir(os.path.join(plugin_path, prefix)) modfiles.sort() for modfile in modfiles: + if modfile.startswith('.'): + continue # the occasional emacs temporary file if modfile.endswith(".py") and modfile != "__init__.py": yield modfile[:-3], my_import(prefix+"."+modfile[:-3]) @@ -55,6 +59,9 @@ def get_plugin(prefix, name): plugin_path = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) if plugin_path not in sys.path: sys.path.append(plugin_path) + +suite = doctest.DocTestSuite() + def _test(): import doctest doctest.testmod() diff --git a/libbe/rcs.py b/libbe/rcs.py index 4487fba..3519c3d 100644 --- a/libbe/rcs.py +++ b/libbe/rcs.py @@ -15,42 +15,45 @@ # 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 os +import os.path +from socket import gethostname +import re import sys +import tempfile +import shutil +import unittest +import doctest -def rcs_by_name(rcs_name): - """Return the module for the RCS with the given name""" - if rcs_name == "Arch": - import arch - return arch - elif rcs_name == "bzr": - import bzr - return bzr - elif rcs_name == "hg": - import hg - return hg - elif rcs_name == "git": - import git - return git - elif rcs_name == "None": - import no_rcs - return no_rcs - -def detect(dir): - """Return the module for the rcs being used in this directory""" +from utility import Dir, search_parent_directories + + +def _get_matching_rcs(matchfn): + """Return the first module for which matchfn(RCS_instance) is true""" import arch import bzr import hg import git - if arch.detect(dir): - return arch - elif bzr.detect(dir): - return bzr - elif hg.detect(dir): - return hg - elif git.detect(dir): - return git - import no_rcs - return no_rcs + for module in [arch, bzr, hg, git]: + rcs = module.new() + if matchfn(rcs) == True: + return rcs + else: + del(rcs) + return RCS() + +def rcs_by_name(rcs_name): + """Return the module for the RCS with the given name""" + return _get_matching_rcs(lambda rcs: rcs.name == rcs_name) + +def detect_rcs(dir): + """Return an RCS instance for the rcs being used in this directory""" + return _get_matching_rcs(lambda rcs: rcs.detect(dir)) + +def installed_rcs(): + """Return an instance of an installed RCS""" + return _get_matching_rcs(lambda rcs: rcs.installed()) + class CommandError(Exception): def __init__(self, err_str, status): @@ -58,19 +61,579 @@ class CommandError(Exception): self.err_str = err_str self.status = status -def invoke(args, expect=(0,), cwd=None): - try : - if sys.platform != "win32": - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd) +class SettingIDnotSupported(NotImplementedError): + pass + +class RCSnotRooted(Exception): + def __init__(self): + msg = "RCS not rooted" + Exception.__init__(self, msg) + +class PathNotInRoot(Exception): + def __init__(self, path, root): + msg = "Path '%s' not in root '%s'" % (path, root) + Exception.__init__(self, msg) + self.path = path + self.root = root + +class NoSuchFile(Exception): + def __init__(self, pathname): + Exception.__init__(self, "No such file: %s" % pathname) + + +def new(): + return RCS() + +class RCS(object): + """ + This class implements a 'no-rcs' interface. + + Support for other RCSs can be added by subclassing this class, and + overriding methods _rcs_*() with code appropriate for your RCS. + + The methods _u_*() are utility methods available to the _rcs_*() + methods. + """ + name = "None" + client = "" # command-line tool for _u_invoke_client + versioned = False + def __init__(self, paranoid=False): + self.paranoid = paranoid + self.verboseInvoke = False + self.rootdir = None + self._duplicateBasedir = None + self._duplicateDirname = None + def __del__(self): + self.cleanup() + + def _rcs_help(self): + """ + Return the command help string. + (Allows a simple test to see if the client is installed.) + """ + pass + def _rcs_detect(self, path=None): + """ + Detect whether a directory is revision controlled with this RCS. + """ + return True + def _rcs_root(self, path): + """ + Get the RCS root. This is the default working directory for + future invocations. You would normally set this to the root + directory for your RCS. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + if path == "": + path = os.path.abspath(".") + return path + def _rcs_init(self, path): + """ + Begin versioning the tree based at path. + """ + pass + def _rcs_cleanup(self): + """ + Remove any cruft that _rcs_init() created outside of the + versioned tree. + """ + pass + def _rcs_get_user_id(self): + """ + Get the RCS's suggested user id (e.g. "John Doe <jdoe@example.com>"). + If the RCS has not been configured with a username, return None. + """ + return None + def _rcs_set_user_id(self, value): + """ + Set the RCS's suggested user id (e.g "John Doe <jdoe@example.com>"). + This is run if the RCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + raise SettingIDnotSupported + def _rcs_add(self, path): + """ + Add the already created file at path to version control. + """ + pass + def _rcs_remove(self, path): + """ + Remove the file at path from version control. Optionally + remove the file from the filesystem as well. + """ + pass + def _rcs_update(self, path): + """ + Notify the versioning system of changes to the versioned file + at path. + """ + pass + def _rcs_get_file_contents(self, path, revision=None): + """ + 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. + + 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() + def _rcs_duplicate_repo(self, directory, revision=None): + """ + Get the repository as it was in a given revision. + revision==None specifies the current revision. + dir specifies a directory to create the duplicate in. + """ + shutil.copytree(self.rootdir, directory, True) + def _rcs_commit(self, commitfile): + """ + Commit the current working directory, using the contents of + commitfile as the comment. Return the name of the old + revision. + """ + return None + def installed(self): + try: + self._rcs_help() + return True + except OSError, e: + if e.errno == errno.ENOENT: + return False + raise e + def detect(self, path="."): + """ + Detect whether a directory is revision controlled with this RCS. + """ + return self._rcs_detect(path) + def root(self, path): + """ + Set the root directory to the path's RCS root. This is the + default working directory for future invocations. + """ + self.rootdir = self._rcs_root(path) + def init(self, path): + """ + Begin versioning the tree based at path. + Also roots the rcs at path. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + self._rcs_init(path) + self.root(path) + def cleanup(self): + self._rcs_cleanup() + def get_user_id(self): + """ + Get the RCS's suggested user id (e.g. "John Doe <jdoe@example.com>"). + If the RCS has not been configured with a username, return the user's + id. You can override the automatic lookup procedure by setting the + RCS.user_id attribute to a string of your choice. + """ + if hasattr(self, "user_id"): + if self.user_id != None: + return self.user_id + id = self._rcs_get_user_id() + if id == None: + name = self._u_get_fallback_username() + email = self._u_get_fallback_email() + id = self._u_create_id(name, email) + print >> sys.stderr, "Guessing id '%s'" % id + try: + self.set_user_id(id) + except SettingIDnotSupported: + pass + return id + def set_user_id(self, value): + """ + Set the RCS's suggested user id (e.g "John Doe <jdoe@example.com>"). + This is run if the RCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + self._rcs_set_user_id(value) + def add(self, path): + """ + Add the already created file at path to version control. + """ + self._rcs_add(self._u_rel_path(path)) + def remove(self, path): + """ + Remove a file from both version control and the filesystem. + """ + self._rcs_remove(self._u_rel_path(path)) + if os.path.exists(path): + os.remove(path) + def recursive_remove(self, dirname): + """ + Remove a file/directory and all its decendents from both + version control and the filesystem. + """ + if not os.path.exists(dirname): + raise NoSuchFile(dirname) + for dirpath,dirnames,filenames in os.walk(dirname, topdown=False): + filenames.extend(dirnames) + for path in filenames: + fullpath = os.path.join(dirpath, path) + if os.path.exists(fullpath) == False: + continue + self._rcs_remove(self._u_rel_path(fullpath)) + if os.path.exists(dirname): + shutil.rmtree(dirname) + def update(self, path): + """ + Notify the versioning system of changes to the versioned file + at path. + """ + self._rcs_update(self._u_rel_path(path)) + def get_file_contents(self, path, revision=None, allow_no_rcs=False): + """ + Get the file as it was in a given revision. + Revision==None specifies the current revision. + """ + if not os.path.exists(path): + raise NoSuchFile(path) + if self._use_rcs(path, allow_no_rcs): + relpath = self._u_rel_path(path) + contents = self._rcs_get_file_contents(relpath,revision) + else: + contents = file(path, "rb").read() + return contents.decode("utf-8") + 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")) + + if self._use_rcs(path, allow_no_rcs): + if add: + self.add(path) + else: + self.update(path) + def mkdir(self, path, allow_no_rcs=False): + """ + Create (if neccessary) a directory at path under version + control. + """ + if not os.path.exists(path): + os.mkdir(path) + if self._use_rcs(path, allow_no_rcs): + self.add(path) else: - # win32 don't have os.execvp() so have to run command in a shell - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True, - cwd=cwd) - except OSError, e : - strerror = "%s\nwhile executing %s" % (e.args[1], args) - raise CommandError(strerror, e.args[0]) - output, error = q.communicate() - status = q.wait() - if status not in expect: - raise CommandError(error, status) - return status, output, error + assert os.path.isdir(path) + if self._use_rcs(path, allow_no_rcs): + self.update(path) + def duplicate_repo(self, revision=None): + """ + Get the repository as it was in a given revision. + revision==None specifies the current revision. + Return the path to the arbitrary directory at the base of the new repo. + """ + # Dirname in Baseir to protect against simlink attacks. + if self._duplicateBasedir == None: + self._duplicateBasedir = tempfile.mkdtemp(prefix='BErcs') + self._duplicateDirname = \ + os.path.join(self._duplicateBasedir, "duplicate") + self._rcs_duplicate_repo(directory=self._duplicateDirname, + revision=revision) + return self._duplicateDirname + def remove_duplicate_repo(self): + """ + Clean up a duplicate repo created with duplicate_repo(). + """ + if self._duplicateBasedir != None: + shutil.rmtree(self._duplicateBasedir) + self._duplicateBasedir = None + self._duplicateDirname = None + def commit(self, summary, body=None): + """ + Commit the current working directory, with a commit message + string summary and body. Return the name of the old revision + (or None if versioning is not supported). + """ + if body is not None: + summary += '\n' + body + descriptor, filename = tempfile.mkstemp() + revision = None + try: + temp_file = os.fdopen(descriptor, 'wb') + temp_file.write(summary) + temp_file.flush() + revision = self._rcs_commit(filename) + temp_file.close() + finally: + os.remove(filename) + return revision + def precommit(self, directory): + pass + def postcommit(self, directory): + pass + def _u_invoke(self, args, expect=(0,), cwd=None): + if cwd == None: + cwd = self.rootdir + if self.verboseInvoke == True: + print >> sys.stderr, "%s$ %s" % (cwd, " ".join(args)) + try : + if sys.platform != "win32": + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd) + else: + # win32 don't have os.execvp() so have to run command in a shell + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, + shell=True, cwd=cwd) + except OSError, e : + strerror = "%s\nwhile executing %s" % (e.args[1], args) + raise CommandError(strerror, e.args[0]) + output, error = q.communicate() + status = q.wait() + if self.verboseInvoke == True: + print >> sys.stderr, "%d\n%s%s" % (status, output, error) + if status not in expect: + strerror = "%s\nwhile executing %s\n%s" % (args[1], args, error) + raise CommandError(strerror, status) + return status, output, error + def _u_invoke_client(self, *args, **kwargs): + directory = kwargs.get('directory',None) + expect = kwargs.get('expect', (0,)) + cl_args = [self.client] + cl_args.extend(args) + return self._u_invoke(cl_args, expect, cwd=directory) + def _u_search_parent_directories(self, path, filename): + """ + Find the file (or directory) named filename in path or in any + of path's parents. + + e.g. + search_parent_directories("/a/b/c", ".be") + will return the path to the first existing file from + /a/b/c/.be + /a/b/.be + /a/.be + /.be + or None if none of those files exist. + """ + return search_parent_directories(path, filename) + def _use_rcs(self, path, allow_no_rcs): + """ + Try and decide if _rcs_add/update/mkdir/etc calls will + succeed. Returns True is we think the rcs_call would + succeeed, and False otherwise. + """ + use_rcs = True + exception = None + if self.rootdir != None: + if self.path_in_root(path) == False: + use_rcs = False + exception = PathNotInRoot(path, self.rootdir) + else: + use_rcs = False + exception = RCSnotRooted + if use_rcs == False and allow_no_rcs==False: + raise exception + return use_rcs + def path_in_root(self, path, root=None): + """ + Return the relative path to path from root. + >>> rcs = new() + >>> rcs.path_in_root("/a.b/c/.be", "/a.b/c") + True + >>> rcs.path_in_root("/a.b/.be", "/a.b/c") + False + """ + if root == None: + if self.rootdir == None: + raise RCSnotRooted + root = self.rootdir + path = os.path.abspath(path) + absRoot = os.path.abspath(root) + absRootSlashedDir = os.path.join(absRoot,"") + if not path.startswith(absRootSlashedDir): + return False + return True + def _u_rel_path(self, path, root=None): + """ + Return the relative path to path from root. + >>> rcs = new() + >>> rcs._u_rel_path("/a.b/c/.be", "/a.b/c") + '.be' + """ + if root == None: + if self.rootdir == None: + raise RCSnotRooted + root = self.rootdir + path = os.path.abspath(path) + absRoot = os.path.abspath(root) + absRootSlashedDir = os.path.join(absRoot,"") + if not path.startswith(absRootSlashedDir): + raise PathNotInRoot(path, absRootSlashedDir) + assert path != absRootSlashedDir, \ + "file %s == root directory %s" % (path, absRootSlashedDir) + relpath = path[len(absRootSlashedDir):] + return relpath + def _u_abspath(self, path, root=None): + """ + Return the absolute path from a path realtive to root. + >>> rcs = new() + >>> rcs._u_abspath(".be", "/a.b/c") + '/a.b/c/.be' + """ + if root == None: + assert self.rootdir != None, "RCS not rooted" + root = self.rootdir + return os.path.abspath(os.path.join(root, path)) + def _u_create_id(self, name, email=None): + """ + >>> rcs = new() + >>> rcs._u_create_id("John Doe", "jdoe@example.com") + 'John Doe <jdoe@example.com>' + >>> rcs._u_create_id("John Doe") + 'John Doe' + """ + assert len(name) > 0 + if email == None or len(email) == 0: + return name + else: + return "%s <%s>" % (name, email) + def _u_parse_id(self, value): + """ + >>> rcs = new() + >>> rcs._u_parse_id("John Doe <jdoe@example.com>") + ('John Doe', 'jdoe@example.com') + >>> rcs._u_parse_id("John Doe") + ('John Doe', None) + >>> try: + ... rcs._u_parse_id("John Doe <jdoe@example.com><what?>") + ... except AssertionError: + ... print "Invalid match" + Invalid match + """ + emailexp = re.compile("(.*) <([^>]*)>(.*)") + match = emailexp.search(value) + if match == None: + email = None + name = value + else: + assert len(match.groups()) == 3 + assert match.groups()[2] == "", match.groups() + email = match.groups()[1] + name = match.groups()[0] + assert name != None + assert len(name) > 0 + return (name, email) + def _u_get_fallback_username(self): + name = None + for envariable in ["LOGNAME", "USERNAME"]: + if os.environ.has_key(envariable): + name = os.environ[envariable] + break + assert name != None + return name + def _u_get_fallback_email(self): + hostname = gethostname() + name = self._u_get_fallback_username() + return "%s@%s" % (name, hostname) + def _u_parse_commitfile(self, commitfile): + """ + Split the commitfile created in self.commit() back into + summary and header lines. + """ + f = file(commitfile, "rb") + summary = f.readline() + body = f.read() + body.lstrip('\n') + if len(body) == 0: + body = None + f.close + return (summary, body) + + +class RCStestCase(unittest.TestCase): + Class = RCS + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + self.dirname = None + def instantiateRCS(self): + return self.Class() + def setUp(self): + self.dir = Dir() + self.dirname = self.dir.path + self.rcs = self.instantiateRCS() + def tearDown(self): + del(self.rcs) + del(self.dirname) + def fullPath(self, path): + return os.path.join(self.dirname, path) + def assertPathExists(self, path): + fullpath = self.fullPath(path) + self.failUnless(os.path.exists(fullpath)==True, + "path %s does not exist" % fullpath) + def uidTest(self): + user_id = self.rcs.get_user_id() + self.failUnless(user_id != None, + "unable to get a user id") + user_idB = "John Doe <jdoe@example.com>" + if self.rcs.name in ["None", "hg"]: + self.assertRaises(SettingIDnotSupported, self.rcs.set_user_id, + user_idB) + else: + self.rcs.set_user_id(user_idB) + self.failUnless(self.rcs.get_user_id() == user_idB, + "user id not set correctly (was %s, is %s)" \ + % (user_id, self.rcs.get_user_id())) + self.failUnless(self.rcs.set_user_id(user_id) == None, + "unable to restore user id %s" % user_id) + self.failUnless(self.rcs.get_user_id() == user_id, + "unable to restore user id %s" % user_id) + def versionTest(self, path): + origpath = path + path = self.fullPath(path) + contentsA = "Lorem ipsum" + contentsB = "dolor sit amet" + self.rcs.set_file_contents(path,contentsA) + self.failUnless(self.rcs.get_file_contents(path)==contentsA, + "File contents not set or read correctly") + revision = self.rcs.commit("Commit current status") + self.failUnless(self.rcs.get_file_contents(path)==contentsA, + "Committing File contents not set or read correctly") + if self.rcs.versioned == True: + self.rcs.set_file_contents(path,contentsB) + self.failUnless(self.rcs.get_file_contents(path)==contentsB, + "File contents not set correctly after commit") + contentsArev = self.rcs.get_file_contents(path, revision) + self.failUnless(contentsArev==contentsA, \ + "Original file contents not saved in revision %s\n%s\n%s\n" \ + % (revision, contentsA, contentsArev)) + dup = self.rcs.duplicate_repo(revision) + duppath = os.path.join(dup, origpath) + dupcont = file(duppath, "rb").read() + self.failUnless(dupcont == contentsA) + self.rcs.remove_duplicate_repo() + def testRun(self): + self.failUnless(self.rcs.installed() == True, + "%s RCS not found" % self.Class.name) + if self.Class.name != "None": + self.failUnless(self.rcs.detect(self.dirname)==False, + "Detected %s RCS before initializing" \ + % self.Class.name) + self.rcs.init(self.dirname) + self.failUnless(self.rcs.detect(self.dirname)==True, + "Did not detect %s RCS after initializing" \ + % self.Class.name) + rp = os.path.realpath(self.rcs.rootdir) + dp = os.path.realpath(self.dirname) + self.failUnless(dp == rp or rp == None, + "%s RCS root in wrong dir (%s %s)" \ + % (self.Class.name, dp, rp)) + self.uidTest() + self.rcs.mkdir(self.fullPath('a')) + self.rcs.mkdir(self.fullPath('a/b')) + self.rcs.mkdir(self.fullPath('c')) + self.assertPathExists('a') + self.assertPathExists('a/b') + self.assertPathExists('c') + self.versionTest('a/text') + self.versionTest('a/b/text') + self.rcs.recursive_remove(self.fullPath('a')) + +unitsuite = unittest.TestLoader().loadTestsFromTestCase(RCStestCase) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/restconvert.py b/libbe/restconvert.py index cc7f866..57148e4 100644 --- a/libbe/restconvert.py +++ b/libbe/restconvert.py @@ -27,7 +27,7 @@ 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() @@ -126,3 +126,5 @@ def foldout(name, arguments, options, content, lineno, content_offset, foldout += foldout_body foldout.set_class('foldout') return [foldout] + +suite = doctest.DocTestSuite() diff --git a/libbe/template b/libbe/template deleted file mode 100644 index 467eee4..0000000 --- a/libbe/template +++ /dev/null @@ -1,48 +0,0 @@ -"""Compare two bug trees""" -from bugdir import cmdutil - -def diff(old_tree, new_tree): - old_bug_map = old_tree.bug_map() - new_bug_map = new_tree.bug_map() - added = [] - removed = [] - modified = [] - for old_bug in old_bug_map.itervalues(): - new_bug = new_bug_map.get(bug.uuid) - if new_bug is None : - removed.append(old_bug) - else: - if old_bug != new_bug: - modified.append((old_bug, new_bug)) - for new_bug in new_bug_map.itervalues(): - if not old_bug_map.haskey(new_bug.id): - added.append(new_bug) - return (removed, modified, added) - - -def reference_diff(bugdir, spec=None): - return diff(bugdir.reference_bugdir(), bugdir) - -def diff_report(diff_data, bugdir) - (removed, modified, added) = diff_data - def modified_cmp(left, right): - return cmp_severity(left[1], right[1]) - - added.sort(bugdir.cmp_severity) - removed.sort(bugdir.cmp_severity) - modified.sort(modified_cmp) - - print "New bug reports:" - for bug in added: - cmdutil.bug_summary(bug, bugdir, no_target=True) - - print "modified bug reports:" - for old_bug, new_bug in modified: - cmdutil.bug_summary(new_bug, bugdir, no_target=True) - - print "Removed bug reports:" - for bug in removed: - cmdutil.bug_summary(bug, bugdir, no_target=True) - - - diff --git a/libbe/tests.py b/libbe/tests.py deleted file mode 100644 index a7d925d..0000000 --- a/libbe/tests.py +++ /dev/null @@ -1,55 +0,0 @@ -# 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 -import tempfile -import shutil -import os -import os.path -from libbe import bugdir, arch -cleanable = [] -def clean_up(): - global cleanable - tmp = cleanable - tmp.reverse() - for obj in tmp: - obj.clean_up() - cleanable = [] - -class Dir: - def __init__(self): - self.name = tempfile.mkdtemp(prefix="testdir") - cleanable.append(self) - def clean_up(self): - shutil.rmtree(self.name) - -def arch_dir(): - arch.ensure_user_id() - dir = Dir() - arch.init_tree(dir.name) - return dir - -def bug_arch_dir(): - dir = arch_dir() - return bugdir.create_bug_dir(dir.name, arch) - -def simple_bug_dir(): - dir = bug_arch_dir() - bug_a = bugdir.new_bug(dir, "a") - bug_b = bugdir.new_bug(dir, "b") - bug_b.status = "closed" - bug_a.save() - bug_b.save() - return dir diff --git a/libbe/tree.py b/libbe/tree.py new file mode 100644 index 0000000..e6f144e --- /dev/null +++ b/libbe/tree.py @@ -0,0 +1,158 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2008 W. Trevor King +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA + +import doctest + +class Tree(list): + """ + Construct + +-b---d-g + a-+ +-e + +-c-+-f-h-i + with + >>> i = Tree(); i.n = "i" + >>> h = Tree([i]); h.n = "h" + >>> f = Tree([h]); f.n = "f" + >>> e = Tree(); e.n = "e" + >>> c = Tree([f,e]); c.n = "c" + >>> g = Tree(); g.n = "g" + >>> d = Tree([g]); d.n = "d" + >>> b = Tree([d]); b.n = "b" + >>> a = Tree(); a.n = "a" + >>> a.append(c) + >>> a.append(b) + + >>> a.branch_len() + 5 + >>> a.sort(key=lambda node : node.branch_len()) + >>> "".join([node.n for node in a.traverse()]) + 'abdgcefhi' + >>> "".join([node.n for node in a.traverse(depthFirst=False)]) + 'abcdefghi' + >>> for depth,node in a.thread(): + ... print "%*s" % (2*depth+1, node.n) + a + b + d + g + c + e + f + h + i + >>> for depth,node in a.thread(flatten=True): + ... print "%*s" % (2*depth+1, node.n) + a + b + d + g + c + e + f + h + i + """ + def branch_len(self): + """ + Exhaustive search every time == SLOW. + + Use only on small trees, or reimplement by overriding + child-addition methods to allow accurate caching. + + For the tree + +-b---d-g + a-+ +-e + +-c-+-f-h-i + this method returns 5. + """ + if len(self) == 0: + return 1 + else: + return 1 + max([child.branch_len() for child in self]) + + def sort(self, *args, **kwargs): + """ + This method can be slow, e.g. on a branch_len() sort, since a + node at depth N from the root has it's branch_len() method + called N times. + """ + list.sort(self, *args, **kwargs) + for child in self: + child.sort() + + def traverse(self, depthFirst=True): + """ + Note: you might want to sort() your tree first. + """ + if depthFirst == True: + yield self + for child in self: + for descendant in child.traverse(): + yield descendant + else: # breadth first, Wikipedia algorithm + # http://en.wikipedia.org/wiki/Breadth-first_search + queue = [self] + while len(queue) > 0: + node = queue.pop(0) + yield node + queue.extend(node) + + def thread(self, flatten=False): + """ + When flatten==False, the depth of any node is one greater than + the depth of its parent. That way the inheritance is + explicit, but you can end up with highly indented threads. + + When flatten==True, the depth of any node is only greater than + the depth of its parent when there is a branch, and the node + is not the last child. This can lead to ancestry ambiguity, + but keeps the total indentation down. E.g. + +-b +-b-c + a-+-c and a-+ + +-d-e-f +-d-e-f + would both produce (after sorting by branch_len()) + (0, a) + (1, b) + (1, c) + (0, d) + (0, e) + (0, f) + """ + stack = [] # ancestry of the current node + if flatten == True: + depthDict = {} + + for node in self.traverse(depthFirst=True): + while len(stack) > 0 \ + and id(node) not in [id(c) for c in stack[-1]]: + stack.pop(-1) + if flatten == False: + depth = len(stack) + else: + if len(stack) == 0: + depth = 0 + else: + parent = stack[-1] + depth = depthDict[id(parent)] + if len(parent) > 1 and node != parent[-1]: + depth += 1 + depthDict[id(node)] = depth + yield (depth,node) + stack.append(node) + +suite = doctest.DocTestSuite() diff --git a/libbe/utility.py b/libbe/utility.py index 1fd83da..2c77fcf 100644 --- a/libbe/utility.py +++ b/libbe/utility.py @@ -18,57 +18,50 @@ import calendar import time import os import tempfile +import shutil +import doctest -class FileString(object): - """Bare-bones pseudo-file class - - >>> f = FileString("me\\nyou") - >>> len(list(f)) - 2 - >>> len(list(f)) - 0 - >>> f = FileString() - >>> f.write("hello\\nthere") - >>> "".join(list(f)) - 'hello\\nthere' - """ - def __init__(self, str=""): - object.__init__(self) - self.str = str - self._iter = None - - def __iter__(self): - if self._iter is None: - self._iter = self._get_iter() - return self._iter - def _get_iter(self): - for line in self.str.splitlines(True): - yield line - - def write(self, line): - self.str += line - - -def get_file(f): +def search_parent_directories(path, filename): """ - Return a file-like object from input. This is a helper for functions that - can take either file or string parameters. - - :param f: file or string - :return: a FileString if input is a string, otherwise return the imput - object. - - >>> isinstance(get_file(file("/dev/null")), file) - True - >>> isinstance(get_file("f"), FileString) - True + Find the file (or directory) named filename in path or in any + of path's parents. + + e.g. + search_parent_directories("/a/b/c", ".be") + will return the path to the first existing file from + /a/b/c/.be + /a/b/.be + /a/.be + /.be + or None if none of those files exist. """ - if isinstance(f, basestring): - return FileString(f) - else: - return f - + path = os.path.realpath(path) + assert os.path.exists(path) + old_path = None + while True: + check_path = os.path.join(path, filename) + if os.path.exists(check_path): + return check_path + if path == old_path: + return None + old_path = path + path = os.path.dirname(path) + +class Dir (object): + "A temporary directory for testing use" + def __init__(self): + self.path = tempfile.mkdtemp(prefix="BEtest") + self.rmtree = shutil.rmtree # save local reference for __del__ + self.removed = False + def __del__(self): + self.cleanup() + def cleanup(self): + if self.removed == False: + self.rmtree(self.path) + self.removed = True + def __call__(self): + return self.path RFC_2822_TIME_FMT = "%a, %d %b %Y %H:%M:%S +0000" @@ -111,10 +104,10 @@ def editor_string(comment=None): CantFindEditor: Can't find editor to get string from >>> os.environ["EDITOR"] = "echo bar > " >>> editor_string() - 'bar\\n' + u'bar\\n' >>> os.environ["VISUAL"] = "echo baz > " >>> editor_string() - 'baz\\n' + u'baz\\n' >>> del os.environ["EDITOR"] >>> del os.environ["VISUAL"] """ @@ -133,7 +126,7 @@ def editor_string(comment=None): os.close(fhandle) oldmtime = os.path.getmtime(fname) os.system("%s %s" % (editor, fname)) - output = trimmed_string(file(fname, "rb").read()) + output = trimmed_string(file(fname, "rb").read().decode("utf-8")) if output.rstrip('\n') == "": output = None finally: @@ -162,3 +155,5 @@ def trimmed_string(instring): break out.append(line) return ''.join(out) + +suite = doctest.DocTestSuite() diff --git a/misc/gui/wxbe b/misc/gui/wxbe index 40c584d..e71ae0c 100755 --- a/misc/gui/wxbe +++ b/misc/gui/wxbe @@ -1,49 +1,87 @@ #!/usr/bin/env python -from wxPython.wx import * -from wxPython.lib.mixins.listctrl import wxListCtrlAutoWidthMixin +import wx +from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin import sys, os.path -sys.path.append(os.path.realpath(os.path.join"..")) -from libbe import bugdir +from libbe import bugdir, names +from libbe.bug import cmp_status, cmp_severity, cmp_time, cmp_full -class MyApp(wxApp): +class MyApp(wx.App): def OnInit(self): - frame = wxFrame(NULL, -1, "Bug display") - frame.Show(true) + frame = BugListFrame(None, title="Bug List") + frame.Show(True) self.SetTopWindow(frame) - panel = wxPanel(frame, -1, style=(wxVSCROLL | wxHSCROLL)) - panel.SetSize((500, 400)) - sizer = wxBoxSizer(wxVERTICAL) - sizer.Add(panel, wxGROW) - frame.SetSizer(sizer) - bugs = BugList(panel) - bugs.SetSize((400, -1)) -# bugs.SetDimensions(-1, -1, -1, -1) - sizer = wxBoxSizer(wxVERTICAL) - sizer.Add(bugs, wxGROW) - frame.SetSizer(sizer) - return true - -class BugList(wxListCtrl, wxListCtrlAutoWidthMixin): + return True + +class BugListFrame(wx.Frame): + def __init__(self, *args, **kwargs): + wx.Frame.__init__(self, *args, **kwargs) + bugs = BugList(self) + + # Widgets to display/sort/edit will go in this panel + # for now it is just a placeholder + panel = wx.Panel(self) + panel.SetBackgroundColour("RED") + + vbox = wx.BoxSizer(wx.VERTICAL) + vbox.Add(panel, 0, wx.EXPAND) + vbox.Add(bugs, 1, wx.EXPAND) + + self.SetAutoLayout(True) + self.SetSizer(vbox) + self.Layout() + +class BugList(wx.ListCtrl, ListCtrlAutoWidthMixin): def __init__(self, parent): - wxListCtrl.__init__(self, parent, -1, - style = wxLC_REPORT|wxLC_VRULES|wxLC_HRULES) - wxListCtrlAutoWidthMixin.__init__(self) - columns = ("Severity", "Creator", "Summary") - for x in range(len(columns)): - self.InsertColumn(x, columns[x]) - self.SetColumnWidth(x, wxLIST_AUTOSIZE_USEHEADER) - for bug in [b for b in bugdir.tree_root(".").list() if b.active]: - id = self.InsertStringItem(self.GetItemCount(), bug.severity) - self.SetStringItem(id, 1, bug.creator) - self.SetStringItem(id, 2, bug.summary) + wx.ListCtrl.__init__(self, parent, + style=wx.LC_REPORT) + ListCtrlAutoWidthMixin.__init__(self) + + self.bugdir = bugdir.tree_root(".") + self.buglist = list(self.bugdir.list()) + self.buglist.sort() + self.columns = ("id", "status", "severity", "summary") + + dataIndex = 0 + for x in range(len(self.columns)): + self.InsertColumn(x, self.columns[x].capitalize()) + self.SetColumnWidth(x, wx.LIST_AUTOSIZE_USEHEADER) + for bug in [b for b in self.buglist if b.active]: + name = names.unique_name(bug, self.buglist) + id = self.InsertStringItem(self.GetItemCount(), name) + self.SetStringItem(id, 1, bug.status) + self.SetStringItem(id, 2, bug.severity) + self.SetStringItem(id, 3, bug.summary) + self.SetItemData(id, dataIndex) # set keys for each line + dataIndex += 1 self.EnsureVisible(id) - for x in range(len(columns)): - self.SetColumnWidth(x, wxLIST_AUTOSIZE) + for x in range(len(self.columns)): + self.SetColumnWidth(x, wx.LIST_AUTOSIZE) conts_width = self.GetColumnWidth(x) - self.SetColumnWidth(x, wxLIST_AUTOSIZE_USEHEADER) + self.SetColumnWidth(x, wx.LIST_AUTOSIZE_USEHEADER) if conts_width > self.GetColumnWidth(x): self.SetColumnWidth(x, conts_width) + self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColumnClick) + self.bugcmp_fn = cmp_full + # For reasons I don't understant, sorting is broken... + #self.SortItems(self.Sorter) + #self.Refresh() + def Sorter(self, key1, key2): + """Get bug info from the keys and pass to self.bugcmp_fn""" + bug1 = self.buglist[key1-1] + bug2 = self.buglist[key2-1] + # Another way of getting bug information + #bug1uuid = self.GetItem(key1, 0).GetText() + #bug2uuid = self.GetItem(key2, 0).GetText() + #print bug1uuid, bug2uuid + #bug1 = self.bugdir.get_bug(bug1uuid) + #bug2 = self.bugdir.get_bug(bug1uuid) + print self.bugcmp_fn(bug1,bug2) + return self.bugcmp_fn(bug1,bug2) + def OnColumnClick(self, event): + """Resort bug list depending on which column was clicked""" + print "TODO: sort by column %d" % event.Column + # change self.bugcmp_fn and resort, but I can't get it working -app = MyApp(0) +app = MyApp() app.MainLoop() @@ -1,40 +1,51 @@ -"""Usage: python test.py [module] +"""Usage: python test.py [module(s) ...] -When called without an optional module name, run the doctests from -*all* modules. This may raise lots of errors if you haven't installed -one of the versioning control systems. +When called without optional module names, run the doctests from *all* +modules. This may raise lots of errors if you haven't installed one +of the versioning control systems. -When called with an optional module name, only run the doctests from -that module. +When called with module name arguments, only run the doctests from +those modules. """ from libbe import plugin +import unittest import doctest import sys + +suite = unittest.TestSuite() + if len(sys.argv) > 1: - match = False - libbe_failures = libbe_tries = becommands_failures = becommands_tries = 0 - mod = plugin.get_plugin("libbe", sys.argv[1]) - if mod is not None: - libbe_failures, libbe_tries = doctest.testmod(mod) - match = True - mod = plugin.get_plugin("becommands", sys.argv[1]) - if mod is not None: - becommands_failures, becommands_tries = doctest.testmod(mod) - match = True - if not match: - print "No modules match \"%s\"" % sys.argv[1] - sys.exit(1) - else: - sys.exit(libbe_failures or becommands_failures) + 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 + mod = plugin.get_plugin("becommands", submodname) + if mod is not None: + suite.addTest(doctest.DocTestSuite(mod)) + match = True + if not match: + print "No modules match \"%s\"" % submodname + sys.exit(1) else: failed = False - for module in plugin.iter_plugins("libbe"): - failures, tries = doctest.testmod(module[1]) - if failures: - failed = True - for module in plugin.iter_plugins("becommands"): - failures, tries = doctest.testmod(module[1]) - if failures: - failed = True - sys.exit(failed) + for modname,module in plugin.iter_plugins("libbe"): + if not hasattr(module, "suite"): + continue + suite.addTest(module.suite) + for modname,module in plugin.iter_plugins("becommands"): + suite.addTest(doctest.DocTestSuite(module)) + +#for s in suite._tests: +# print s +#exit(0) +result = unittest.TextTestRunner(verbosity=2).run(suite) + +numErrors = len(result.errors) +numFailures = len(result.failures) +numBad = numErrors + numFailures +if numBad > 126: + numBad = 1 +sys.exit(numBad) diff --git a/test_usage.sh b/test_usage.sh new file mode 100755 index 0000000..43b5d4d --- /dev/null +++ b/test_usage.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# +# Run through some simple usage cases. This both tests that important +# features work, and gives an example of suggested usage to get people +# started. +# +# usage: test_usage.sh RCS +# where RCS is one of: +# bzr, git, hg, arch, none +# +# Note that this script uses the *installed* version of be, not the +# one in your working tree. + +set -e # exit imediately on failed command +set -o pipefail # pipes fail if any stage fails +set -v # verbose, echo commands to stdout + +exec 6>&2 # save stderr to file descriptor 6 +exec 2>&1 # fd 2 now writes to stdout + +if [ $# -gt 1 ] +then + echo "usage: test_usage.sh [RCS]" + echo "" + echo "where RCS is one of" + for RCS in bzr git hg arch none + do + echo " $RCS" + done + exit 1 +elif [ $# -eq 0 ] +then + for RCS in bzr git hg arch none + do + echo -e "\n\nTesting $RCS\n\n" + $0 "$RCS" || exit 1 + done + exit 0 +fi + +RCS="$1" + +TESTDIR=`mktemp -d /tmp/BEtest.XXXXXXXXXX` +cd $TESTDIR + +if [ "$RCS" == "bzr" ] +then + ID=`bzr whoami` + bzr init +elif [ "$RCS" == "git" ] +then + NAME=`git-config user.name` + EMAIL=`git-config user.email` + ID="$NAME <$EMAIL>" + git init +elif [ "$RCS" == "hg" ] +then + ID=`hg showconfig ui.username` + hg init +elif [ "$RCS" == "arch" ] +then + ID=`tla my-id` + ARCH_PARAM_DIR="$HOME/.arch-params" + ARCH_ARCHIVE_ROOT=`mktemp -d /tmp/BEtest.XXXXXXXXXX` + UNIQUE=`echo "$ARCH_ARCHIVE_ROOT" | sed 's/\/tmp\/BEtest.//;s/[0-9]//g'` + ARCH_ARCHIVE="j@x.com--BE-test-usage-$UNIQUE" + ARCH_PROJECT="BE-test-usage--twig--99.5" + ARCH_ARCHIVE_DIR="$ARCH_ARCHIVE_ROOT/$ARCH_PROJECT" + echo "tla make-archive $ARCH_ARCHIVE $ARCH_ARCHIVE_DIR" + tla make-archive $ARCH_ARCHIVE $ARCH_ARCHIVE_DIR + echo "tla archive-setup -A $ARCH_ARCHIVE $ARCH_PROJECT" + tla archive-setup -A $ARCH_ARCHIVE $ARCH_PROJECT + echo "tla init-tree -A $ARCH_ARCHIVE $ARCH_PROJECT" + tla init-tree -A $ARCH_ARCHIVE $ARCH_PROJECT + echo "Adjusing the naming conventions to allow .files" + sed -i 's/^source .*/source ^[._=a-zA-X0-9].*$/' '{arch}/=tagging-method' + echo "tla import -A $ARCH_ARCHIVE --summary 'Began versioning'" + tla import -A $ARCH_ARCHIVE --summary 'Began versioning' +elif [ "$RCS" == "none" ] +then + ID=`id -nu` +else + echo "Unrecognized RCS '$RCS'" + exit 1 +fi +if [ -z "$ID" ] +then # set a default ID + ID="John Doe <jdoe@example.com>" +fi +echo "I am '$ID'" + +be set-root +OUT=`be new 'having too much fun'` +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 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' +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 +be list -m -s fixed # see fixed bugs assigned to you +be assign $BUG 'Joe' # assign the bug to Joe +be list -a Joe -s fixed # list the fixed bugs assigned to Joe +be assign $BUG none # assign the bug to noone +be diff # see what has changed +be remove $BUG # decide that you don't like that bug after all +cd / +rm -rf $TESTDIR + +if [ "$RCS" == "arch" ] +then + # Cleanup everything outside of TESTDIR + rm -rf "$ARCH_ARCHIVE_ROOT" + rm -rf "$ARCH_PARAM_DIR/=locations/$ARCH_ARCHIVE" +fi + +exec 2>&6 6>&- # restore stderr and close fd 6 |