cGit-UI for Git Repositories

cGit-UI – is a web interface for Git Repositories. cGit CGI script is writen in C and therefore it's fast enough

12 Commits   0 Branches   1 Tag

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdlib.h>
#include <stdio.h>
#include <sys/sysinfo.h>
#include <sys/types.h>
#include <stdint.h>
#include <dirent.h>
#include <sys/stat.h> /* chmod(2)    */
#include <sys/file.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <limits.h>
#include <string.h>   /* strdup(3)   */
#include <libgen.h>   /* basename(3) */
#include <ctype.h>    /* tolower(3)  */
#include <errno.h>
#include <time.h>
#include <sys/time.h>
#include <pwd.h>
#include <grp.h>
#include <stdarg.h>
#include <unistd.h>
#include <magic.h>

#include <git2.h>

#include <nls.h>

#include <defs.h>
#include <fatal.h>
#include <http.h>
#include <html.h>
#include <strbuf.h>
#include <wrapper.h>
#include <system.h>

#include <ctx.h>
#include <git-shared.h>


#define CGIT_ERRMSG_SIZE 4096

void cgit_error( const char *fmt, ... )
{
  va_list arg_ptr;
  char  buf[CGIT_ERRMSG_SIZE];
  char  msg[CGIT_ERRMSG_SIZE];
  char *format = "%s: %s\n";

  va_start( arg_ptr, fmt );

  vsnprintf( msg, CGIT_ERRMSG_SIZE, (const void *)fmt, arg_ptr );

  va_end( arg_ptr ); /* Reset variable arguments. */

  snprintf( buf, CGIT_ERRMSG_SIZE, format, "cgit", msg );

  (void)write( STDERR_FILENO, buf, strlen( buf ) );

  exit( 1 );
}

cgit_errfunc cgit_fatal = cgit_error;



int is_bare( const char *path )
{
  char *p = NULL;
  int   ret = -1;

  if( !path || !*path ) return ret;

  p = rindex( path, '.' );
  if( !p )
  {
    ret = 0;
  }
  else
  {
    if( !strncmp( p, ".git", 4 ) )
      ret = 1;
    else
      ret = 0;
  }

  return ret;
}

git_repository *open_repository( const char *path )
{
  git_repository *repo = NULL;
  int bare = 0;

  if( (bare = is_bare( path )) < 0 ) return repo;

  if( bare )
  {
    if( git_repository_open_bare( &repo, path ) < 0 ) return repo;
  }
  else
  {
    if( git_repository_open( &repo, path ) < 0 ) return repo;
  }

  return repo;
}

void close_repository( git_repository *repo )
{
  if( !repo ) return;
  git_repository_free( repo );
}

/*
  Get Last Commit:
  (Only commit-ish refs are permitted)
 */
git_commit *get_commit_by_ref( git_repository *repo, const char *ref )
{
  int error = 0;
  git_object *obj = NULL;
  git_commit *commit = NULL;

  char reference[PATH_MAX] = { 0 };

  if( !repo ) return commit;
  if( !ref ) ref = "HEAD^{commit}";

  sprintf( (char *)&reference[0], "%s^{commit}", ref );

  error = git_revparse_single( &obj, repo, (const char *)&reference[0] );
  if( error < 0 )
  {
    git_error_clear();
    return commit;
  }

  if( git_object_type(obj) != GIT_OBJECT_COMMIT )
  {
    git_object_free( obj );
    return commit;
  }

  error = git_commit_lookup( &commit, repo, git_object_id( obj ) );
  if( error < 0 )
  {
    git_error_clear();
    git_object_free( obj );
    return NULL;
  }

  git_object_free( obj );

  return commit;
}

git_commit *get_commit_by_hex( git_repository *repo, const char *hex )
{
  git_oid     cid;
  git_commit *commit = NULL;

  if( !repo || !hex ) return commit;
  if( git_oid_fromstr( &cid, hex ) < 0 ) return commit;

  if( git_commit_lookup( &commit, repo, &cid ) < 0 ) return NULL;
  return commit;
}


void fill_short_commit_info( struct short_commit_info *info, const char *path )
{
  git_repository *repo = NULL;
  git_commit     *commit = NULL;

  if( !info || !path ) return;

  if( !(repo = open_repository( path )) ) return;

  if( !(commit = get_commit_by_ref( repo, NULL )) )
  {
    close_repository( repo );
    return;
  }

  git_oid_tostr( info->rev, GIT_OID_HEXSZ+1, git_commit_id( commit ) );
  info->date   = git_commit_time( commit );
  info->offset = git_commit_time_offset( commit );

  info->offset = (info->offset % 60) + ((info->offset / 60) * 100);

  git_commit_free( commit );

  close_repository( repo );
}

