/* ** Copyright (c) 2002 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the GNU General Public ** License as published by the Free Software Foundation; either ** version 2 of the License, or (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ** General Public License for more details. ** ** You should have received a copy of the GNU General Public ** License along with this library; if not, write to the ** Free Software Foundation, Inc., 59 Temple Place - Suite 330, ** Boston, MA 02111-1307, USA. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains code used to call svnlook to get information ** about repository's history and update the CHNG and FILECHNG ** tables according to the content of it's output. All the other ** Subversion-specific stuff should also be found here. */ #include "config.h" #include #include #include #include #include "svn.h" /* ** Instance of following structure holds information about revision */ typedef struct Revision Revision; struct Revision { char zAuthor[100]; /* Author name */ int nDate; /* Revision date */ int nMsgLength; /* Number of characters in log message */ char *zMessage; /* Log message */ }; /* ** This func is supposed to deal with "svn copy" and update FILECHNG and ** FILE for each file in copied directory. */ static void svn_insert_copied_files( int cn, int nRev, const char *zNewDir, const char *zOldDir, int skipInsertFile ){ int i; size_t nLen; char **azTree; char *zTmp; /* We need paths without leading '/' here since that is what we ** already have in db. */ while( zNewDir[0]=='/' ) zNewDir++; while( zOldDir[0]=='/' ) zOldDir++; zTmp = mprintf("%s/", zOldDir); if( zTmp==0 ) return; nLen = strlen(zTmp)-1; /* TODO: make sure zOldDir always ends with / if it's not empty */ /* This query should return only files that are not deleted. */ azTree = db_query( "SELECT t1.filename, t1.chngtype FROM filechng t1 " "INNER JOIN (SELECT filename, max(vers) AS maxvers FROM filechng " " WHERE filename LIKE '%q%%' GROUP BY filename) t2 ON " "t1.filename=t2.filename AND t1.vers=t2.maxvers AND t1.chngtype<>2", zTmp ); free(zTmp); if( azTree==0 ) return; for(i=0; azTree[i]; i+=2){ char *zFile = mprintf( "%s%s", zNewDir, &(azTree[i])[nLen]); int nFLen = strlen(zFile); if( nFLen && zFile[nFLen-1]!='/' ){ db_execute( "REPLACE INTO filechng(cn,filename,vers,prevvers,chngtype) " "VALUES(%d,'%q',%d,'%s',1);", cn, zFile, nRev, ( azTree[i+1][0]=='0' ) ? "" : azTree[i+1] ); } if( !skipInsertFile ) insert_file(zFile, cn); free(zFile); } db_query_free(azTree); } /* ** This func is supposed to deal with "svn delete" on direcories and update ** FILECHNG for each file and dir in deleted directory. */ static void svn_delete_dir( int cn, int nRev, const char *zDir, int skipInsertFile ){ int i; char **azTree; /* We need paths without leading '/' here since that is what we ** already have in db. */ while( zDir[0]=='/' ) zDir++; azTree = db_query( "SELECT t1.filename, t1.vers FROM filechng t1 " "INNER JOIN (SELECT filename, max(vers) AS maxvers FROM filechng " " WHERE filename LIKE '%q%%' GROUP BY filename) t2 ON " "t1.filename=t2.filename AND t1.vers=t2.maxvers AND t1.chngtype<>2", zDir ); if( azTree==0 ) return; for(i=0; azTree[i]; i+=2){ int nFLen = strlen(azTree[i]); if( nFLen && azTree[i][nFLen-1]!='/' ){ db_execute( "REPLACE INTO filechng(cn,filename,vers,prevvers,chngtype) " "VALUES(%d,'%q',%d,'%s',2);", cn, azTree[i], nRev, ( azTree[i+1][0]=='0' ) ? "" : azTree[i+1] ); } if( !skipInsertFile ) insert_file(azTree[i], cn); } db_query_free(azTree); } /* ** A little hackish way to determine if we even need to call "svnlook youngest". */ static int svn_did_repository_change( const char * zRoot, time_t since ){ struct stat statbuf; char *zFilename = mprintf("%s/db/revisions",zRoot); /* bdb */ if( stat(zFilename, &statbuf) ){ free(zFilename); zFilename = mprintf("%s/db/current",zRoot); /* fsfs */ if( stat(zFilename, &statbuf) ){ free(zFilename); return 1; /* don't know, say yes */ } } free(zFilename); /* Should run every hour, just in case there was some timing problems... */ return statbuf.st_mtime > since || (time(NULL)-since)>3600; } /* ** Process recent activity in the Subversion repository. ** ** If any errors occur, output an error page and exit. ** ** If the "isReread" flag is set, it means the history file should be ** reread to pick up changes that we may have missed earlier. */ static int svn_history_update(int isReread){ const char *zRoot; char **azResult; FILE *in; int nRev; /* Current revison number in loop */ int cnum = 0; /* check-in number to use for this checkin */ int next_cnum; /* next unused check-in number */ int nErr = 0; char *zCmd; char zLine[2000]; int nBaseRevision = 0; /* Revision we last seen, and have stored in db */ int nHeadRevision = 0; /* Latest revision in repository */ int nLine; int isInitialScan; /* If set delay updating FILE till after the last revision */ Revision *pRev; db_execute("BEGIN"); /* Get the path to local repository and last revision number we have in db * If there's no repository defined, bail and wait until the admin sets one. */ zRoot = db_config("cvsroot",""); if( zRoot[0]==0 ) return 1; nBaseRevision = atoi(db_config("historysize","0")); if( nBaseRevision && !svn_did_repository_change(zRoot,atoi(db_config("svnlastupdate","0"))) ){ db_execute("COMMIT"); return 1; } isInitialScan = (nBaseRevision==0); /* Get the number of latest revision in repository */ zCmd = mprintf("svnlook youngest '%s' 2>/dev/null", quotable_string(zRoot)); in = popen(zCmd, "r"); if( in==0 ){ error_init(&nErr); cgi_printf("
  • Unable to execute the following command:\n" "

    \n"
               "%h\n"
               "

  • \n",zCmd); error_finish(nErr); free(zCmd); return -1; } free(zCmd); if( fgets(zLine, sizeof(zLine), in) ){ nHeadRevision = atoi(zLine); } pclose(in); /* This could mean that repository is empty, no revisions in it yet. ** But this could also mean that atoi() failed. And that would be bad since ** it means svnlook's output format changed. */ if( nHeadRevision==0 ){ error_init(&nErr); cgi_printf("
  • Repository '%h' appears empty. Latest revision\n" "seems to be '%h'

  • \n",zRoot,zLine); error_finish(nErr); return -1; } /* See if there are some new revisions in repository */ if( nHeadRevision==nBaseRevision ) { /* No changes to the repository since our last scan ** Exit without doing anything */ db_execute("UPDATE config SET value=%d WHERE name='svnlastupdate';", time(NULL)+1); db_execute("COMMIT"); return 0; } /* Find the next available change number */ azResult = db_query("SELECT max(cn) FROM chng"); next_cnum = atoi(azResult[0])+1; db_query_free(azResult); pRev = (Revision *) malloc( sizeof(*pRev) ); /* ** Parse output of svnlook changes and svnlook info to get the info we need ** We do this for each revision we miss from db */ for(nRev=nBaseRevision+1; nRev<=nHeadRevision; nRev++){ int nAddChng = 1; int isMsgEnd = 0; /* Example output of "svnlook info" is as follows: ** ** chorlya <-Author ** 2005-08-02 01:51:45 +0200 (Tue, 02 Aug 2005) <-Date ** 26 <- # of chars in message ** Changes to some make files <- multiline message ** <- followed by blank line */ zCmd = mprintf("svnlook info -r %d '%s' 2>/dev/null", nRev, quotable_string(zRoot)); in = popen(zCmd, "r"); if( in==0 ){ error_init(&nErr); cgi_printf("
  • Unable to execute the following command:\n" "

    \n"
                 "%h\n"
                 "

  • \n",zCmd); error_finish(nErr); free(zCmd); return -1; } free(zCmd); nLine = 0; while( fgets(zLine,sizeof(zLine),in) ){ if( nLine==0 ){ remove_newline(zLine); bprintf(pRev->zAuthor, sizeof(pRev->zAuthor), "%.90s", zLine); }else if( nLine==1 ){ struct tm sTm; /* Since timestamp is localtime, we need to subtract timezone offset ** to get timestamp in UTC */ int nOffstHr, nOffstMn; /* Timezone offset hour and minute */ zLine[25] = 0; if( sscanf(zLine,"%d-%d-%d %d:%d:%d %3d%2d", &sTm.tm_year, &sTm.tm_mon, &sTm.tm_mday, &sTm.tm_hour, &sTm.tm_min, &sTm.tm_sec, &nOffstHr, &nOffstMn)==8 ){ sTm.tm_year -= 1900; sTm.tm_mon--; /* We subtract our timezone offset from tm.tm_sec since tm_sec ** is just added to rest of the timestamp without any calculations ** being performed on it. Because of that tm_sec can be negative! */ sTm.tm_sec -= (nOffstHr>0) ? (nOffstHr*60 + nOffstMn)*60 : (nOffstHr*60 - nOffstMn)*60 ; pRev->nDate = mkgmtime(&sTm); } }else if( nLine==2 ){ pRev->nMsgLength = atoi(zLine); if( pRev->nMsgLength==0 ){ isMsgEnd = 1; pRev->zMessage = ""; break; /* No comment here */ }else{ /* Allocate storage space for comment. */ pRev->zMessage = (char *) malloc(pRev->nMsgLength+16); if( pRev->zMessage==NULL ){ break; /* malloc() failed */ } } }else{ /* Before we begin concatenating lines of comment make sure we start ** with empty string. */ if( nLine==3 ){ pRev->zMessage[0] = 0; } /* Concat comment lines into one string */ strcat(pRev->zMessage, zLine); } if( isMsgEnd ) break; nLine++; } pclose(in); if( isReread ){ char *zPrior = db_short_query( "SELECT cn FROM chng WHERE date=%d AND user='%q'", pRev->nDate, pRev->zAuthor ); if( zPrior==0 || (cnum = atoi(zPrior))<=0 ){ cnum = next_cnum++; }else{ nAddChng = 0; } if(zPrior) free(zPrior); }else{ cnum = next_cnum++; } /* We assume there can't ever be an empty commit, so we just INSERT ** TODO: check information read from "svnlook info" for assumed pattern. ** If it deviates from it too much, abort. */ if( nAddChng ){ if( pRev->zMessage==0 ) pRev->zMessage = ""; db_execute( "INSERT INTO chng(cn, date, branch, milestone, user, message) " "VALUES(%d,%d,'',0,'%q','%q');", cnum, pRev->nDate, pRev->zAuthor, pRev->zMessage ); xref_checkin_comment(cnum, pRev->zMessage); } /* Now that we've got the common revision info, that is same for every ** file in this revision, we need to get the list of files that changed ** in this revision and populate filechng table with it. ** ** Example output of "svnlook changed" is as follows: ** A trunk/vendors/deli/ ** A trunk/vendors/deli/chips.txt ** A trunk/vendors/deli/sandwich.txt ** A trunk/vendors/deli/pickle.txt ** ** First column is “svn update-style” status letter. ** Then there are 3 columns that are of no intrest to us, ** and then comes the file path. */ zCmd = mprintf("svnlook changed -r %d '%s' 2>/dev/null", nRev, quotable_string(zRoot)); in = popen(zCmd, "r"); if( in==0 ){ error_init(&nErr); cgi_printf("
  • Unable to execute the following command:\n" "

    \n"
                 "%h\n"
                 "

  • \n",zCmd); error_finish(nErr); free(zCmd); return -1; } free(zCmd); while( fgets(zLine,sizeof(zLine),in) && zLine[0]!='\n' && zLine[0]!='\r' ){ int nChngType = 0; const char *zFilename = &zLine[4]; char *zPrevVersion; size_t nPos, nLen = strlen(zLine); if( nLen<4 ) continue; /* ** First char indicates what happend to file/dir in this revision. ** Following table shows the meaning of some expected values for first ** char and how we map that to filechng field in database: ** ** Char Meaning chngtype ** ========================================= ** U File updated (modified) 0 ** A File added to repository 1 ** D File deleted (removed) 2 ** ** First strip CR and LF chars from right end of filename */ remove_newline(&zLine[4]); if( zLine[0]=='U' ){ nChngType = 0; }else if( zLine[0]=='D' ){ nChngType = 2; /* If dir was deleted we need to delete all files and dirs in it. */ nPos = strlen(zFilename)-1; if( zFilename[nPos]=='/' ){ svn_delete_dir(cnum, nRev, zFilename, isInitialScan); } }else if( zLine[0]=='A' ){ /* If this is the first revision there can't be any copies in ** repository yet. */ if( nRev>1 ){ /* TODO: check to make sure dir is actually a copy before calling ** svn_insert_copied_files() since this func is pretty slow. */ nPos = strlen(zFilename)-1; if( zFilename[nPos]=='/' ){ FILE *history; char zHistLine[2000]; char *zCurrPath = NULL; int i=0, nHistErr=0; zCmd = mprintf("svnlook history -r %d '%s' '%s' 2>/dev/null", nRev, quotable_string(zRoot), quotable_string(zFilename)); history = popen(zCmd, "r"); if( history==0 ){ error_init(&nErr); cgi_printf("
  • Unable to execute the following command:\n" "

    \n"
                         "%h\n"
                         "

  • \n",zCmd); error_finish(nErr); free(zCmd); return -1; } free(zCmd); while( fgets(zHistLine,sizeof(zHistLine),history) && i<4 && !nHistErr ){ remove_newline(zHistLine); switch( i ){ case 0: if( strncmp(zHistLine, "REVISION PATH", 15)!=0 ) nHistErr++; break; case 1: if( strncmp(zHistLine, "-------- ----", 15)!=0 ) nHistErr++; break; case 2: /* We need to store this just so we can compare it ** with next line. */ zCurrPath = mprintf("%s", &zHistLine[11]); break; case 3: /* If paths changed, this dir was copied. */ if( strcmp(zCurrPath, &zHistLine[11])!=0 ){ svn_insert_copied_files(cnum, nRev, zCurrPath, &zHistLine[11], isInitialScan); } break; } i++; } pclose(history); if( zCurrPath ) free(zCurrPath); } } nChngType = 1; }else{ nChngType = -1; /* TODO: do something smart(tm) here :D */ continue; } zPrevVersion = db_short_query( "SELECT vers FROM filechng WHERE filename='%q' AND cn<%d " "ORDER BY cn DESC;", zFilename, cnum ); db_execute( "REPLACE INTO filechng(cn,filename,vers,prevvers,chngtype) " "VALUES(%d,'%q',%d,'%q',%d);", cnum, zFilename, nRev, zPrevVersion ? zPrevVersion : "", nChngType ); if(zPrevVersion) free(zPrevVersion); if( !isInitialScan ) insert_file(zFilename, cnum); } pclose(in); } free(pRev); /* We delayed populating FILE till now on initial scan */ if( isInitialScan ){ update_file_table_with_lastcn(); } /* Update the "historysize" entry so that we know last revision number that ** we have in db, and "svnlastupdate" to keep those calls to svnlook youngest ** to minimum. */ db_execute("UPDATE config SET value=%d WHERE name='historysize';", nHeadRevision); db_execute("UPDATE config SET value=%d WHERE name='svnlastupdate';", time(NULL)+1); db_config(0,0); /* Commit all changes to the database */ db_execute("COMMIT;"); error_finish(nErr); return nErr ? -1 : 0; } /* ** Diff two versions of a file, handling all exceptions. ** ** If oldVersion is NULL or "0", then this function will output the ** text of version newVersion of the file instead of doing a diff. */ static int svn_diff_versions( const char *oldVersion, const char *newVersion, const char *zFile ){ const char *zTemplate; char *zCmd; FILE *in; int i; const char *azSubst[16]; char zLine[2][2000]; int nBuf, inFile; char *z; long long nLine; int file_cat; /* set when we want "svnlook cat" instead of "svnlook diff" */ if( zFile==0 ) return -1; /* If this is the first time file is checked into repository (Added), ** don't diff it just display it's contents */ z = mprintf("%s", newVersion); previous_version(z, zFile); if( z==0 || z[0]==0 ) file_cat = 1; /* svnlook cat */ else file_cat = 0; /* svnlook diff */ free(z); /* Find the command used to compute the file difference. */ azSubst[0] = "F"; azSubst[1] = zFile; azSubst[2] = "V1"; azSubst[3] = oldVersion; azSubst[4] = "V2"; azSubst[5] = newVersion; azSubst[6] = "RP"; azSubst[7] = db_config("cvsroot", ""); azSubst[8] = "V"; azSubst[9] = newVersion; azSubst[10] = 0; if( file_cat==1 ){ zTemplate = db_config("filelist","svnlook cat -r '%V' '%RP' '%F' 2>/dev/null"); }else{ zTemplate = db_config("filediff","svnlook diff -r '%V2' '%RP' 2>/dev/null"); } zCmd = subst(zTemplate, azSubst); in = popen(zCmd, "r"); free(zCmd); if( in==0 ) return -1; if( file_cat==0 ){ /* We're looking for something like this: ** ** Modified: trunk/vendors/deli/sandwich.txt ** ============================================================================== ** ** So first we look for a delimiter line since it should be more uniqe ** then one before it. When we find it, we go one line back and check ** if that is the file we're after. That line can begin with any element ** of zMarker. */ const char zMarker[3][11] = { "Modified: ", "Added: ", "Deleted: " }; int nMarkerLen[3] = { 10, 7, 9 }; nBuf = nLine = inFile = 0; cgi_printf("
    \n");
        while( fgets(zLine[nBuf], sizeof(zLine[nBuf]), in) ){
          if( strncmp(zLine[nBuf], "===================================================================", 67)==0
              && (zLine[nBuf][67]=='\n' || zLine[nBuf][67]=='\r')
          ){
            if( inFile ){
              /* This is the begging of some other file, and end of ours */
              break;
            } else {
              /* If previous line begins with one of our markers, 
              ** it could be what we're looking for
              */
              for(i=0; i\n");
      } else {
        output_pipe_as_html(in,1);
        pclose(in);
      }
      
      return 0;
    }
    
    static int svn_dump_version(const char *zVersion, const char *zFile,int bRaw){
      char *zCmd;
      int rc = -1;
      
      if( !zVersion || !zVersion[0] ){
        zCmd = mprintf("svnlook cat '%s' '%s' 2>/dev/null", 
          db_config("cvsroot", ""), quotable_string(zFile) );
      }else{
        zCmd = mprintf("svnlook cat -r '%s' '%s' '%s' 2>/dev/null", 
          quotable_string(zVersion), db_config("cvsroot", ""), quotable_string(zFile) );
      }
      
      rc = common_dumpfile( zCmd, zVersion, zFile, bRaw );
      free(zCmd);
    
      return rc;
    }
    
    static int svn_diff_chng(int cn, int bRaw){
      const char *zRoot;
      char *zRev;
      char *zCmd;
      char zLine[2000];
      FILE *in;
      
      zRev = db_short_query("SELECT vers FROM filechng WHERE cn=%d", cn);
      if( !zRev || !zRev[0] ) return -1; /* Invalid check-in number */
      
      zRoot = db_config("cvsroot", "");
      zCmd = mprintf("svnlook diff -r '%s' '%s' 2>/dev/null",
        quotable_string(zRev), quotable_string(zRoot));
      free(zRev);
      
      in = popen(zCmd, "r");
      free(zCmd);
      if( in==0 ) return -1;
      
      if( bRaw ){
        while( !feof(in) ){
          int amt = fread(zLine, 1, sizeof(zLine), in);
          if( amt<=0 ) break;
          cgi_append_content(zLine, amt);
        }
      }else{
        output_pipe_as_html(in,1);
      }
      pclose(in);
      
      return 0;
    }
    
    /*
    ** svntrac can import users only from svnserver's users file. If you use some 
    ** other authentication method you'll have to enter users manually or import 
    ** them in some other way.
    */
    
    /*
    ** Load the names of all users listed in users file into a temporary
    ** table "tuser". First locate the users file and figureout what permisions
    ** have authenticated users.
    */
    static void svn_read_users_file(const char *zSvnRoot){
      FILE *f;
      int i, bInSection=0;
      char *zFile, *zKey, *zValue;
      char zLine[2000];
      char *zAnonAccess, *zAuthAccess;
      /* We set these to Subversion defaults in case they are not defined 
      ** explicitly in .conf file.
      */
      zAnonAccess = "o"; /* TODO: use this to set anon user's perms */
      zAuthAccess = "io";
      
      db_execute(
        "CREATE TEMP TABLE tuser(id UNIQUE ON CONFLICT IGNORE,pswd,sysid,cap);"
      );
      if( zSvnRoot==0 ){
        zSvnRoot = db_config("cvsroot","");
      }
      zFile = mprintf("%s/conf/svnserve.conf", zSvnRoot);
      f = fopen(zFile, "r");
      free(zFile);
      /*
      ** This is how Subversion .conf file should look like:
      ** 
      ** # This is commnet
      ** [general] # Section name
      ** anon-access = read # What can anon users do
      ** auth-access = write # What can auth users do
      ** password-db = passwd # Absolute or relative location of users file
      ** realm = My First Repository # This is of no intrest to us
      ** [some_other_section]
      */
      if( f ){
        while( fgets(zLine, sizeof(zLine), f) ){
          remove_newline(zLine);
          for(i=0; zLine[i] && isspace(zLine[i]); i++){}
          if( zLine[i]==0 || zLine[i]=='#' ) continue;
          if( zLine[i]=='[' ){
            /* If we are already in section, then this marks the end of our section.
            ** If not, this might be the start of our section.
            */
            if( bInSection ){
              bInSection = 0;
              break;
            } else if( strncmp(&zLine[i], "[general]", 9)==0 ){
              bInSection = 1;
              continue;
            }
          }
          
          /* If we're not in our section there is nothing to look for in this line.
          ** TODO: I made an assumption here that all keys for key-value pairs in 
          ** .conf files have to start with alnum char. Check if this really 
          ** is the case.
          */
          if( !bInSection || !isalnum(zLine[i]) ) continue;
          
          /* Extract key-value pair */
          zKey = &zLine[i];
          for(; zLine[i] && !isspace(zLine[i]); i++){}
          zLine[i++] = 0;
          for(; zLine[i] && isspace(zLine[i]); i++){}
          if( zLine[i++]!='=' ) continue; /* Not valid key-value pair */
          for(; zLine[i] && isspace(zLine[i]); i++){}
          zValue = &zLine[i];
          for(; zLine[i] && !isspace(zLine[i]); i++){}
          zLine[i] = 0; /* Make sure we rtrim our value */
          
          /* We handel each key differently */
          if( strcmp(zKey, "anon-access")==0 ){
            if( strcmp(zValue, "none")==0 ){ zAnonAccess=""; continue; }
            if( strcmp(zValue, "read")==0 ){ zAnonAccess="o"; continue; }
            if( strcmp(zValue, "write")==0 ){ zAnonAccess="io"; continue; }
          }
          
          if( strcmp(zKey, "auth-access")==0 ){
            if( strcmp(zValue, "write")==0 ){ zAuthAccess="io"; continue; }
            if( strcmp(zValue, "read")==0 ){ zAuthAccess="o"; continue; }
            if( strcmp(zValue, "none")==0 ){ zAuthAccess=""; continue; }
          }
          
          if( strcmp(zKey, "password-db")==0 ){
            if( zValue[0]=='/' ){
              /* We've got absolute path, no problem here */
              zFile = mprintf("%s", zValue);
              continue;
            } else {
              /* We've got relative path */
              zFile = mprintf("%s/conf/%s", zSvnRoot, zValue);
              continue;
            }
          }
        }
        fclose(f);
      }
      
      /* If we didn't find path to users file, exit */
      if( !zFile || !zFile[0] ) return;
      
      f = fopen(zFile, "r");
      free(zFile);
      
      /* Read username->password pairs from file and store them in db
      ** Passwords are stored in clear text.
      */
      if( f ){
        bInSection = 0;
        while( fgets(zLine, sizeof(zLine), f) ){
          char zBuf[3];
          char zSeed[100];
          const char *z;
          remove_newline(zLine);
          for(i=0; zLine[i] && isspace(zLine[i]); i++){}
          if( zLine[i]==0 || zLine[i]=='#' ) continue;
          if( zLine[i]=='[' ){
            /* If we are already in section, then this marks the end of our section.
            ** If not, this might be the start of our section.
            */
            if( bInSection ){
              bInSection = 0;
              break;
            } else if( strncmp(&zLine[i], "[users]", 7)==0 ){
              bInSection = 1;
              continue;
            }
          }
          
          /* If we're not in our section there is nothing to look for in this line.
          ** TODO: I made an assumption here that all usernames have to start 
          ** with alnum char. Check if this really is the case.
          */
          if( !bInSection || !isalnum(zLine[i]) ) continue;
          
          /* Each line here is key-value pair, or in our case uname-passwd pair */
          zKey = &zLine[i];
          for(; zLine[i] && !isspace(zLine[i]); i++){}
          zLine[i++] = 0;
          for(; zLine[i] && isspace(zLine[i]); i++){}
          if( zLine[i++]!='=' ) continue; /* Not valid key-value pair */
          for(; zLine[i] && isspace(zLine[i]); i++){}
          zValue = &zLine[i];
          for(; zLine[i] && !isspace(zLine[i]); i++){}
          zLine[i] = 0; /* Make sure we rtrim our value */
          
          /* We need to encrypt passwords */
          bprintf(zSeed,sizeof(zSeed),"%d%.20s",getpid(),zKey);
          z = crypt(zSeed, "aa");
          zBuf[0] = z[2];
          zBuf[1] = z[3];
          zBuf[2] = 0;
              
          db_execute("INSERT INTO tuser VALUES('%q','%q','','%s');",
             zKey, crypt(zValue, zBuf), zAuthAccess);
        }
        fclose(f);
      }
    }
    
    /*
    ** Read svnserve users file and record infomation gleaned from that file
    ** in the local database.
    */
    static int svn_user_read(void){
      db_add_functions();
      db_execute("BEGIN");
      svn_read_users_file(0);
    
      db_execute(
        "REPLACE INTO user(id,name,email,passwd,capabilities) "
        "  SELECT n.id, o.name, o.email, n.pswd, "
        "      cap_or(cap_and(o.capabilities, 'aknrsw'), n.cap) "
        "  FROM tuser as n, user as o WHERE n.id=o.id;"
        "INSERT OR IGNORE INTO user(id,name,email,passwd,capabilities) "
        "  SELECT id, id, '', pswd,"
        "      cap_or((SELECT capabilities FROM user WHERE id='anonymous'),cap) "
        "  FROM tuser;"
        "COMMIT;"
      );
      return 0;
    }
    
    void init_svn(){
      g.scm.zSCM = "svn";
      g.scm.zName = "Subversion";
      g.scm.pxHistoryUpdate = svn_history_update;
      g.scm.pxDiffVersions = svn_diff_versions;
      g.scm.pxDiffChng = svn_diff_chng;
      g.scm.pxIsFileAvailable = 0;  /* use the database */
      g.scm.pxDumpVersion = svn_dump_version;
      g.scm.pxUserRead = svn_user_read;
    }