2014-09-15 16 views
5

Ive ha appena spostato il codice dei miei progetti da java.net a BitBucket. Ma il mio problema di tracciamento di jira è ancora ospitato su java.net, sebbene BitBucket abbia alcune opzioni per il collegamento a un tracker di problemi esterni. Non penso di poterlo usare per java.net, non ultimo perché non ho i privilegi di amministratore è necessario installare il connettore DVCS.Come esportare i problemi Jira in BitBucket

Quindi ho pensato che sarebbe stata un'opzione alternativa per esportare e quindi importare i problemi nel tracker dei problemi di BitBucket, è possibile?

I progressi compiuti finora Così ho provato seguendo la procedura descritta in entrambe le risposte informative utilizzando OSX sotto, ma mi ha colpito un problema - Sono un po 'confuso su ciò che lo script sarebbe in realtà essere chiamato perché nelle risposte si parla di esportazione .py ma non esiste uno script simile con quel nome, quindi ho rinominato quello che ho scaricato.

  • sudo easy_install pip (OSX)
  • pip install jira
  • PIP installare ConfigParser
  • easy_install -U setuptools
  • Vai a https://bitbucket.org/reece/rcore, selezionare la scheda download, scaricare compressione e decompressione, e rinominare a reece (per qualche motivo git clone https://bitbucket.org/reece/rcore fallisce con errore)
  • cd reece/rcore
  • Salva script come export.py in rcore sottocartella
  • Sostituire iteritems con gli elementi in import.py
  • Sostituire iteritems con i tipi/immutabledict.py
  • Crea .config nella cartella rcore

  • Crea .config/jira-problemi -move-to-bitbucket.conf contenente

    jira-username = paultaylor

    jira-host = https://java.net/jira/browse/JAUDIOTAGGER

    jira-password = password

  • Run python export.py --jira-progetto 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 

ho bisogno di correre insdtall pip come root così ha fatto

  • sudo pip installa configparser

e che ha funzionato

ma ora

  • pitone export.py --jira.progetto jaudiotagger

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

è 'pip install configparser' non riesce o ha esito positivo? Il tuo errore sta dicendo che manca il 'configparser'. Provalo di nuovo e vedi se ottieni un "Requisito già soddisfatto". – Fabio

+0

@Fabio Grazie ha provato a reinstallare e c'era un errore che dovevo aver perso che in qualche modo, quindi ho provato con 'sudo pip install configparser' e questo ha funzionato. Ma ora quando riprovo non riesco a lamentarsi di nessun modulo chiamato jira.client –

+0

oh ovviamente ho bisogno di fare sudo pip installare jira, ma ora fallisce su NESSUN modulo chiamato rcore.types.immutabledict –

risposta

3

È possibile importare le questioni in BitBucket, hanno solo bisogno di essere in appropriate format. Fortunatamente, Reece Hart ha già written a Python script per connettersi a un'istanza di Jira ed esportare i problemi.

Per ottenere lo script da eseguire ho dovuto installare il Jira Python package così come l'ultima versione di rcore (se si utilizza pip si ottiene una versione precedente non compatibile, in modo da avere per ottenere la fonte). Ho anche dovuto sostituire tutte le istanze di iteritems con items nello script e in rcore/types/immutabledict.py per farlo funzionare con Python 3. Sarà inoltre necessario compilare i dizionari (priority_map, person_map, ecc.) Con i valori utilizzati dal progetto. Infine, è necessario un file di configurazione con le informazioni di connessione (vedere i commenti nella parte superiore dello script).

L'uso di base riga di comando è export.py --jira-project <project>

Una volta che hai i dati esportati, vedere la 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

grazie Ill dare questo andare e riportare –

+0

Ciao @Turch, ho fatto tutto come hai descritto, ma sto ricevendo un errore quando eseguo lo script: 'requests.exceptions.ConnectionError: ('Connection aborted.', Gaierror (8, 'nodename né servname fornito, o non noto')) '. Hai un'idea a riguardo? Grazie! – Fabio

+1

@Fabio Sembra che non sappia a quale server connettersi, quindi indovinerei un problema con il file di configurazione. Di default lo script lo cerca in ~ ~/.config/jira-issues-move-to-bitbucket.conf' (vedi i commenti nella parte superiore dello script) ma da quando ero su una macchina Windows l'ho sostituito con un local './jira-issues-move-to-bitbucket.conf'. Anche se sto assumendo che possa leggere il file poiché altrimenti probabilmente darebbe un errore diverso ... – Turch

1

NOTA: Sto scrivendo una nuova risposta, perché scrivere questo in un commento sarebbe essere orribile, ma la maggior parte del merito va alla risposta di @ Turch.

miei passi (in macchine OSX e Debian, sia lavorato bene):

  1. apt-get install python-pip (Debian) o sudo easy_install pip (OSX)
  2. pip install jira
  3. pip install configparser
  4. easy_install -U setuptools (non sono sicuro se veramente necessario)
  5. Scaricare o clonare il codice sorgente da https://bitbucket.org/reece/rcore/ nella cartella Inizio, ad esempio. Nota: non scaricare utilizzando pip, otterrà la versione 0.0.2 e sarà necessario il 0.0.3.
  6. Scarica il Python script creato da Reece, citato da @Turch, e posizionarlo all'interno della cartella rcore.
  7. Seguire le istruzioni di @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). Nota: Ho usato il nome host come jira.domain.com (no http o https).
  8. (Questo cambiamento ha fatto il trucco per me) ho dovuto cambiare parte della linea 250 da 'https://{opts.jira_hostname}/' a 'http://{opts.jira_hostname}/'
  9. Per finire, eseguire lo script come @Turch menzionati: The basic command line usage is export.py --jira-project <project>
  10. Il file è stato posto in/tmp /.zip per me.
  11. Il file è stato perfettamente accettato nell'importatore BitBucket oggi.

Urrà per Reece e Turch! Grazie ragazzi!

+0

Ive ha avuto un tentativo ma si è bloccato per favore vedi l'aggiornamento alla mia domanda –

Problemi correlati