2014-09-15 7 views
5

exportieren Ich habe gerade meine Projekte Code von java.net zu BitBucket bewegt. Aber mein jira-Problem-Tracking wird immer noch auf java.net gehostet, obwohl BitBucket einige Optionen zum Verknüpfen mit einem externen Issue-Tracker bietet. Ich glaube nicht, dass ich es für java.net verwenden kann, nicht zuletzt, weil ich keine Admin-Privilegien habe müssen den DVCS-Anschluss installieren.Wie kann ich Jira Probleme zu BitBucket

Also dachte ich, eine alternative Option wäre, die Ausgaben in BitBucket Issue Tracker zu exportieren und dann zu importieren, ist das möglich?

Fortschritt bisher So habe ich versucht, die Schritte in beiden informativen Antworten folgende mit OSX unten, aber ich traf ein Problem - ich bin ziemlich verwirrt darüber, was das Skript tatsächlich, weil in den Antworten genannt würde es über den Export spricht .py, aber kein solches Skript existiert mit diesem Namen, also habe ich den heruntergeladen, den ich heruntergeladen habe.

  • sudo easy_install pip (OSX)
  • jira installieren pip
  • ConfigParser
  • easy_install -U Setuptools
  • Zum https://bitbucket.org/reece/rcore, wählen Sie Registerkarte Downloads, Download-zip installieren PIP- und entpacken und umbenennen reece (aus irgendeinem Grund git Klon https://bitbucket.org/reece/rcore schlägt fehl)
  • cd reece/rcore
  • Skript speichern unter export.py in rcore Unterordner
  • ersetzen iteritems mit Elementen in import.py
  • ersetzen iteritems mit types/immutabledict.py
  • in rcore Ordner erstellen .config

  • .config/jira-Ausgaben erstellen -move-to-bitbucket.conf

    jira-username = paultaylor

    jira-Host-Namen enthält, =

    https://java.net/jira/browse/JAUDIOTAGGERKennwort

    jira-password =

  • Run Python export.py --jira-Projekt jaudiotagger

macbook:rcore paul$ python export.py --jira-project jaudiotagger 
Traceback (most recent call last): 
    File "export.py", line 24, in <module> 
    import configparser 
ImportError: No module named configparser 
- Run python export.py --jira-project jaudiotagger 

gibt Ich brauche pip insdtall laufen als root so

tat
  • sudo pip installieren configparser

und arbeitete

aber jetzt

  • Python export.py --jira.jaudiotagger Projekt

File "export.py" line 35, in <module? 
    from jira.client import JIRA 
ImportError: No module named jira.client 
+0

ist 'pip install configparser' fehlgeschlagen oder erfolgreich? Ihr Fehler besagt, dass der 'configparser' fehlt. Probieren Sie es noch einmal und sehen Sie, ob Sie eine Anforderung bereits erfüllt haben. – Fabio

+0

@Fabio Danke, es hat versucht, neu zu installieren und es gab einen Fehler, den ich irgendwie vermisst haben muss, also habe ich es mit 'sudo pip install configparser' versucht und das hat funktioniert. Aber jetzt, wenn ich es erneut versuche beschweren kein Modul namens jira.client –

+0

oh natürlich muss ich sudo pip installieren jira, aber jetzt scheitert es auf kein Modul namens rcore.types.immutabledict –

Antwort

3

Sie gibt Probleme in BitBucket importieren können, müssen sie nur in der appropriate format sein. Glücklicherweise hat Reece Hart bereits written a Python script, um sich mit einer Jira-Instanz zu verbinden und die Probleme zu exportieren.

Um das Skript zu starten, musste ich die Jira Python package sowie die neueste Version von rcore installieren (wenn Sie pip verwenden, erhalten Sie eine inkompatible vorherige Version, so müssen Sie die Quelle erhalten). Ich musste auch alle Instanzen von iteritems durch items im Skript und in rcore/types/immutabledict.py ersetzen, damit es mit Python 3 funktioniert. Sie müssen auch die Wörterbücher (priority_map, person_map, usw.) mit den Werten füllen, die Ihr Projekt verwendet. Schließlich benötigen Sie eine Konfigurationsdatei mit den Verbindungsinformationen (siehe Kommentare am Anfang des Skripts).

Die grundlegende Kommandozeilennutzung ist export.py --jira-project <project>