struct found {
  git_object_t    kind;
  git_filemode_t  mode;
  const git_oid  *oid;
  git_object     *obj;
};

static void find_entry( struct found *ret, git_repository *repo, git_tree *tree, char *path )
{
  git_tree     *ntree = NULL;
  int           i, cnt;
  char         *s, *p = NULL, *n;

  if( !ret || !repo || !tree ) return;

  if( path && *path )
  {
    s = path;

    while( *s &&  *s == '/' )
      ++s;
    n = p = s;

    while( *p && *p != '/' )
      ++p;
    if( *p )
    {
      *p = '\0'; s = ++p;
    }
    else
      s = p;

    git_tree_dup( &ntree, tree );
    cnt = git_tree_entrycount( ntree );

    for( i = 0; i < cnt; ++i )
    {
      const git_tree_entry *entry;
      entry = git_tree_entry_byindex( ntree, i );

      if( !strcmp( git_tree_entry_name( entry ), n ) )
      {
        ret->kind = git_tree_entry_type( entry );
        ret->mode = git_tree_entry_filemode( entry );
        git_tree_entry_to_object( &ret->obj, repo, entry );
        ret->oid = git_object_id( (const git_object *)ret->obj );

        if( ret->kind == GIT_OBJECT_TREE && s && *s )
        {
          git_object *obj;
          git_tree_entry_to_object( &obj, repo, entry );
          find_entry( ret, repo, (git_tree *)obj, s );
          git_object_free( obj );
          break;
        }
        else
        {
          break;
        }
      }

    }
    git_tree_free( ntree );
  }

  return;
}


void fill_commit_info( struct cgit_info *info, const char *path, const char *rpath )
{
  git_repository *repo = NULL;
  git_oid         cid;
  git_commit     *commit = NULL;
  git_tree       *tree = NULL;
  const git_oid  *oid;
  const git_signature *sign = NULL;

  char *author;
  int   len;

  if( !info || !path || !info->revision[0] ) return;
  if( git_oid_fromstr( &cid, (const char *)&info->revision[0] ) < 0 ) return;
  if( !(repo = open_repository( path )) ) return;

  git_commit_lookup( &commit, repo, &cid );
  git_commit_tree( &tree, commit );

  info->kind = git_object_type( (const git_object *)tree );
  oid = git_tree_id( (const git_tree *)tree );
  git_oid_fmt( (char *)&info->oid[0], oid );

  if( rpath && *rpath )
  {
    struct found  ret = { .kind = GIT_OBJECT_TREE, .oid = NULL, .obj = NULL };
    char relative_path[PATH_MAX] = { 0 };
    sprintf( (char *)&relative_path[0], rpath );
    find_entry( &ret, repo, tree, (char *)&relative_path[0] );
    info->kind = ret.kind;
    info->mode = ret.mode;
    git_oid_fmt( (char *)&info->oid[0], ret.oid );
    git_object_free( ret.obj );
  }

  git_tree_free( tree );

  info->date = git_commit_time( commit );
  info->offset = git_commit_time_offset( commit );

  info->offset = (info->offset % 60) + ((info->offset / 60) * 100);

  sign = git_commit_author( commit );
  len = (int)strlen( sign->name ) + 1;
  author = (char *)__sbrk( len );
  strcpy( author, sign->name );
  info->author = author;

  git_commit_free( commit );
  close_repository( repo );
}


static const char *mime_info( struct cgit_info *info, const char *buffer, size_t length )
{
  const char *mime = NULL;
  magic_t     magic;

  if( !info || !buffer || !length ) return mime;

  magic = magic_open( MAGIC_MIME );
  if( !magic )
  {
    html_fatal( "unable to initialize magic library" );
    return mime;
  }
  if( magic_load( magic, NULL ) != 0 )
  {
    html_fatal( "cannot load magic database - %s\n", magic_error( magic ) );
    magic_close( magic );
    return mime;
  }

  mime = magic_buffer( magic, buffer, length );
  if( mime )
  {
    int len = (int)strlen( mime ) + 1;
    char *mime_type = (char *)__sbrk( len );
    memcpy( (void *)mime_type, (const void *)mime, len );
    info->mime = (const char *)mime_type;
  }

  magic_close( magic );
  return mime;
}

