view hgext/convert/git.py @ 15462:2b1ec74c961f stable

mdiff/patch: fix bad hunk handling for unified diffs with zero context Prior to this patch "hg diff -U0", i.e., zero lines of context, would output hunk headers with a start line one greater than what GNU patch and git output. Guido van Rossum documents the unified diff format[1] as having a start line value "one lower than one would expect" for zero length hunks. Comparing the behaviour of the three systems prior to this patch in transforming c1 c3 to c1 c2 c3 - GNU "diff -U0" reports the hunk as "@@ -1,0 +2 @@" - "git diff -U0" reports the hunk as "@@ -1,0 +2 @@" - "hg diff -U0" reports the hunk as "@@ -2,0 +2,1 @@" After this patch, "hg diff -U0" reports "@@ -1,0 +2,1 @@". Since "hg export --config diff.unified=0" outputs zero-context unified diffs, "hg import" has also been updated to account for start lines one less than expected for zero length hunk ranges. [1]: http://www.artima.com/weblogs/viewpost.jsp?thread=164293
author Nicolas Venegas <nvenegas@atlassian.com>
date Wed, 09 Nov 2011 16:55:59 -0800
parents 11aad09a6370
children 589aab2ca716 101c8a1befb1
line wrap: on
line source

# git.py - git support for the convert extension
#
#  Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

import os
from mercurial import util
from mercurial.node import hex, nullid
from mercurial.i18n import _

from common import NoRepo, commit, converter_source, checktool

class convert_git(converter_source):
    # Windows does not support GIT_DIR= construct while other systems
    # cannot remove environment variable. Just assume none have
    # both issues.
    if util.safehasattr(os, 'unsetenv'):
        def gitopen(self, s, noerr=False):
            prevgitdir = os.environ.get('GIT_DIR')
            os.environ['GIT_DIR'] = self.path
            try:
                if noerr:
                    (stdin, stdout, stderr) = util.popen3(s)
                    return stdout
                else:
                    return util.popen(s, 'rb')
            finally:
                if prevgitdir is None:
                    del os.environ['GIT_DIR']
                else:
                    os.environ['GIT_DIR'] = prevgitdir
    else:
        def gitopen(self, s, noerr=False):
            if noerr:
                (sin, so, se) = util.popen3('GIT_DIR=%s %s' % (self.path, s))
                return so
            else:
                return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')

    def gitread(self, s):
        fh = self.gitopen(s)
        data = fh.read()
        return data, fh.close()

    def __init__(self, ui, path, rev=None):
        super(convert_git, self).__init__(ui, path, rev=rev)

        if os.path.isdir(path + "/.git"):
            path += "/.git"
        if not os.path.exists(path + "/objects"):
            raise NoRepo(_("%s does not look like a Git repository") % path)

        checktool('git', 'git')

        self.path = path

    def getheads(self):
        if not self.rev:
            heads, ret = self.gitread('git rev-parse --branches --remotes')
            heads = heads.splitlines()
        else:
            heads, ret = self.gitread("git rev-parse --verify %s" % self.rev)
            heads = [heads[:-1]]
        if ret:
            raise util.Abort(_('cannot retrieve git heads'))
        return heads

    def catfile(self, rev, type):
        if rev == hex(nullid):
            raise IOError()
        data, ret = self.gitread("git cat-file %s %s" % (type, rev))
        if ret:
            raise util.Abort(_('cannot read %r object at %s') % (type, rev))
        return data

    def getfile(self, name, rev):
        data = self.catfile(rev, "blob")
        mode = self.modecache[(name, rev)]
        return data, mode

    def getchanges(self, version):
        self.modecache = {}
        fh = self.gitopen("git diff-tree -z --root -m -r %s" % version)
        changes = []
        seen = set()
        entry = None
        for l in fh.read().split('\x00'):
            if not entry:
                if not l.startswith(':'):
                    continue
                entry = l
                continue
            f = l
            if f not in seen:
                seen.add(f)
                entry = entry.split()
                h = entry[3]
                p = (entry[1] == "100755")
                s = (entry[1] == "120000")
                self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
                changes.append((f, h))
            entry = None
        if fh.close():
            raise util.Abort(_('cannot read changes in %s') % version)
        return (changes, {})

    def getcommit(self, version):
        c = self.catfile(version, "commit") # read the commit hash
        end = c.find("\n\n")
        message = c[end + 2:]
        message = self.recode(message)
        l = c[:end].splitlines()
        parents = []
        author = committer = None
        for e in l[1:]:
            n, v = e.split(" ", 1)
            if n == "author":
                p = v.split()
                tm, tz = p[-2:]
                author = " ".join(p[:-2])
                if author[0] == "<": author = author[1:-1]
                author = self.recode(author)
            if n == "committer":
                p = v.split()
                tm, tz = p[-2:]
                committer = " ".join(p[:-2])
                if committer[0] == "<": committer = committer[1:-1]
                committer = self.recode(committer)
            if n == "parent":
                parents.append(v)

        if committer and committer != author:
            message += "\ncommitter: %s\n" % committer
        tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
        tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
        date = tm + " " + str(tz)

        c = commit(parents=parents, date=date, author=author, desc=message,
                   rev=version)
        return c

    def gettags(self):
        tags = {}
        fh = self.gitopen('git ls-remote --tags "%s"' % self.path)
        prefix = 'refs/tags/'
        for line in fh:
            line = line.strip()
            if not line.endswith("^{}"):
                continue
            node, tag = line.split(None, 1)
            if not tag.startswith(prefix):
                continue
            tag = tag[len(prefix):-3]
            tags[tag] = node
        if fh.close():
            raise util.Abort(_('cannot read tags from %s') % self.path)

        return tags

    def getchangedfiles(self, version, i):
        changes = []
        if i is None:
            fh = self.gitopen("git diff-tree --root -m -r %s" % version)
            for l in fh:
                if "\t" not in l:
                    continue
                m, f = l[:-1].split("\t")
                changes.append(f)
        else:
            fh = self.gitopen('git diff-tree --name-only --root -r %s "%s^%s" --'
                             % (version, version, i + 1))
            changes = [f.rstrip('\n') for f in fh]
        if fh.close():
            raise util.Abort(_('cannot read changes in %s') % version)

        return changes

    def getbookmarks(self):
        bookmarks = {}

        # Interesting references in git are prefixed
        prefix = 'refs/heads/'
        prefixlen = len(prefix)

        # factor two commands
        gitcmd = { 'remote/': 'git ls-remote --heads origin',
                          '': 'git show-ref'}

        # Origin heads
        for reftype in gitcmd:
            try:
                fh = self.gitopen(gitcmd[reftype], noerr=True)
                for line in fh:
                    line = line.strip()
                    rev, name = line.split(None, 1)
                    if not name.startswith(prefix):
                        continue
                    name = '%s%s' % (reftype, name[prefixlen:])
                    bookmarks[name] = rev
            except:
                pass

        return bookmarks