import - 如何将 Jira 问题导出到 BitBucket

标签 import export jira bitbucket

我刚刚将我的项目代码从 java.net 移到了 BitBucket。但是我的 jira 问题跟踪仍然托管在 java.net 上,尽管 BitBucket 确实有一些选项可以链接到外部问题跟踪器,但我认为我不能将它用于 java.net,尤其是因为我没有管理员权限需要安装 DVCS 连接器。

所以我认为另一种选择是导出然后将问题导入 BitBucket 问题跟踪器,这可能吗?

到目前为止的进展
所以我尝试按照下面使用 OSX 的两个信息丰富的答案中的步骤进行操作,但我遇到了一个问题 - 我对脚本实际调用的内容感到困惑,因为在答案中它谈到了 export.py 但不存在具有该名称的脚本所以我重命名了我下载的那个。

  • 须藤easy_install pip (OSX)
  • pip 安装 jira
  • pip 安装配置解析器
  • easy_install -U 安装工具
  • 转至 https://bitbucket.org/reece/rcore ,选择下载选项卡,下载 zip 并解压缩,然后重命名为 reece(由于某种原因 git clone https://bitbucket.org/reece/rcore 失败并出现错误)
  • cd reece/rcore
  • 在 rcore 子文件夹中将脚本另存为 export.py
  • 用 import.py 中的项目替换 iteritem
  • 将 iteritems 替换为 types/immutabledict.py
  • 在 rcore 文件夹中创建 .config
  • 创建 .config/jira-issues-move-to-bitbucket.conf 包含

    jira-用户名=paultaylor

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

    jira-password=密码
  • 运行 python export.py --jira-project 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
    

    我需要以 root 身份运行 pip insdtall
  • 须藤 pip 安装 configparser

  • 这有效

    但现在
  • python export.py --jira.project jaudiotagger


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

    最佳答案

    您可以将问题导入 BitBucket,它们只需要在 appropriate format 中.幸运的是,Reece Hart 已经written a Python script连接到 Jira 实例并导出问题。

    为了让脚本运行,我必须安装 Jira Python package以及最新版本的rcore (如果你使用 pip,你会得到一个不兼容的先前版本,所以你必须获取源代码)。我还必须替换 iteritems 的所有实例与 items在脚本和 rcore/types/immutabledict.py 中使其适用于 Python 3。您还需要使用您的项目使用的值填写字典( priority_mapperson_map 等)。最后,您需要一个包含连接信息的配置文件(请参阅脚本顶部的注释)。

    基本的命令行用法是 export.py --jira-project <project>
    导出数据后,请参阅 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 <reecehart@gmail.com>
    
    
    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))
    

    关于import - 如何将 Jira 问题导出到 BitBucket,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25845589/

    相关文章:

    r - 将 ASCII 文件导入 R

    来自第二个表的 MySQL CSV 导出 ID

    java - 简单 jar 文件上出现 NoClassDefFoundError

    jira - 有没有一种简单、免费的方法可以在 Jira 中创建测试用例管理

    java - JIRA - 从帖子功能更新摘要

    python - 导入错误: cannot import name Weather even though i had installed the weather-api

    linux - 在 Linux/Unix 中将密码从文件导入 CLI 命令参数

    java - 包不存在错误

    vba - 如何将具有给定名称的附件(图像)导出到文件夹?

    java - 在 groovy 中使用 HTML 格式的 JIRA Rest API 创建发行说明