Nachdem Sie die Daten exportiert haben, finden Sie in den instructions for importing issues to BitBucket

#!/usr/bin/env python 

"""extract issues from JIRA and export to a bitbucket archive 

See: 
https://confluence.atlassian.com/pages/viewpage.action?pageId=330796872 
https://confluence.atlassian.com/display/BITBUCKET/Mark+up+comments 
https://bitbucket.org/tutorials/markdowndemo/overview 

2014-04-12 08:26 Reece Hart <[email protected]> 


Requires a file ~/.config/jira-issues-move-to-bitbucket.conf 
with content like 
[default] 
jira-username=some.user 
jira-hostname=somewhere.jira.com 
jira-password=ur$pass 

""" 

import argparse 
import collections 
import configparser 
import glob 
import itertools 
import json 
import logging 
import os 
import pprint 
import re 
import sys 
import zipfile 

from jira.client import JIRA 

from rcore.types.immutabledict import ImmutableDict 


priority_map = { 
    'Critical (P1)': 'critical', 
    'Major (P2)': 'major', 
    'Minor (P3)': 'minor', 
    'Nice (P4)': 'trivial', 
    } 
person_map = { 
    'reece.hart': 'reece', 
    # etc 
    } 
issuetype_map = { 
    'Improvement': 'enhancement', 
    'New Feature': 'enhancement', 
    'Bug': 'bug', 
    'Technical task': 'task', 
    'Task': 'task', 
    } 
status_map = { 
    'Closed': 'resolved', 
    'Duplicate': 'duplicate', 
    'In Progress': 'open', 
    'Open': 'new', 
    'Reopened': 'open', 
    'Resolved': 'resolved', 
    } 



def parse_args(argv): 
    def sep_and_flatten(l): 
     # split comma-sep elements and flatten list 
     # e.g., ['a','b','c,d'] -> set('a','b','c','d') 
     return list(itertools.chain.from_iterable(e.split(',') for e in l)) 

    cf = configparser.ConfigParser() 
    cf.readfp(open(os.path.expanduser('~/.config/jira-issues-move-to-bitbucket.conf'),'r')) 

    ap = argparse.ArgumentParser(
     description = __doc__ 
     ) 

    ap.add_argument(
     '--jira-hostname', '-H', 
     default = cf.get('default','jira-hostname',fallback=None), 
     help = 'host name of Jira instances (used for url like https://hostname/, e.g., "instancename.jira.com")', 
     ) 
    ap.add_argument(
     '--jira-username', '-u', 
     default = cf.get('default','jira-username',fallback=None), 
     ) 
    ap.add_argument(
     '--jira-password', '-p', 
     default = cf.get('default','jira-password',fallback=None), 
     ) 
    ap.add_argument(
     '--jira-project', '-j', 
     required = True, 
     help = 'project key (e.g., JRA)', 
     ) 
    ap.add_argument(
     '--jira-issues', '-i', 
     action = 'append', 
     default = [], 
     help = 'issue id (e.g., JRA-9); multiple and comma-separated okay; default = all in project', 
     ) 
    ap.add_argument(
     '--jira-issues-file', '-I', 
     help = 'file containing issue ids (e.g., JRA-9)' 
     ) 
    ap.add_argument(
     '--jira-components', '-c', 
     action = 'append', 
     default = [], 
     help = 'components criterion; multiple and comma-separated okay; default = all in project', 
     ) 
    ap.add_argument(
     '--existing', '-e', 
     action = 'store_true', 
     default = False, 
     help = 'read existing archive (from export) and merge new issues' 
     ) 

    opts = ap.parse_args(argv) 

    opts.jira_components = sep_and_flatten(opts.jira_components) 
    opts.jira_issues = sep_and_flatten(opts.jira_issues) 

    return opts 


def link(url,text=None): 
    return "[{text}]({url})".format(url=url,text=url if text is None else text) 

def reformat_to_markdown(desc): 
    def _indent4(mo): 
     i = " " 
     return i + mo.group(1).replace("\n",i) 
    def _repl_mention(mo): 
     return "@" + person_map[mo.group(1)] 
    #desc = desc.replace("\r","") 
    desc = re.sub("{noformat}(.+?){noformat}",_indent4,desc,flags=re.DOTALL+re.MULTILINE) 
    desc = re.sub(opts.jira_project+r"-(\d+)",r"issue #\1",desc) 
    desc = re.sub(r"\[~([^]]+)\]",_repl_mention,desc) 
    return desc 