void fill_mime_info( struct cgit_info *info, const char *path, const char *rpath )
{
  git_repository *repo = NULL;
  git_oid         cid;
  git_commit     *commit = NULL;
  git_tree       *tree = NULL;
  git_object     *obj = NULL;
  git_object_t    kind;
  git_blob       *blob = NULL;
  int             blob_allocated = 0;
  git_off_t       rawsize = 0;
  const char     *raw = NULL;
  char            mime_buf[1024] = { 0 };
  size_t          len = 1024;

  if( !info || !path || !info->revision[0] || info->kind != GIT_OBJECT_BLOB ) return;
  if( git_oid_fromstr( &cid, (const char *)&info->revision[0] ) < 0 ) return;
  if( !(repo = open_repository( path )) ) return;

  git_commit_lookup( &commit, repo, &cid );
  git_commit_tree( &tree, commit );

  blob = (git_blob *)tree;
  kind = git_object_type( (const git_object *)tree );
  if( rpath && *rpath )
  {
    struct found  ret = { .kind = GIT_OBJECT_TREE, .oid = NULL, .obj = NULL };
    char relative_path[PATH_MAX] = { 0 };
    sprintf( (char *)&relative_path[0], rpath );
    find_entry( &ret, repo, tree, (char *)&relative_path[0] );
    kind = ret.kind;
    if( kind == GIT_OBJECT_BLOB )
    {
      git_object_dup( &obj, ret.obj );
      blob_allocated = 1;
      blob = (git_blob *)obj;
    }
    git_object_free( ret.obj ); /* We have to free allocated object returned by find_entry(). */
  }

  if( kind != GIT_OBJECT_BLOB )
  {
    if( blob_allocated )
      git_object_free( (git_object *)blob );
    git_tree_free( tree );
    git_commit_free( commit );
    close_repository( repo );
    return;
  }

  rawsize = git_blob_rawsize( blob );
  raw = (const char *)git_blob_rawcontent( blob );
  if( rawsize > 1024 )
  {
    memcpy( (void *)&mime_buf[0], (const void *)raw, len );
  }
  else
  {
    memcpy( (void *)&mime_buf[0], (const void *)raw, (size_t)rawsize );
    len = (size_t)rawsize;
  }

  if( blob_allocated )
    git_object_free( (git_object *)blob );
  git_tree_free( tree );

  mime_info( info, (const char *)&mime_buf[0], len );

  git_commit_free( commit );
  close_repository( repo );
}


size_t branches_number( const char *path, const char *skip )
{
  git_repository *repo = NULL;
  git_reference_iterator *iter = NULL;
  const char *name = NULL;
  size_t ret = 0;

  if( !path ) return ret;

  if( !(repo = open_repository( path )) ) return ret;

  if( git_reference_iterator_glob_new( &iter, repo, "refs/heads/*" ) < 0 )
  {
    close_repository( repo );
    return ret;
  }

  while( !git_reference_next_name( &name, iter ) )
  {
    if( skip && *skip )
    {
      if( strcmp( (const char *)&name[11], skip ) )
      {
        ++ret;
      }
    }
    else
      ++ret;
  }

  git_reference_iterator_free( iter );
  close_repository( repo );

  return ret;
}

size_t tags_number( const char *path )
{
  git_repository *repo = NULL;
  git_strarray    tags;
  size_t ret = 0;

  if( !path ) return ret;

  if( !(repo = open_repository( path )) ) return ret;

  if( git_tag_list( &tags, repo ) < 0 )
  {
    close_repository( repo );
    return ret;
  }

  ret = tags.count;
  git_strarray_free( &tags );

  close_repository( repo );

  return ret;
}

/**********************************
  List of references functions:
 */
struct cgit_ref_names *cgit_ref_names_new( void )
{
  struct cgit_ref_names *names = NULL;

  names = (struct cgit_ref_names *)xmalloc( sizeof(struct cgit_ref_names) );
  return names;
}

void cgit_ref_names_allocate( struct cgit_ref_names **ref_names )
{
  struct cgit_ref_names *names = NULL;

  if( !ref_names ) return;
  names = (struct cgit_ref_names *)xmalloc( sizeof(struct cgit_ref_names) );
  *ref_names = names;
}

void cgit_ref_names_add( struct cgit_ref_names *names, const char *name )
{
  if( !names ) return;

  if( !names->name )
  {
    names->name = (char **)xmalloc( sizeof(char *) );
    names->name[0] = strdup( name );
    names->len = (size_t)1;
  }
  else
  {
    names->name = (char **)xrealloc( names->name, (names->len + 1) * sizeof(char *) );
    names->name[names->len] = strdup( name );
    ++names->len;
  }
}

void cgit_ref_names_free( struct cgit_ref_names *names )
{
  if( !names ) return;

  if( names->len && names->name )
  {
    size_t i;
    for( i = 0; i < names->len; ++i )
    {
      if( names->name[i] )
        free( names->name[i] );
    }
    free( names->name );
  }
  free( names );
}
/*
  End of List of references functions.
 **************************************/

