diff --git a/hgext3rd/crdump.py b/hgext3rd/crdump.py new file mode 100644 --- /dev/null +++ b/hgext3rd/crdump.py @@ -0,0 +1,187 @@ +# crdump.py - dump changesets information to filesystem +# +from __future__ import absolute_import + +import json, re, shutil, tempfile +from os import path + +from mercurial import ( + error, + extensions, + phases, + registrar, + scmutil, +) + +from mercurial.i18n import _ +from mercurial.node import hex + +DIFFERENTIAL_REGEX = re.compile( + 'Differential Revision: http.+?/' # Line start, URL + 'D(?P[0-9]+)', # Differential ID, just numeric part + flags = re.LOCALE +) +cmdtable = {} +command = registrar.command(cmdtable) + +@command('debugcrdump', [ + ('r', 'rev', [], _("revisions to dump")), + # We use 1<<15 for "as much context as possible" + ('U', 'unified', + 1 << 15, _('number of lines of context to show'), _('NUM')), + ], + _('hg debugcrdump [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": path to file containing patch in unified diff format + relative to output_directory, + "files": list of files touched by commit, + "binary_files": [ + { + "filename": path to file relative to repo root, + "old_file": path to file (relative to output_directory) with + a dump of the old version of the file, + "new_file": path to file (relative to output_directory) with + a dump of the newversion 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) + + if 'unified' in opts: + contextlines = opts['unified'] + + cdata = [] + 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].phase() != phases.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()), + } + try: + hgsubversion = extensions.find('hgsubversion') + svnrev = hgsubversion.util.getsvnrev(pbctx) + # There are no abstractions in hgsubversion for doing + # it see hgsubversion/svncommands.py:267 + rdata['public_base']['svnrev'] = \ + svnrev.split('@')[1] if svnrev else None + except KeyError: + pass + rdata['patch_file'] = dumppatch(ui, repo, ctx, outdir, contextlines) + rdata['binary_files'] = dumpbinaryfiles(ui, repo, ctx, outdir) + cdata.append(rdata) + + ui.write(json.dumps({ + 'output_directory': outdir, + 'commits': cdata, + }, sort_keys=True, indent=4, separators=(',', ': '))) + ui.write('\n') + except Exception as e: + shutil.rmtree(outdir) + raise e + +def dumppatch(ui, repo, ctx, outdir, contextlines): + chunks = ctx.diff(git=True, unified=contextlines, binary=False) + 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' % hex(fctx.filenode()) + writepath = path.join(outdir, outfile) + if not path.isfile(writepath): + with open(writepath, 'w') as f: + f.write(fctx.data()) + return outfile + +def dumpbinaryfiles(ui, repo, ctx, outdir): + binaryfiles = [] + pctx = ctx.parents()[0] + for fname in ctx.files(): + oldfile = newfile = None + dump = False + + fctx = ctx[fname] if fname in ctx else None + pfctx = pctx[fname] if fname in pctx else None + + # if one of the versions is binary file the whole change will show + # up as binary in diff output so we need to dump both versions + if fctx and fctx.isbinary(): + dump = True + if pfctx and pfctx.isbinary(): + dump = True + + if dump: + if fctx: + newfile = dumpfctx(outdir, fctx) + if pfctx: + oldfile = dumpfctx(outdir, pfctx) + binaryfiles.append({ + 'file_name': fname, + 'old_file': oldfile, + 'new_file': newfile, + }) + + return binaryfiles + +def phabricatorrevision(ctx): + match = DIFFERENTIAL_REGEX.search(ctx.description()) + return match.group(1) if match else '' + +def publicbase(repo, ctx): + base = repo.revs('last(::%d & 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,226 @@ + $ 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 . + + $ printf "A\nB\nC\nD\nE\nF\n" > a + $ echo -e "a\0b" > bin1 + $ echo -e "b\0" > bin2 + $ hg addremove + adding bin2 + $ hg commit -m "b + > Differential Revision: https://phabricator.facebook.com/D123" + + $ echo G >> a + $ echo C > c + $ rm bin2 + $ echo x > bin1 + $ hg addremove + removing bin2 + adding c + $ hg commit -m c + +Test basic dump of two commits + + $ hg debugcrdump -U 1 -r ".^^::." --traceback| tee ../json_output + { + "commits": [ + { + "binary_files": [ + { + "file_name": "bin1", + "new_file": "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" + }, + "user": "test" + }, + { + "binary_files": [ + { + "file_name": "bin1", + "new_file": "5f54dc7f5b744f0bf88fcfe31eaba3cabc7a5f0c", + "old_file": "23c26c825bddcb198e701c6f7043a4e35dcb8b97" + }, + { + "file_name": "bin2", + "new_file": "31f7b4d23cf93fd41972d0a879086e900cbf06c9", + "old_file": null + } + ], + "date": [ + 0, + 0 + ], + "desc": "b\nDifferential Revision: https://phabricator.facebook.com/D123", + "files": [ + "a", + "bin1", + "bin2" + ], + "node": "6370cd64643d547e11c6bc91920bca7b44ea21b5", + "p1": { + "node": "65d913976cc18347138f7b9f5186010d39b39b0f" + }, + "patch_file": "6370cd64643d547e11c6bc91920bca7b44ea21b5.patch", + "public_base": { + "node": "65d913976cc18347138f7b9f5186010d39b39b0f" + }, + "user": "test" + }, + { + "binary_files": [ + { + "file_name": "bin1", + "new_file": "4281f31b8cfa1376dc036a729c4118cd192db663", + "old_file": "5f54dc7f5b744f0bf88fcfe31eaba3cabc7a5f0c" + }, + { + "file_name": "bin2", + "new_file": null, + "old_file": "31f7b4d23cf93fd41972d0a879086e900cbf06c9" + } + ], + "date": [ + 0, + 0 + ], + "desc": "c", + "files": [ + "a", + "bin1", + "bin2", + "c" + ], + "node": "c2c1919228a86d876dbb46befd0e0433c62a9f5f", + "p1": { + "differential_revision": "123", + "node": "6370cd64643d547e11c6bc91920bca7b44ea21b5" + }, + "patch_file": "c2c1919228a86d876dbb46befd0e0433c62a9f5f.patch", + "public_base": { + "node": "65d913976cc18347138f7b9f5186010d39b39b0f" + }, + "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') + ... import shutil + ... shutil.rmtree(outdir) + #### commit 65d913976cc18347138f7b9f5186010d39b39b0f + diff --git a/a b/a + new file mode 100644 + --- /dev/null + +++ b/a + @@ -0,0 +1,1 @@ + +A + diff --git a/bin1 b/bin1 + new file mode 100644 + Binary file bin1 has changed + + ######## file bin1 + ######## new + 41000a + #### commit 6370cd64643d547e11c6bc91920bca7b44ea21b5 + diff --git a/a b/a + --- a/a + +++ b/a + @@ -1,1 +1,6 @@ + A + +B + +C + +D + +E + +F + diff --git a/bin1 b/bin1 + Binary file bin1 has changed + diff --git a/bin2 b/bin2 + new file mode 100644 + Binary file bin2 has changed + + ######## file bin1 + ######## old + 41000a + ######## new + 6100620a + ######## file bin2 + ######## new + 62000a + #### commit c2c1919228a86d876dbb46befd0e0433c62a9f5f + diff --git a/a b/a + --- a/a + +++ b/a + @@ -6,1 +6,2 @@ + F + +G + diff --git a/bin1 b/bin1 + Binary file bin1 has changed + diff --git a/bin2 b/bin2 + deleted file mode 100644 + Binary file bin2 has changed + diff --git a/c b/c + new file mode 100644 + --- /dev/null + +++ b/c + @@ -0,0 +1,1 @@ + +C + + ######## file bin1 + ######## old + 6100620a + ######## new + 780a + ######## file bin2 + ######## old + 62000a + + +