def fetch_issues(opts,jcl): 
    jql = [ 'project = ' + opts.jira_project ] 
    if opts.jira_components: 
     jql += [ ' OR '.join([ 'component = '+c for c in opts.jira_components ]) ] 
    if opts.jira_issues: 
     jql += [ ' OR '.join([ 'issue = '+i for i in opts.jira_issues ]) ] 
    jql_str = ' AND '.join(["("+q+")" for q in jql]) 
    logging.info('executing query ' + jql_str) 
    return jcl.search_issues(jql_str,maxResults=500) 


def jira_issue_to_bb_issue(opts,jcl,ji): 
    """convert a jira issue to a dictionary with values appropriate for 
    POSTing as a bitbucket issue""" 
    logger = logging.getLogger(__name__) 

    content = reformat_to_markdown(ji.fields.description) if ji.fields.description else '' 

    if ji.fields.assignee is None: 
     resp = None 
    else: 
     resp = person_map[ji.fields.assignee.name] 

    reporter = person_map[ji.fields.reporter.name] 

    jiw = jcl.watchers(ji.key) 
    watchers = [ person_map[u.name] for u in jiw.watchers ] if jiw else [] 

    milestone = None 
    if ji.fields.fixVersions: 
     vnames = [ v.name for v in ji.fields.fixVersions ] 
     milestone = vnames[0] 
     if len(vnames) > 1: 
      logger.warn("{ji.key}: bitbucket issues may have only 1 milestone (JIRA fixVersion); using only first ({f}) and ignoring rest ({r})".format(
       ji=ji, f=milestone, r=",".join(vnames[1:]))) 

    issue_id = extract_issue_number(ji.key) 

    bbi = { 
     'status': status_map[ji.fields.status.name], 
     'priority': priority_map[ji.fields.priority.name], 
     'kind': issuetype_map[ji.fields.issuetype.name], 
     'content_updated_on': ji.fields.created, 
     'voters': [], 
     'title': ji.fields.summary, 
     'reporter': reporter, 
     'component': None, 
     'watchers': watchers, 
     'content': content, 
     'assignee': resp, 
     'created_on': ji.fields.created, 
     'version': None,     # ? 
     'edited_on': None, 
     'milestone': milestone, 
     'updated_on': ji.fields.updated, 
     'id': issue_id, 
     } 

    return bbi 


def jira_comment_to_bb_comment(opts,jcl,jc): 
    bbc = { 
     'content': reformat_to_markdown(jc.body), 
     'created_on': jc.created, 
     'id': int(jc.id), 
     'updated_on': jc.updated, 
     'user': person_map[jc.author.name], 
     } 
    return bbc 

def extract_issue_number(jira_issue_key): 
    return int(jira_issue_key.split('-')[-1]) 
def jira_key_to_bb_issue_tag(jira_issue_key): 
    return 'issue #' + str(extract_issue_number(jira_issue_key)) 

def jira_link_text(jk): 
    return link("https://invitae.jira.com/browse/"+jk,jk) + " (Invitae access required)" 