void lookup_branches_by_prefix( struct cgit_ref_names **ref_names, const char *prefix )
{
  const char *name = NULL, *git_root = NULL, *repo_root = NULL;
  struct cgit_ref_names *names = NULL;

  if( !ref_names || !prefix || !*prefix ) return;

  name      = ctx.repo.name;
  git_root  = ctx.repo.git_root;
  repo_root = ctx.repo.repo_root;

  if( name && git_root )
  {
    git_repository *repo = NULL;
    char path[PATH_MAX] = { 0 };

    sprintf( (char *)&path[0], "%s/", git_root );
    if( repo_root && *repo_root )
    {
      strcat( (char *)&path[0], repo_root );
      strcat( (char *)&path[0], "/" );
    }
    strcat( (char *)&path[0], name );

    if( !is_bare( (char *)&path[0] ) )
    {
      strcat( (char *)&path[0], "/.git" );
    }

    if( !(repo = open_repository( (const char *)&path[0] )) ) return;

    {
      git_reference_iterator *iter = NULL;
      const char *name = NULL;
      const char *refs = "refs/heads/*";

      if( git_reference_iterator_glob_new( &iter, repo, refs ) < 0 )
      {
        close_repository( repo );
        return;
      }

      cgit_ref_names_allocate( &names );
      *ref_names = names;

      while( !git_reference_next_name( &name, iter ) )
      {
        size_t len = strlen( prefix );

        if( !strncmp( prefix, (char *)&name[11], len ) )
          cgit_ref_names_add( names, (const char *)&name[11] );
      }
      git_reference_iterator_free( iter );
    }

    close_repository( repo );
  }

  return;
}

void lookup_tags_by_prefix( struct cgit_ref_names **ref_names, const char *prefix )
{
  const char *name = NULL, *git_root = NULL, *repo_root = NULL;
  struct cgit_ref_names *names = NULL;

  if( !ref_names || !prefix || !*prefix ) return;

  name      = ctx.repo.name;
  git_root  = ctx.repo.git_root;
  repo_root = ctx.repo.repo_root;

  if( name && git_root )
  {
    git_repository *repo = NULL;
    char path[PATH_MAX] = { 0 };

    sprintf( (char *)&path[0], "%s/", git_root );
    if( repo_root && *repo_root )
    {
      strcat( (char *)&path[0], repo_root );
      strcat( (char *)&path[0], "/" );
    }
    strcat( (char *)&path[0], name );

    if( !is_bare( (char *)&path[0] ) )
    {
      strcat( (char *)&path[0], "/.git" );
    }

    if( !(repo = open_repository( (const char *)&path[0] )) ) return;

    {
      git_reference_iterator *iter = NULL;
      const char *name = NULL;
      const char *refs = "refs/tags/*";

      if( git_reference_iterator_glob_new( &iter, repo, refs ) < 0 )
      {
        close_repository( repo );
        return;
      }

      cgit_ref_names_allocate( &names );
      *ref_names = names;

      while( !git_reference_next_name( &name, iter ) )
      {
        size_t len = strlen( prefix );

        if( !strncmp( prefix, (char *)&name[10], len ) )
          cgit_ref_names_add( names, (const char *)&name[10] );
      }
      git_reference_iterator_free( iter );
    }

    close_repository( repo );
  }

  return;
}


/***********************************
  List of commits functions:
 */
struct cgit_hex_commits *cgit_hex_commits_new( void )
{
  struct cgit_hex_commits *commits = NULL;

  commits = (struct cgit_hex_commits *)xmalloc( sizeof(struct cgit_hex_commits) );
  return commits;
}

void cgit_hex_commits_allocate( struct cgit_hex_commits **hex_commits )
{
  struct cgit_hex_commits *commits = NULL;

  if( !hex_commits ) return;
  commits = (struct cgit_hex_commits *)xmalloc( sizeof(struct cgit_hex_commits) );
  *hex_commits = commits;
}

void cgit_hex_commits_add( struct cgit_hex_commits *commits, const char *hex )
{
  if( !commits ) return;

  if( !commits->hex )
  {
    commits->hex = (char **)xmalloc( sizeof(char *) );
    commits->hex[0] = strdup( hex );
    commits->len = (size_t)1;
  }
  else
  {
    commits->hex = (char **)xrealloc( commits->hex, (commits->len + 1) * sizeof(char *) );
    commits->hex[commits->len] = strdup( hex );
    ++commits->len;
  }
}

void cgit_hex_commits_free( struct cgit_hex_commits *commits )
{
  if( !commits ) return;

  if( commits->len && commits->hex )
  {
    size_t i;
    for( i = 0; i < commits->len; ++i )
    {
      if( commits->hex[i] )
        free( commits->hex[i] );
    }
    free( commits->hex );
  }
  free( commits );
}
/*
  End of List of commits functions.
 ***********************************/

