2014-09-15 13 views
5

Właśnie przeniosłem mój kod projektu z java.net do BitBucket. Ale moje śledzenie problemów z jirą jest nadal hostowane na java.net, chociaż BitBucket ma kilka opcji do łączenia z zewnętrznym trackerem problemów Nie sądzę, żebym mógł go użyć dla java.net, między innymi dlatego, że nie mam przywilejów administratora trzeba zainstalować złącze DVCS.Jak mogę eksportować problemy Jira do BitBucket

Więc pomyślałem, że alternatywną opcją byłoby wyeksportowanie, a następnie zaimportowanie problemów do trackera problemów BitBucket, czy to możliwe?

Progress dotychczas Spróbowałem więc czynności opisane w obu odpowiedziach informacyjnych z wykorzystaniem OSX poniżej ale hit problem - Jestem raczej mylić co skrypt byłby rzeczywiście zwane, ponieważ w odpowiedzi mówi o eksporcie .py ale nie istnieje taki skrypt o tej nazwie, więc zmieniłem nazwę na tę, którą pobrałem.

  • sudo easy_install pip (OSX)
  • pip zainstalować JIRA
  • pip zainstalować ConfigParser
  • easy_install -U setuptools
  • Przejdź do https://bitbucket.org/reece/rcore, wybierz zakładkę pobrania, do pobrania zip i rozpakować i zmienić nazwę na reece (z jakiegoś powodu git clone https://bitbucket.org/reece/rcore zawodzi z błędem)
  • cd reece/rcore
  • Zapisz skrypt jako export.py w rcore podfolderze
  • Wymień iteritems z elementów w import.py
  • Wymień iteritems z typami/immutabledict.py
  • Tworzenie .config w folderze rcore

  • Tworzenie .config/zagadnień JIRA przesuwanie znaczników do bitbucket.conf zawierającego

    Jíry-użytkownik = paultaylor

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

    JIRA-password = hasło

  • Run pyton export.py --jira-projekt jaudiotagger

daje

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 

trzeba uruchomić pip insdtall jako root tak zrobił

  • sudo pip install configparser

i że pracował

ale teraz

  • pyton export.py --jira.Projekt jaudiotagger

daje

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

"Instalator instalacji pip" nie działa lub się powiódł? Twój błąd mówi, że brakuje 'configparser'. Spróbuj ponownie i sprawdź, czy otrzymałeś "Wymaganie już spełnione". – Fabio

+0

@Fabio Dzięki próbie ponownego zainstalowania i wystąpił błąd, którego musiałem opuścić, w jakiś sposób, więc próbowałem ponownie z "sudo pip install configparser" i to zadziałało. Ale teraz, gdy próbuję ponownie, nie narzeka na brak modułu o nazwie jira.client –

+0

oh oczywiście muszę zrobić sudo pip install jira, ale teraz to zawiedzie na NO module o nazwie rcore.types.immutabledict –

Odpowiedz

3

można importować do BitBucket problemów, po prostu trzeba być w appropriate format. Na szczęście Reece Hart ma już written a Python script, aby połączyć się z instancją Jira i wyeksportować problemy.

Aby uruchomić skrypt, musiałem zainstalować Jira Python package, a także najnowszą wersję rcore (jeśli użyjesz pip, otrzymasz niekompatybilną poprzednią wersję, więc musisz pobrać źródło). Musiałem również zamienić wszystkie wystąpienia iteritems w items w skrypcie oraz w rcore/types/immutabledict.py, aby działało z Pythonem 3. Konieczne będzie również wypełnienie słowników (priority_map, person_map itp.) Wartościami używanymi w projekcie. Na koniec potrzebujesz pliku konfiguracyjnego, który istnieje z informacjami o połączeniu (zobacz komentarze u góry skryptu).

Podstawowe użycie linii poleceń jest export.py --jira-project <project>

Gdy masz dane eksportowane zobaczyć ten 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

dzięki Ill daj to i zgłoś się ponownie –

+0

Hi @Turch, zrobiłem wszystko, co opisałeś, ale dostaję błąd, gdy uruchomię skrypt: 'requests.exceptions.ConnectionError: ('Connection Aborted.', Gaierror (8, "nodename lub servname pod warunkiem, lub nie wiadomo")) '. Czy masz pomysł na ten temat? Dzięki! – Fabio

+1

@Fabio Wygląda na to, że nie wie, z jakim serwerem się połączyć, więc zgaduję, że problem dotyczy pliku konfiguracyjnego. Domyślnie skrypt szuka go w pliku '~/.config/jira-issues-move-to-bitbucket.conf' (patrz komentarze u góry skryptu), ale ponieważ byłem na komputerze z systemem Windows, zamieniłem go na lokalne './jira-issues-move-to-bitbucket.conf'. Chociaż zakładam, że może odczytać plik, ponieważ w przeciwnym razie prawdopodobnie dałby inny błąd ... – Turch

1

UWAGA: Piszę nową odpowiedź, bo pisanie to w komentarzu będzie być okropnym, ale większość zasługuje na odpowiedź @ Turcha.

moje kroki (w OSX i Debiana maszyn, zarówno działało w porządku):

  1. apt-get install python-pip (Debian) lub sudo easy_install pip (OSX)
  2. pip install jira
  3. pip install configparser
  4. easy_install -U setuptools (nie wiem, czy naprawdę potrzebne)
  5. Pobierz lub sklonuj kod źródłowy z https://bitbucket.org/reece/rcore/ w swoim katalogu domowym, na przykład. Uwaga: nie pobieraj za pomocą pip, otrzymasz wersję 0.0.2 i potrzebujesz 0.0.3.
  6. Pobierz Python script stworzony przez Reece, wspomniany przez @Turch, i umieść go wewnątrz folderu rcore.
  7. Postępuj zgodnie z instrukcjami @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). Uwaga: Użyłem nazwy hosta takiej jak jira.domain.com (nr http lub https).
  8. (Ta zmiana wystarczyły dla mnie) musiałem zmienić część linii 250 z 'https://{opts.jira_hostname}/' do 'http://{opts.jira_hostname}/'
  9. Aby zakończyć, należy uruchomić skrypt jak @Turch wymienić: The basic command line usage is export.py --jira-project <project>
  10. plik został umieszczony w katalogu/tmp /.zip dla mnie.
  11. Plik został dziś doskonale przyjęty przez importera BitBucket.

Szał za Reece i Turch! Dzięki chłopaki!

+0

Ive miał szansę, ale utknął, zobacz aktualizację mojego pytania –