diff --git a/hgext3rd/crdump.py b/hgext3rd/crdump.py new file mode 100644 --- /dev/null +++ b/hgext3rd/crdump.py @@ -0,0 +1,165 @@ +# crdump.py - dump changesets information to filesystem +# +from __future__ import absolute_import + +import json, os, re, shutil, tempfile +from os import path + +from mercurial import ( + error, + registrar, + scmutil, +) + +from mercurial.i18n import _ +from mercurial.node import hex + +try: + from hgsubversion import util as svnutil +except ImportError: + svnutil = None + +cmdtable = {} +command = registrar.command(cmdtable) + +@command('crdump', + [('r', 'rev', [], _("revision to dump"))], + _('hg crdump [OPTION]... [-r] [REV]')) +def crdump(ui, repo, *revs, **opts): + """ + Dump the info about the revisions in format that's friendly for sending the + patches for code review. + + The output is a JSON list with dictionary for each specified revision: :: + + { + "output_directory": an output directory for all temporary files + "commits": [ + { + "node": commit hash, + "date": date in format [unixtime, timezone offset], + "desc": commit message, + "patch_file": file containing patch in unified diff format, + "files": list of files touched by commit, + "binary_files": [ + { + "filename": filename relative to repo root, + "old_file": path to file (relative to base_tmp_dir) with a dump + of the old version of the file, + "new_file": path to file (relative to base_tmp_dir) with a dump + of the new version of the file, + }, + ... + ], + "user": commit author, + "p1": { + "node": hash, + "differential_revision": xxxx + }, + "public_base": { + "node": public base commit hash, + "svnrev": svn revision of public base (if hgsvn repo), + } + }, + ... + ] + } + """ + + revs = list(revs) + revs.extend(opts['rev']) + + if not revs: + raise error.Abort(_('revisions must be specified')) + revs = scmutil.revrange(repo, revs) + + commits = [] + outdir = tempfile.mkdtemp(suffix='hg.crdump') + try: + for rev in revs: + ctx = repo[rev] + rdata = { + 'node': hex(ctx.node()), + 'date': map(int, ctx.date()), + 'desc': ctx.description(), + 'files': ctx.files(), + 'p1': { + 'node': ctx.parents()[0].hex(), + }, + 'user': ctx.user(), + } + if ctx.parents()[0].phasestr() != "public": + # we need this only if parent is in the same draft stack + rdata['p1']['differential_revision'] = \ + phabricatorrevision(ctx.parents()[0]) + + pbctx = publicbase(repo, ctx) + if pbctx: + rdata['public_base'] = { + 'node': hex(pbctx.node()), + } + if svnutil: + svnrev = svnutil.getsvnrev(pbctx) + rdata['public_base']['svnrev'] = \ + svnrev.split('@')[1] if svnrev else None + rdata['patch_file'] = dumppatch(ui, repo, ctx, outdir) + rdata['binary_files'] = dumpbinaryfiles(ui, repo, ctx, outdir) + commits.append(rdata) + + ui.write(json.dumps({ + 'output_directory': outdir, + 'commits': commits, + }, sort_keys=True, indent=4, separators=(',', ': '))) + ui.write('\n') + except Exception as e: + shutil.rmtree(outdir) + raise e + +def dumppatch(ui, repo, ctx, outdir): + chunks = ctx.diff() + patchfile = '%s.patch' % hex(ctx.node()) + with open(path.join(outdir, patchfile), 'w') as f: + for chunk in chunks: + f.write(chunk) + return patchfile + +def dumpfctx(outdir, fctx): + outfile = '%s_%s' % (fctx.path(), hex(fctx.filenode())) + writepath = path.join(outdir, outfile) + if not path.isdir(path.dirname(writepath)): + os.makedirs(path.dirname(writepath)) + if not path.isfile(writepath): + with open(writepath, 'w') as f: + f.write(fctx.data()) + return outfile + +def dumpbinaryfiles(ui, repo, ctx, outdir): + binary_files = [] + for fname in ctx.files(): + fctx = ctx[fname] + pctx = ctx.parents()[0] + if fctx.isbinary(): + newfile = dumpfctx(outdir, fctx) + oldfile = None + if fname in pctx: + pfctx = pctx[fname] + if pfctx.isbinary(): + oldfile = dumpfctx(outdir, pfctx) + binary_files.append({ + 'file_name': fname, + 'old_file': oldfile, + 'new_file': newfile, + }) + + return binary_files + +def phabricatorrevision(ctx): + match = re.search('Differential Revision:.*facebook\.com.*/D(\d+)', + ctx.description()) + return match.group(1) if match else '' + +def publicbase(repo, ctx): + base = repo.revs('last(::%d - not public())', ctx.rev()) + if len(base): + return repo[base.first()] + return None diff --git a/tests/test-crdump.t b/tests/test-crdump.t new file mode 100644 --- /dev/null +++ b/tests/test-crdump.t @@ -0,0 +1,177 @@ + $ cat >> $HGRCPATH << EOF + > [extensions] + > drawdag=$RUNTESTDIR/drawdag.py + > crdump=$TESTDIR/../hgext3rd/crdump.py + > EOF + +Create repo + $ mkdir repo + $ cd repo + $ hg init + $ echo A > a + $ echo -e "A\0" > bin1 + $ hg addremove + adding a + adding bin1 + $ hg commit -m a + $ hg phase -p . + + $ echo A > a + $ echo -e "a\0b" > bin1 + $ echo -e "b\0" > bin2 + $ hg addremove + adding bin2 + $ hg commit -m "b + > Differential Revision: phabricator.facebook.com/D123" + + $ echo C > c + $ hg addremove + adding c + $ hg commit -m c + +Test basic dump of two commits + + $ hg crdump -r ".^^::." | tee ../json_output + { + "commits": [ + { + "binary_files": [ + { + "file_name": "bin1", + "new_file": "bin1_23c26c825bddcb198e701c6f7043a4e35dcb8b97", + "old_file": null + } + ], + "date": [ + 0, + 0 + ], + "desc": "a", + "files": [ + "a", + "bin1" + ], + "node": "65d913976cc18347138f7b9f5186010d39b39b0f", + "p1": { + "node": "0000000000000000000000000000000000000000" + }, + "patch_file": "65d913976cc18347138f7b9f5186010d39b39b0f.patch", + "public_base": { + "node": "65d913976cc18347138f7b9f5186010d39b39b0f", + "svnrev": null + }, + "user": "test" + }, + { + "binary_files": [ + { + "file_name": "bin1", + "new_file": "bin1_5f54dc7f5b744f0bf88fcfe31eaba3cabc7a5f0c", + "old_file": "bin1_23c26c825bddcb198e701c6f7043a4e35dcb8b97" + }, + { + "file_name": "bin2", + "new_file": "bin2_31f7b4d23cf93fd41972d0a879086e900cbf06c9", + "old_file": null + } + ], + "date": [ + 0, + 0 + ], + "desc": "b\nDifferential Revision: phabricator.facebook.com/D123", + "files": [ + "bin1", + "bin2" + ], + "node": "bfcf9917c5dbf2b3b24f0bb4bf5b73611c5fe573", + "p1": { + "node": "65d913976cc18347138f7b9f5186010d39b39b0f" + }, + "patch_file": "bfcf9917c5dbf2b3b24f0bb4bf5b73611c5fe573.patch", + "public_base": { + "node": "65d913976cc18347138f7b9f5186010d39b39b0f", + "svnrev": null + }, + "user": "test" + }, + { + "binary_files": [], + "date": [ + 0, + 0 + ], + "desc": "c", + "files": [ + "c" + ], + "node": "bfaadbd049a38e851652d584627afd83a7298969", + "p1": { + "differential_revision": "123", + "node": "bfcf9917c5dbf2b3b24f0bb4bf5b73611c5fe573" + }, + "patch_file": "bfaadbd049a38e851652d584627afd83a7298969.patch", + "public_base": { + "node": "65d913976cc18347138f7b9f5186010d39b39b0f", + "svnrev": null + }, + "user": "test" + } + ], + "output_directory": "*" (glob) + } + + >>> import json + >>> from os import path + >>> with open("../json_output") as f: + ... data = json.loads(f.read()) + >>> outdir = data['output_directory'] + >>> for commit in data['commits']: + ... print "#### commit %s" % commit['node'] + ... print open(path.join(outdir, commit['patch_file'])).read() + ... for binfile in commit['binary_files']: + ... print "######## file %s" % binfile['file_name'] + ... if binfile['old_file'] is not None: + ... print "######## old" + ... print open(path.join(outdir, binfile['old_file'])).read().encode('hex') + ... if binfile['new_file'] is not None: + ... print "######## new" + ... print open(path.join(outdir, binfile['new_file'])).read().encode('hex') + #### commit 65d913976cc18347138f7b9f5186010d39b39b0f + diff -r 000000000000 -r 65d913976cc1 a + --- /dev/null Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -0,0 +1,1 @@ + +A + diff -r 000000000000 -r 65d913976cc1 bin1 + Binary file bin1 has changed + + ######## file bin1 + ######## new + 41000a + #### commit bfcf9917c5dbf2b3b24f0bb4bf5b73611c5fe573 + diff -r 65d913976cc1 -r bfcf9917c5db bin1 + Binary file bin1 has changed + diff -r 65d913976cc1 -r bfcf9917c5db bin2 + Binary file bin2 has changed + + ######## file bin1 + ######## old + 41000a + ######## new + 6100620a + ######## file bin2 + ######## new + 62000a + #### commit bfaadbd049a38e851652d584627afd83a7298969 + diff -r bfcf9917c5db -r bfaadbd049a3 c + --- /dev/null Thu Jan 01 00:00:00 1970 +0000 + +++ b/c Thu Jan 01 00:00:00 1970 +0000 + @@ -0,0 +1,1 @@ + +C + + + + >>> import shutil + >>> shutil.rmtree(outdir) + NameError("name 'outdir' is not defined",)