void parse_relative_path( char *ref, char *rpath, const char *relative_path )
{
  char *s, *p = NULL, *n, *path, *path_info;
  int   len = 0;

  *ref = '\0', *rpath = '\0';

  if( relative_path && *relative_path )
  {
    path_info = xstrdup( relative_path );
    s = path_info;

    while( *s )
    {
      while( *s &&  *s == '/' )
        ++s;
      n = p = s;

      while( *p && *p != '/' )
        ++p;
      if( *p )
      {
        *p = '\0'; s = ++p;
      }
      else
        s = p;

      if( !strcmp( n, "tags" ) )
      {
        sprintf( ref, "refs/%s/", n );
        n = s;
        while( *p && *p != '/' )
          ++p;
        if( *p )
        {
          *p = '\0'; s = ++p;
        }
        else
         s = p;

        if( n && *n )
        {
          int   found = 0;
          char  tag[PATH_MAX] = { 0 };
          char  reminder[PATH_MAX] = { 0 };
          char *b, *r;

          struct cgit_ref_names *names = NULL;

          sprintf( reminder, "%s", s );
          sprintf( tag, "%s", n );

          r = (char *)&reminder[0];

          /*********************************
            Searching the real name of tag:
           */
          lookup_tags_by_prefix( &names, n );
          while( *r )
          {
            b = r;
            while( *r && *r != '/' )
              ++r;
            if( *r )
            {
              *r = '\0'; ++r;
            }

            if( b && *b )
            {
              char probe[PATH_MAX] = { 0 };
              sprintf( probe, "%s/%s", tag, b );

              found = 0;
              {
                size_t i, len = strlen( probe );
                for( i = 0; i < names->len; ++i )
                {
                  if( !strncmp( probe, names->name[i], len ) )
                  {
                    ++found;
                  }
                }
              }
              if( found == 1 )
              {
                strcat( tag, "/" );
                strcat( tag, b );
                break;
              }
              if( found )
              {
                strcat( tag, "/" );
                strcat( tag, b );
              }
            }
          }
          cgit_ref_names_free( names );

          if( found )
          {
            strcat( ref, tag );
            p += r - &reminder[0];
            s = p;
          }
          else
            strcat( ref, n );
        }
        else
          sprintf( ref, "refs/heads/%s", ctx.repo.trunk );

        break;
      }
      else if( !strcmp( n, "branches" ) )
      {
        sprintf( ref, "refs/heads/" );

        n = s;
        while( *p && *p != '/' )
          ++p;
        if( *p )
        {
          *p = '\0'; s = ++p;
        }
        else
         s = p;

        if( n && *n )
        {
          int   found = 0;
          char  branch[PATH_MAX] = { 0 };
          char  reminder[PATH_MAX] = { 0 };
          char *b, *r;

          struct cgit_ref_names *names = NULL;

          sprintf( reminder, "%s", s );
          sprintf( branch, "%s", n );

          r = (char *)&reminder[0];

          /************************************
            Searching the real name of branch:
           */
          lookup_branches_by_prefix( &names, n );
          while( *r )
          {
            b = r;
            while( *r && *r != '/' )
              ++r;
            if( *r )
            {
              *r = '\0'; ++r;
            }

            if( b && *b )
            {
              char probe[PATH_MAX] = { 0 };
              sprintf( probe, "%s/%s", branch, b );

              found = 0;
              {
                size_t i, len = strlen( probe );
                for( i = 0; i < names->len; ++i )
                {
                  if( !strncmp( probe, names->name[i], len ) )
                  {
                    ++found;
                  }
                }
              }
              if( found == 1 )
              {
                strcat( branch, "/" );
                strcat( branch, b );
                break;
              }
              if( found )
              {
                strcat( branch, "/" );
                strcat( branch, b );
              }
            }
          }
          cgit_ref_names_free( names );

          if( found )
          {
            strcat( ref, branch );
            p += r - &reminder[0];
            s = p;
          }
          else
            strcat( ref, n );
        }
        else
          sprintf( ref, "refs/heads/%s", ctx.repo.trunk );

        break;
      }
      else if( !strcmp( n, "trunk" ) )
      {
        sprintf( ref, "refs/heads/%s", ctx.repo.trunk );
        break;
      }
      else
      {
        /* return '/' */
        *--p = '/'; p = n;
        sprintf( ref, "refs/heads/%s", ctx.repo.trunk );
        break;
      }
    }

    path = p;

    if( *path )
    {
      len = (int)strlen( path );

      if( path[len-1] =='/' ) { path[len-1] = '\0'; --len; }
      len += 1;

      sprintf( rpath, "%s", path );
    }

    free( path_info );
  }
}