if __name__ == '__main__': 
    logging.basicConfig(level=logging.INFO) 
    logger = logging.getLogger(__name__) 


    opts = parse_args(sys.argv[1:]) 

    dir_name = opts.jira_project 
    if opts.jira_components: 
     dir_name += '-' + ','.join(opts.jira_components) 

    if opts.jira_issues_file: 
     issues = [i.strip() for i in open(opts.jira_issues_file,'r')] 
     logger.info("added {n} issues from {opts.jira_issues_file} to issues list".format(n=len(issues),opts=opts)) 
     opts.jira_issues += issues 

    opts.dir = os.path.join('/','tmp',dir_name) 
    opts.att_rel_dir = 'attachments' 
    opts.att_abs_dir = os.path.join(opts.dir,opts.att_rel_dir) 
    opts.json_fn = os.path.join(opts.dir,'db-1.0.json') 
    if not os.path.isdir(opts.att_abs_dir): 
     os.makedirs(opts.att_abs_dir) 

    opts.jira_issues = list(set(opts.jira_issues)) # distinctify 

    jcl = JIRA({'server': 'https://{opts.jira_hostname}/'.format(opts=opts)}, 
     basic_auth=(opts.jira_username,opts.jira_password)) 


    if opts.existing: 
     issues_db = json.load(open(opts.json_fn,'r')) 
     existing_ids = [ i['id'] for i in issues_db['issues'] ] 
     logger.info("read {n} issues from {fn}".format(n=len(existing_ids),fn=opts.json_fn)) 
    else: 
     issues_db = dict() 
     issues_db['meta'] = { 
      'default_milestone': None, 
      'default_assignee': None, 
      'default_kind': "bug", 
      'default_component': None, 
      'default_version': None, 
      } 
     issues_db['attachments'] = [] 
     issues_db['comments'] = [] 
     issues_db['issues'] = [] 
     issues_db['logs'] = [] 

    issues_db['components'] = [ {'name':v.name} for v in jcl.project_components(opts.jira_project) ] 
    issues_db['milestones'] = [ {'name':v.name} for v in jcl.project_versions(opts.jira_project) ] 
    issues_db['versions'] = issues_db['milestones'] 


    # bb_issue_map: bb issue # -> bitbucket issue 
    bb_issue_map = ImmutableDict((i['id'],i) for i in issues_db['issues']) 

    # jk_issue_map: jira key -> bitbucket issue 
    # contains only items migrated from JIRA (i.e., not preexisting issues with --existing) 
    jk_issue_map = ImmutableDict() 

    # issue_links is a dict of dicts of lists, using JIRA keys 
    # e.g., links['CORE-135']['depends on'] = ['CORE-137'] 
    issue_links = collections.defaultdict(lambda: collections.defaultdict(lambda: [])) 


    issues = fetch_issues(opts,jcl) 
    logger.info("fetch {n} issues from JIRA".format(n=len(issues))) 
    for ji in issues: 
     # Pfft. Need to fetch the issue again due to bug in JIRA. 
     # See https://bitbucket.org/bspeakmon/jira-python/issue/47/, comment on 2013-10-01 by ssonic 
     ji = jcl.issue(ji.key,expand="attachments,comments") 

     # create the issue 
     bbi = jira_issue_to_bb_issue(opts,jcl,ji) 
     issues_db['issues'] += [bbi] 

     bb_issue_map[bbi['id']] = bbi 
     jk_issue_map[ji.key] = bbi 
     issue_links[ji.key]['imported from'] = [jira_link_text(ji.key)] 

     # add comments 
     for jc in ji.fields.comment.comments: 
      bbc = jira_comment_to_bb_comment(opts,jcl,jc) 
      bbc['issue'] = bbi['id'] 
      issues_db['comments'] += [bbc] 

     # add attachments 
     for ja in ji.fields.attachment: 
      att_rel_path = os.path.join(opts.att_rel_dir,ja.id) 
      att_abs_path = os.path.join(opts.att_abs_dir,ja.id) 

      if not os.path.exists(att_abs_path): 
       open(att_abs_path,'w').write(ja.get()) 
       logger.info("Wrote {att_abs_path}".format(att_abs_path=att_abs_path)) 
      bba = { 
       "path": att_rel_path, 
       "issue": bbi['id'], 
       "user": person_map[ja.author.name], 
       "filename": ja.filename, 
       } 
      issues_db['attachments'] += [bba] 

     # parent-child is task-subtask 
     if hasattr(ji.fields,'parent'): 
      issue_links[ji.fields.parent.key]['subtasks'].append(jira_key_to_bb_issue_tag(ji.key)) 
      issue_links[ji.key]['parent task'].append(jira_key_to_bb_issue_tag(ji.fields.parent.key)) 

     # add links 
     for il in ji.fields.issuelinks: 
      if hasattr(il,'outwardIssue'): 
       issue_links[ji.key][il.type.outward].append(jira_key_to_bb_issue_tag(il.outwardIssue.key)) 
      elif hasattr(il,'inwardIssue'): 
       issue_links[ji.key][il.type.inward].append(jira_key_to_bb_issue_tag(il.inwardIssue.key)) 


     logger.info("migrated issue {ji.key}: {ji.fields.summary} ({components})".format(
      ji=ji,components=','.join(c.name for c in ji.fields.components))) 


    # append links section to content 
    # this section shows both task-subtask and "issue link" relationships 
    for src,dstlinks in issue_links.iteritems(): 
     if src not in jk_issue_map: 
      logger.warn("issue {src}, with issue_links, not in jk_issue_map; skipping".format(src=src)) 
      continue 

     links_block = "Links\n=====\n" 
     for desc,dsts in sorted(dstlinks.iteritems()): 
      links_block += "* **{desc}**: {links} \n".format(desc=desc,links=", ".join(dsts)) 

     if jk_issue_map[src]['content']: 
      jk_issue_map[src]['content'] += "\n\n" + links_block 
     else: 
      jk_issue_map[src]['content'] = links_block 


    id_counts = collections.Counter(i['id'] for i in issues_db['issues']) 
    dupes = [ k for k,cnt in id_counts.iteritems() if cnt>1 ] 
    if dupes: 
     raise RuntimeError("{n} issue ids appear more than once from existing {opts.json_fn}".format(
      n=len(dupes),opts=opts)) 

    json.dump(issues_db,open(opts.json_fn,'w')) 
    logger.info("wrote {n} issues to {opts.json_fn}".format(n=len(id_counts),opts=opts)) 


    # write zipfile 
    os.chdir(opts.dir) 
    with zipfile.ZipFile(opts.dir + '.zip','w') as zf: 
     for fn in ['db-1.0.json']+glob.glob('attachments/*'): 
      zf.write(fn) 
      logger.info("added {fn} to archive".format(fn=fn)) 
