Hi, I belive that cloning a bug is a useful feature. Below I'm
attaching a patch that should do the trick. I attempted to follow
the behaviour of Bugzilla's web UI:
----------------8<---------------------------------------------
diff --git a/bugzilla/base.py b/bugzilla/base.py
index 7748e2b..7e148c4 100644
--- a/bugzilla/base.py
+++ b/bugzilla/base.py
@@ -618,6 +618,17 @@ class BugzillaBase(object):
You may also add a "nomail":1 item, which will suppress email if set.'''
raise NotImplementedError
+ #---- Methods for cloning bugs.
+
+ def _clonebug(self,id,product=None,component=None,version=None):
+ '''IMPLEMENT ME: Clone the given bug. This is the raw call, and no data checking is
+ done here. That's up to the clonebug method.'''
+ raise NotImplementedError
+
+ def clonebug(self,product=None,component=None,version=None):
+ '''Clone the given bug.'''
+ return self.bugzilla._clonebug(self.bug_id,product,component,version)
+
#---- Methods for working with attachments
def _attachment_encode(self,fh):
@@ -1305,6 +1316,11 @@ class _Bug(object):
'''An alias for refresh()'''
self.refresh()
+ def clonebug(self,product=None,component=None,version=None):
+ '''Clone the given bug. This is the raw call, and no data checking is
+ done here. That's up to the clonebug method.'''
+ return self.bugzilla._clonebug(self.bug_id,product,component,version)
+
def setstatus(self,status,comment='',private=False,private_in_it=False,nomail=False):
'''Update the status for this bug report.
Valid values for status are listed in querydefaults['bug_status_list']
diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py
index 4233d32..59fb418 100644
--- a/bugzilla/rhbugzilla.py
+++ b/bugzilla/rhbugzilla.py
@@ -130,6 +130,17 @@ class RHBugzilla(Bugzilla4):
requestee (as in: needinfo from smartguy(a)answers.com) Alas.'''
return self._proxy.bugzilla.updateFlags(id,flags)
+ #---- Methods for cloning bugs.
+
+ def _clonebug(self,id,product=None,component=None,version=None):
+ '''IMPLEMENT MEClone the given bug. This is the raw call, and no data checking is
+ done here. That's up to the clonebug method.'''
+ raise NotImplementedError
+
+ def clonebug(self,product=None,component=None,version=None):
+ '''Clone the given bug.'''
+ return self.bugzilla._clonebug(self.bug_id,product,component,version)
+
#---- Methods for working with attachments
# If your bugzilla wants attachments in something other than base64, you
@@ -281,6 +292,94 @@ class RHBugzilla(Bugzilla4):
return self._close_bug(id, update)
#return self._update_bug(id,update)
+ def _clonebug(self,id,product=None,component=None,version=None):
+ '''Clone this bug similarly as one can do it in the webui'''
+ origbug = self.getbug(id)
+ clone_message = '+++ This bug was initially created as a clone of Bug #%s +++\n\n' % (origbug.id)
+ isprivate = False
+ isadditional = False
+ for rec in origbug.longdescs:
+ if isadditional:
+ clone_message += '\n--- Additional comment from '
+ clone_message += rec['author']['realname']
+ clone_message += ' on '
+ clone_message += rec['time']
+ clone_message += ' EDT ---\n\n'
+ if 'extra_data' in rec.keys():
+ clone_message += '\n\n*** This bug has been marked as a duplicate of bug %s ***\n' % (rec['extra_data'])
+ else:
+ clone_message += rec['body'] + "\n"
+ isadditional = True
+ if rec['isprivate'] == 1:
+ isprivate = True
+ cc = origbug.cc
+ cc.append(origbug.reporter)
+ depends_on = str(origbug.bug_id) + ' '
+ depends_on += ' '.join([str(i) for i in origbug.dependson])
+ new_product = product
+ if new_product == None: new_product = origbug.product
+ new_component = component
+ if new_component == None: new_component = origbug.component
+ new_version = version
+ new_target_release = None
+ if new_version == None and new_product != origbug.product:
+ releases = self._proxy.Product.get({'names': new_product, 'include_fields': ['releases']})
+ prodinfo = releases['products'][0]['releases']
+ versions = [p['name'] for p in prodinfo]
+ new_version = versions[-1]
+ elif new_version == None:
+ new_target_release = origbug.target_release
+ new_version = origbug.version
+ kwargs = {
+ 'product': new_product,
+ 'component': new_component,
+ 'version': new_version,
+ 'op_sys': origbug.op_sys,
+ 'platform': origbug.platform,
+ 'summary': origbug.summary,
+ 'description': clone_message,
+ 'comment_is_private': isprivate,
+ 'priority': origbug.priority,
+ 'bug_severity': origbug.bug_severity,
+ 'depends_on': depends_on,
+ 'blocked': origbug.blocked,
+ 'whiteboard': origbug.whiteboard,
+ 'keywords': origbug.keywords,
+ 'cc': cc,
+ 'estimated_time': origbug.estimated_time,
+ 'remaining_time': origbug.remaining_time,
+ 'deadline': origbug.deadline,
+ 'url': origbug.bug_file_loc,
+ 'target_release': new_target_release
+ }
+ for key in kwargs.keys():
+ if kwargs[key] == None:
+ del kwargs[key]
+ newbug = self.createbug(**kwargs)
+ origgroups = [grp['name'] for grp in origbug.groups if grp['ison'] == 1]
+ update = dict(
+ cf_clone_of = str(origbug.id),
+ groups = dict(add = origgroups),
+ cf_qa_whiteboard = origbug.qa_whiteboard,
+ cf_cust_facing = origbug.cust_facing,
+ cf_devel_whiteboard = origbug.devel_whiteboard,
+ cf_internal_whiteboard = origbug.internal_whiteboard,
+ cf_build_id = origbug.build_id,
+ cf_partner = origbug.partner,
+ cf_verified = ['Any'],
+ cf_environment = origbug.cf_environment
+ )
+ changes = self._update_bug(newbug.id,update)
+ # external tracker references
+ extbugs = []
+ for eb in origbug.external_bugs:
+ neb = {}
+ neb['ext_bz_bug_id'] = eb['ext_bz_bug_id']
+ neb['ext_type_id'] = eb['type']['id']
+ extbugs.append(neb)
+ self._proxy.ExternalBugs.add_external_bug({'bug_ids': [newbug.id], 'external_bugs': extbugs})
+ return newbug
+
def _setassignee(self,id,**data):
'''Raw xmlrpc call to set one of the assignee fields on a bug.
changeAssignment($id, $data, $username, $password)
----------------8<---------------------------------------------
Martin