static void fill_hex_list( struct cgit_hex_commits **hex_commits, char *buf )
{
  char *s, *p = NULL, *h;
  struct cgit_hex_commits *commits = NULL;

  if( !hex_commits || !buf ) return;

  cgit_hex_commits_allocate( &commits );
  *hex_commits = commits;

  s = buf;
  while( *s )
  {
    while( *s &&  *s == '\n' )
      ++s;
    h = p = s;

    while( *p && *p != '\n' )
      ++p;
    if( *p )
    {
      *p = '\0'; s = ++p;
    }
    else
      s = p;

    cgit_hex_commits_add( commits, h );
  }
}


void cgit_fill_commits_list( struct cgit_hex_commits **hex_commits, int ofs, const char *relative_path, const char *revision )
{
  const char *name = NULL, *git_root = NULL, *repo_root = NULL;

  if( !hex_commits || !relative_path ) return;

  name      = ctx.repo.name;
  git_root  = ctx.repo.git_root;
  repo_root = ctx.repo.repo_root;

  if( name && git_root )
  {
    char ref[PATH_MAX] = { 0 };
    char rpath[PATH_MAX] = { 0 };
    int  psize = atoi( ctx.vars.page_size );

    char path[PATH_MAX] = { 0 };
    char cmd[PATH_MAX];
    struct strbuf buf = STRBUF_INIT;
    pid_t p = (pid_t) -1;
    int   rc;

    sprintf( (char *)&path[0], "%s/", git_root );
    if( repo_root && *repo_root )
    {
      strcat( (char *)&path[0], repo_root );
      strcat( (char *)&path[0], "/" );
    }
    strcat( (char *)&path[0], name );

    if( !is_bare( (char *)&path[0] ) )
    {
      strcat( (char *)&path[0], "/.git" );
    }

    parse_relative_path( (char *)&ref[0], (char *)&rpath[0], relative_path );

    /*
      NOTE:
      ====
        The --follow option for blobs is not applicable here because the option --skip=n
        doesn't works when option --follow is defined.

        In other words we cannot use --follow here:

        struct cgit_info  info = CGIT_INFO_INIT;
        const char *follow = "";

        if( revision && *revision )
          strncpy( info.revision, revision, GIT_OID_HEXSZ+1 );
        else
          strncpy( info.revision, ctx.repo.info.revision, GIT_OID_HEXSZ+1 );

        fill_commit_info( &info, (const char *)&path[0], (const char *)&rpath[0] );
        if( info.kind == GIT_OBJECT_BLOB )
        {
          follow = "--follow";
        }
     */

    ++psize;

    if( revision && *revision )
      snprintf( (char *)&cmd[0], 1024, "git --git-dir=%s log --pretty=format:'%%H' --skip=%d -%d %s -- %s 2>/dev/null", (char *)&path[0], ofs, psize, revision, (char *)&rpath[0] );
    else
      snprintf( (char *)&cmd[0], 1024, "git --git-dir=%s log --pretty=format:'%%H' --skip=%d -%d %s -- %s 2>/dev/null", (char *)&path[0], ofs, psize, (char *)&ref[0], (char *)&rpath[0] );
    p = sys_exec_command( &buf, cmd );
    rc = sys_wait_command( p, NULL );
    if( rc != 0 )
    {
      strbuf_release( &buf );
      return;
    }

    if( buf.buf[0] )
    {
      strbuf_trim( &buf );

      fill_hex_list( hex_commits, (char *)&buf.buf[0] );
    }

    strbuf_release( &buf );
  }

  return;
}

git_repository *cgit_open_repository( void )
{
  const char     *name = NULL, *git_root = NULL, *repo_root = NULL;
  git_repository *repo = NULL;

  name      = ctx.repo.name;
  git_root  = ctx.repo.git_root;
  repo_root = ctx.repo.repo_root;

  if( name && git_root )
  {
    char path[PATH_MAX] = { 0 };

    sprintf( (char *)&path[0], "%s/", git_root );
    if( repo_root && *repo_root )
    {
      strcat( (char *)&path[0], repo_root );
      strcat( (char *)&path[0], "/" );
    }
    strcat( (char *)&path[0], name );

    if( !is_bare( (char *)&path[0] ) )
    {
      strcat( (char *)&path[0], "/.git" );
    }

    return open_repository( (const char *)&path[0] );
  }

  return repo;
}