+0

danke Ich gebe dies ein und melde mich zurück –

+0

Hallo @Turch, Ich habe alles wie Sie beschrieben, aber ich bekomme einen Fehler, wenn ich das Skript ausführen: 'requests.exceptions.ConnectionError: ('Verbindung abgebrochen.', Gaierror (8, "Knotenname oder Servenname" oder "nicht bekannt")). Hast du eine Vorstellung davon? Vielen Dank! – Fabio

+1

@Fabio Sieht so aus, als wüsste er nicht, mit welchem ​​Server er sich verbinden soll, also würde ich ein Problem mit der Konfigurationsdatei erraten. Standardmäßig sucht das Skript danach in '~/.config/jira-issues-move-to-bitbucket.conf' (siehe die Kommentare am Anfang des Skripts), aber da ich auf einem Windows-Rechner war, habe ich das durch einen ersetzt local './jira-issues-move-to-bitbucket.conf'. Obwohl ich annehme, dass es die Datei lesen kann, da es sonst wahrscheinlich einen anderen Fehler geben würde ... – Turch

1

HINWEIS: Ich bin eine neue Antwort zu schreiben, weil dies in einem Kommentar zu schreiben würde schrecklich sein, aber der größte Teil des Verdienstes geht auf @ Turchs Antwort.

Meine Schritte (in OSX und Debian Maschinen arbeiteten beide gut):

  1. apt-get install python-pip (Debian) oder sudo easy_install pip (OSX)
  2. pip install jira
  3. pip install configparser
  4. easy_install -U setuptools (sicher nicht, wenn wirklich benötigt)
  5. Downloaden oder klonen Sie den Quellcode von https://bitbucket.org/reece/rcore/ in Ihrem Home-Ordner, zum Beispiel. Hinweis: Laden Sie nicht mit pip, es wird die 0.0.2 Version und Sie benötigen die 0.0.3.
  6. Laden Sie die Python script von Reece, erwähnt von @Turch, und legen Sie es in den Ordner rcore.
  7. Folgen Sie den Anweisungen von @Turch: I also had to replace all instances of iteritems with items in the script and in rcore/types/immutabledict.py to make it work with Python 3. You will also need to fill in the dictionaries (priority_map, person_map, etc) with the values your project uses. Finally, you need a config file to exist with the connection info (see comments at the top of the script). Hinweis: Ich habe Hostname wie jira.domain.com (keine http oder https) verwendet.
  8. (Diese Änderung hat den Trick für mich) ich ein Teil der Linie 250 von 'https://{opts.jira_hostname}/' zu 'http://{opts.jira_hostname}/'
  9. zu beenden, führen Sie das Skript wie @Turch erwähnt geändert hatte: The basic command line usage is export.py --jira-project <project>
  10. die Datei in/tmp gesetzt wurde /.zip für mich.
  11. Die Datei wurde heute im BitBucket-Importer perfekt akzeptiert.

Hurra für Reece und Turch! Danke Leute!

+0

Ich hatte einen Versuch, aber stecken geblieben Bitte sehen Sie das Update auf meine Frage –

Verwandte Themen