git_commit *lookup_commit_by_ref( const char *ref )
{
  const char *name = NULL, *git_root = NULL, *repo_root = NULL;
  git_commit *commit = NULL;

  if( !ref || !*ref ) return commit;

  name      = ctx.repo.name;
  git_root  = ctx.repo.git_root;
  repo_root = ctx.repo.repo_root;

  if( name && git_root )
  {
    git_repository *repo = NULL;
    char path[PATH_MAX] = { 0 };

    sprintf( (char *)&path[0], "%s/", git_root );
    if( repo_root && *repo_root )
    {
      strcat( (char *)&path[0], repo_root );
      strcat( (char *)&path[0], "/" );
    }
    strcat( (char *)&path[0], name );

    if( !is_bare( (char *)&path[0] ) )
    {
      strcat( (char *)&path[0], "/.git" );
    }

    if( !(repo = open_repository( (const char *)&path[0] )) ) return commit;
    commit = get_commit_by_ref( repo, ref );
    close_repository( repo );
  }

  return commit;
}

git_commit *lookup_commit_by_hex( const char *hex )
{
  const char *name = NULL, *git_root = NULL, *repo_root = NULL;
  git_commit *commit = NULL;

  if( !hex || !*hex ) return commit;

  name      = ctx.repo.name;
  git_root  = ctx.repo.git_root;
  repo_root = ctx.repo.repo_root;

  if( name && git_root )
  {
    git_repository *repo = NULL;
    char path[PATH_MAX] = { 0 };

    sprintf( (char *)&path[0], "%s/", git_root );
    if( repo_root && *repo_root )
    {
      strcat( (char *)&path[0], repo_root );
      strcat( (char *)&path[0], "/" );
    }
    strcat( (char *)&path[0], name );

    if( !is_bare( (char *)&path[0] ) )
    {
      strcat( (char *)&path[0], "/.git" );
    }

    if( !(repo = open_repository( (const char *)&path[0] )) ) return commit;
    commit = get_commit_by_hex( repo, hex );
    close_repository( repo );
  }

  return commit;
}


struct print_data {
  struct strbuf *sb;
  int *files;
  int *insertions;
  int *deletions;
};

static int printer( const git_diff_delta *delta, const git_diff_hunk *hunk, const git_diff_line *line, void *data )
{
  struct print_data *pdata = (struct print_data *)data;

  (void)delta; (void)hunk;

  switch( line->origin )
  {
    case GIT_DIFF_LINE_ADDITION: *pdata->insertions += 1; break;
    case GIT_DIFF_LINE_DELETION: *pdata->deletions  += 1; break;
    case GIT_DIFF_LINE_FILE_HDR: *pdata->files      += 1; break;
    default: break;
  }

  if( line->origin == GIT_DIFF_LINE_CONTEXT  ||
      line->origin == GIT_DIFF_LINE_ADDITION ||
      line->origin == GIT_DIFF_LINE_DELETION   )
    strbuf_addch( pdata->sb, line->origin );

  strbuf_add( pdata->sb, (const void *)line->content, (size_t)line->content_len );

  return 0;
}

void cgit_fill_diff_with_parent( struct strbuf *sb, char *parent_hex, size_t parent_len, int *files, int *insertions, int *deletions, const char *hex )
{
  const char *name = NULL, *git_root = NULL, *repo_root = NULL;

  git_commit *commit = NULL, *parent = NULL;
  git_tree   *commit_tree = NULL, *parent_tree = NULL;
  git_diff   *diff = NULL;

  struct print_data  data = { .sb = NULL, .files = NULL, .insertions = NULL, .deletions = NULL };

  if( !parent_hex || !files || !insertions || !deletions || !hex || !*hex ) return;
  if( parent_len != GIT_OID_HEXSZ+1 ) return;

  name      = ctx.repo.name;
  git_root  = ctx.repo.git_root;
  repo_root = ctx.repo.repo_root;

  if( name && git_root )
  {
    git_repository *repo = NULL;
    char path[PATH_MAX] = { 0 };

    sprintf( (char *)&path[0], "%s/", git_root );
    if( repo_root && *repo_root )
    {
      strcat( (char *)&path[0], repo_root );
      strcat( (char *)&path[0], "/" );
    }
    strcat( (char *)&path[0], name );

    if( !is_bare( (char *)&path[0] ) )
    {
      strcat( (char *)&path[0], "/.git" );
    }

    if( !(repo = open_repository( (const char *)&path[0] )) ) return;

    commit = get_commit_by_hex( repo, hex );
    if( git_commit_parent( &parent, commit, 0 ) < 0 )
    {
      git_commit_free( commit );
      close_repository( repo );
      return;
    }
    if( git_oid_tostr( parent_hex, GIT_OID_HEXSZ+1, git_commit_id( parent ) ) < 0 )
    {
      git_commit_free( commit );
      git_commit_free( parent );
      close_repository( repo );
      return;
    }

    if( git_commit_tree( &commit_tree, commit ) < 0 )
    {
      git_commit_free( commit );
      git_commit_free( parent );
      close_repository( repo );
      return;
    }
    if( git_commit_tree( &parent_tree, parent ) < 0 )
    {
      git_tree_free( commit_tree );
      git_commit_free( commit );
      git_commit_free( parent );
      close_repository( repo );
      return;
    }
    if( git_diff_tree_to_tree( &diff, repo, parent_tree, commit_tree, NULL ) < 0 )
    {
      git_tree_free( commit_tree );
      git_tree_free( parent_tree );
      git_commit_free( commit );
      git_commit_free( parent );
      close_repository( repo );
      return;
    }


    *files = 0, *insertions = 0, *deletions = 0;

    data.sb         = sb;
    data.files      = files;
    data.insertions = insertions;
    data.deletions  = deletions;

    git_diff_print( diff, GIT_DIFF_FORMAT_PATCH, printer, &data );

    *files = *data.files, *insertions = *data.insertions, *deletions = *data.deletions;

    git_diff_free( diff );
    git_tree_free( commit_tree );
    git_tree_free( parent_tree );
    git_commit_free( commit );
    git_commit_free( parent );

    close_repository( repo );
  }
}


void cgit_diff_with_parent( struct strbuf *sb, char *parent_hex, size_t parent_len, int *files, int *insertions, int *deletions, const char *hex, const char *relative_path )
{
  const char *name = NULL, *git_root = NULL, *repo_root = NULL;

  git_commit *commit = NULL, *parent = NULL;

  if( !parent_hex || !files || !insertions || !deletions || !hex || !*hex || !relative_path || !*relative_path ) return;
  if( parent_len != GIT_OID_HEXSZ+1 ) return;

  name      = ctx.repo.name;
  git_root  = ctx.repo.git_root;
  repo_root = ctx.repo.repo_root;

  *files = 0, *insertions = 0, *deletions = 0;

  if( name && git_root )
  {
    char ref[PATH_MAX] = { 0 };
    char rpath[PATH_MAX] = { 0 };

    git_repository *repo = NULL;
    char path[PATH_MAX] = { 0 };
    char cmd[PATH_MAX];
    struct strbuf buf = STRBUF_INIT;
    pid_t p = (pid_t) -1;
    int   rc;

    sprintf( (char *)&path[0], "%s/", git_root );
    if( repo_root && *repo_root )
    {
      strcat( (char *)&path[0], repo_root );
      strcat( (char *)&path[0], "/" );
    }
    strcat( (char *)&path[0], name );

    if( !is_bare( (char *)&path[0] ) )
    {
      strcat( (char *)&path[0], "/.git" );
    }

    if( !(repo = open_repository( (const char *)&path[0] )) ) return;

    commit = get_commit_by_hex( repo, hex );
    if( git_commit_parent( &parent, commit, 0 ) < 0 )
    {
      git_commit_free( commit );
      close_repository( repo );
      return;
    }
    if( git_oid_tostr( parent_hex, GIT_OID_HEXSZ+1, git_commit_id( parent ) ) < 0 )
    {
      git_commit_free( commit );
      git_commit_free( parent );
      close_repository( repo );
      return;
    }

    git_commit_free( commit );
    git_commit_free( parent );
    close_repository( repo );

    /* now we have two revisions */
    parse_relative_path( (char *)&ref[0], (char *)&rpath[0], relative_path );

    snprintf( (char *)&cmd[0], 1024, "git --git-dir=%s diff %s %s -- %s 2>/dev/null", (char *)&path[0], parent_hex, hex, (char *)&rpath[0] );
    p = sys_exec_command( &buf, cmd );
    rc = sys_wait_command( p, NULL );
    if( rc != 0 )
    {
      strbuf_release( &buf );
      return;
    }

    if( buf.buf[0] )
    {
      char *p = (char *)&buf.buf[0];

      while( *p )
      {
        if( *p == '\n' && p[1] && p[2] && p[3] )
        {
          if( (p[1] == '+' && p[2] == '+' && p[3] == '+') ||
              (p[1] == '-' && p[2] == '-' && p[3] == '-')   )
            *files += 1;
          if( p[1] == '+' && p[2] != '+' && p[3] != '+' ) { ++p; *insertions += 1; }
          if( p[1] == '-' && p[2] != '-' && p[3] != '-' ) { ++p; *deletions  += 1; }
        }
        ++p;
      }
      *files /= 2;

      strbuf_addbuf( sb, (const struct strbuf *)&buf );
    }

    strbuf_release( &buf );
  }
}