# # add_file "CVS_prot" # # add_file "cryptopp/md5.cpp" # # add_file "cryptopp/md5.h" # # add_file "cvs_client.cc" # # add_file "cvs_client.hh" # # add_file "cvs_repository.cc" # # add_file "cvs_sync.hh" # # add_file "tests.cvs/cleanup" # # add_file "tests.cvs/cvs_test_setup" # # add_file "tests.cvs/cvs_test_setup2" # # add_file "tests.cvs/cvs_test_setup3" # # add_file "tests.cvs/cvs_test_setup4" # # add_file "tests.cvs/cvs_test_setup5" # # add_file "tests.cvs/db_setup" # # add_file "tests.cvs/db_setup2" # # add_file "tests.cvs/show_disk_usage" # # add_file "tests.cvs/test_all" # # patch "CVS_prot" # from [] # to [2edf9eed139ed6c230c369936226b54c3e48d5c1] # # patch "Makefile.am" # from [b8121dcf5a8aecc511e437ae64e8bdfbba9e880f] # to [c64be8cfa00d495951365647e859fe334746b879] # # patch "app_state.cc" # from [63120a266ad19ebd72022cf172ccbd0c299aa413] # to [9c72da32b289c232fe033e01a8a5e71423ae4b42] # # patch "app_state.hh" # from [9cfdb7a5976dc31edda11e99ee74046fdc318469] # to [1c4db2a52cf1545879ec72794a048dc9dc1e4337] # # patch "commands.cc" # from [958cd3bbc982aef81f63b90b24dcebaddf135166] # to [954eae0f9aab6d01ac0f1d004dcbfa891ed9efa0] # # patch "cryptopp/md5.cpp" # from [] # to [c793132fc542095cd1be2202bfbbba975689bc06] # # patch "cryptopp/md5.h" # from [] # to [d8dc97b72a5c70dae9aee05a738b3d4b60850326] # # patch "cvs_client.cc" # from [] # to [4bec53be82a9270e46502a8d07a57238d8a3a14d] # # patch "cvs_client.hh" # from [] # to [86c60057e7f4e7701941f3325443830055c6d26f] # # patch "cvs_repository.cc" # from [] # to [c341f1a00f649770dae83565671ffd220577a760] # # patch "cvs_sync.hh" # from [] # to [b8f88462b636a87da0c4e13741c8317cac2058d4] # # patch "monotone.cc" # from [2419f27fdb3cba77c658c0f4eac4d0b5026e1884] # to [05a3339e9ffad86e0a30fd9c098b454f437ad100] # # patch "tests.cvs/cleanup" # from [] # to [78eabbe32cfeac82e5a5ea39fb66d7eba4274db7] # # patch "tests.cvs/cvs_test_setup" # from [] # to [a1aa4811e4eefab324c3ecec9c5b7f0dccfb51f9] # # patch "tests.cvs/cvs_test_setup2" # from [] # to [bcd67b86a92f212aaaba3a2e386cb6f91082c291] # # patch "tests.cvs/cvs_test_setup3" # from [] # to [bb48d6e082cb04fb28c9166c857d27fcd21433bb] # # patch "tests.cvs/cvs_test_setup4" # from [] # to [870845f6feea679b69a78accda0da0aa17144965] # # patch "tests.cvs/cvs_test_setup5" # from [] # to [40606183f2ac98cc92c859e00c85c84635f8fcc4] # # patch "tests.cvs/db_setup" # from [] # to [bab9d4f569598d11c9d76d8364a4e7a45e272cc2] # # patch "tests.cvs/db_setup2" # from [] # to [c8441181093f8f7c362ce69cbe2ad4493baa89ac] # # patch "tests.cvs/show_disk_usage" # from [] # to [5b2d9f6c4c070775c84df1a03d5e8a745c51f252] # # patch "tests.cvs/test_all" # from [] # to [b326f02c2a34f85114739df05f065784495db82c] # # patch "ui.cc" # from [f0e2ff249a83fa8a210f961d773ca560bb281592] # to [c79f0455cbe6fb8d0c93131af4e99851cd9a0b9c] # # patch "ui.hh" # from [7cdd4335a8fa4b4828470eb69b363792bd0a5ce8] # to [49bd919b74de6d09b5c435f9b4a9adb788f11acc] # --- CVS_prot +++ CVS_prot @@ -0,0 +1,139 @@ +commands with a capital letter do not give a response +first command must be Root (only as first command) + +Interesting commands: + Checkin-time Gzip-stream + list status history annotate + +history -c -l -a -w ? + +rlist -e christof/mf +[-de] +cvs rls: Listing module: `christof/mf' +/slantsl.mf/1.1.1.1/Mon Nov 18 14:03:17 1996// +/steno.mf/1.1.1.1/Mon Nov 18 14:03:17 1996// + +rlist -l christof/mf +cvs rls: Listing module: `christof/mf' +---- 1996-11-18 14:03:17 +0000 1.1.1.1 slantsl.mf +---- 1996-11-18 14:03:17 +0000 1.1.1.1 steno.mf + +Root /usr/local/cvsroot +Valid-responses ok error Valid-requests Checked-in New-entry Checksum Copy-file Updated Created Update-existing Merged Patched Rcs-diff Mode Mod-time Removed Remove-entry Set-static-directory Clear-static-directory Set-sticky Clear-sticky Template Clear-template Notified Module-expansion Wrapper-rcsOption M Mbinary E F MT +valid-requests +UseUnchanged +Argument -d +Argument -P +Argument -u +Argument -- +Directory . +/usr/local/cvsroot/christof/tex +Entry /.cvsignore/1.1.1.1/// +Unchanged .cvsignore +Entry /c....tgz/1.1//-ko/ +Unchanged c...s.tgz +Directory a...um +/usr/local/cvsroot/christof/tex/a...um +Entry /te....tex/1.21/// +Modified te....tex +u=rw,g=rw,o=r +1762 +... +Questionable F...nst.txt +update + + +---- how + +Root +... +rlist -Red +... +feststellen, welche Versionen vermutlich importiert sind und welche fehlen +(nur timestamp) + +fehlende revisions/cmanifests mittels +rlog -d 'Jun 20 09:38:29 1997 revision_selectors; std::vector extra_rcfiles; path_set restrictions; @@ -63,6 +64,7 @@ void set_root(utf8 const & root); void set_message(utf8 const & message); void set_depth(long depth); + void set_since(utf8 const & since); void add_revision(utf8 const & selector); void set_stdhooks(bool b); --- commands.cc +++ commands.cc @@ -38,6 +38,7 @@ #include "update.hh" #include "vocab.hh" #include "work.hh" +#include "cvs_sync.hh" #include "automate.hh" #include "inodeprint.hh" #include "platform.hh" @@ -3831,6 +3832,48 @@ put_revision_id(null); } + +// missing: compression level (-z), cvs-branch (-r), since (-D) +CMD(cvs_pull, "network", "[CVS-REPOSITORY CVS-MODULE]", + "(re-)import a module from a remote cvs repository") +{ + + if (args.size() != 2 && args.size() != 0) throw usage(name); + + string repository,module; + if (args.size() == 2) + { repository = idx(args, 0)(); + module = idx(args, 1)(); + } + N(!app.branch_name().empty(), F("no destination branch specified\n")); + + cvs_sync::pull(repository,module,app); +} + + +CMD(cvs_push, "network", "CVS-REPOSITORY CVS-MODULE", + "commit changes in local database to a remote cvs repository") +{ + + if (args.size() != 2 && args.size() != 0) throw usage(name); + + string repository,module; + if (args.size() == 2) + { repository = idx(args, 0)(); + module = idx(args, 1)(); + } + cvs_sync::push(repository,module,app); +} + + +CMD(cvs_admin, "network", "COMMAND ARG", + "e.g. manifest REVISION") +{ + if (args.size() != 2) throw usage(name); + cvs_sync::admin(idx(args, 0)(), idx(args, 1)(), app); +} + + CMD(automate, "automation", "interface_version\n" "heads BRANCH\n" --- cryptopp/md5.cpp +++ cryptopp/md5.cpp @@ -0,0 +1,115 @@ +// md5.cpp - modified by Wei Dai from Colin Plumb's public domain md5.c +// any modifications are placed in the public domain + +#include "pch.h" +#include "md5.h" +#include "misc.h" + +NAMESPACE_BEGIN(CryptoPP) + +void MD5_TestInstantiations() +{ + MD5 x; +} + +void MD5::InitState(HashWordType *state) +{ + state[0] = 0x67452301L; + state[1] = 0xefcdab89L; + state[2] = 0x98badcfeL; + state[3] = 0x10325476L; +} + +void MD5::Transform (word32 *digest, const word32 *in) +{ +// #define F1(x, y, z) (x & y | ~x & z) +#define F1(x, y, z) (z ^ (x & (y ^ z))) +#define F2(x, y, z) F1(z, x, y) +#define F3(x, y, z) (x ^ y ^ z) +#define F4(x, y, z) (y ^ (x | ~z)) + +#define MD5STEP(f, w, x, y, z, data, s) \ + w = rotlFixed(w + f(x, y, z) + data, s) + x + + word32 a, b, c, d; + + a=digest[0]; + b=digest[1]; + c=digest[2]; + d=digest[3]; + + MD5STEP(F1, a, b, c, d, in[0] + 0xd76aa478, 7); + MD5STEP(F1, d, a, b, c, in[1] + 0xe8c7b756, 12); + MD5STEP(F1, c, d, a, b, in[2] + 0x242070db, 17); + MD5STEP(F1, b, c, d, a, in[3] + 0xc1bdceee, 22); + MD5STEP(F1, a, b, c, d, in[4] + 0xf57c0faf, 7); + MD5STEP(F1, d, a, b, c, in[5] + 0x4787c62a, 12); + MD5STEP(F1, c, d, a, b, in[6] + 0xa8304613, 17); + MD5STEP(F1, b, c, d, a, in[7] + 0xfd469501, 22); + MD5STEP(F1, a, b, c, d, in[8] + 0x698098d8, 7); + MD5STEP(F1, d, a, b, c, in[9] + 0x8b44f7af, 12); + MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17); + MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22); + MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7); + MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12); + MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17); + MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22); + + MD5STEP(F2, a, b, c, d, in[1] + 0xf61e2562, 5); + MD5STEP(F2, d, a, b, c, in[6] + 0xc040b340, 9); + MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14); + MD5STEP(F2, b, c, d, a, in[0] + 0xe9b6c7aa, 20); + MD5STEP(F2, a, b, c, d, in[5] + 0xd62f105d, 5); + MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9); + MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14); + MD5STEP(F2, b, c, d, a, in[4] + 0xe7d3fbc8, 20); + MD5STEP(F2, a, b, c, d, in[9] + 0x21e1cde6, 5); + MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9); + MD5STEP(F2, c, d, a, b, in[3] + 0xf4d50d87, 14); + MD5STEP(F2, b, c, d, a, in[8] + 0x455a14ed, 20); + MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5); + MD5STEP(F2, d, a, b, c, in[2] + 0xfcefa3f8, 9); + MD5STEP(F2, c, d, a, b, in[7] + 0x676f02d9, 14); + MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20); + + MD5STEP(F3, a, b, c, d, in[5] + 0xfffa3942, 4); + MD5STEP(F3, d, a, b, c, in[8] + 0x8771f681, 11); + MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16); + MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23); + MD5STEP(F3, a, b, c, d, in[1] + 0xa4beea44, 4); + MD5STEP(F3, d, a, b, c, in[4] + 0x4bdecfa9, 11); + MD5STEP(F3, c, d, a, b, in[7] + 0xf6bb4b60, 16); + MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23); + MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4); + MD5STEP(F3, d, a, b, c, in[0] + 0xeaa127fa, 11); + MD5STEP(F3, c, d, a, b, in[3] + 0xd4ef3085, 16); + MD5STEP(F3, b, c, d, a, in[6] + 0x04881d05, 23); + MD5STEP(F3, a, b, c, d, in[9] + 0xd9d4d039, 4); + MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11); + MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16); + MD5STEP(F3, b, c, d, a, in[2] + 0xc4ac5665, 23); + + MD5STEP(F4, a, b, c, d, in[0] + 0xf4292244, 6); + MD5STEP(F4, d, a, b, c, in[7] + 0x432aff97, 10); + MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15); + MD5STEP(F4, b, c, d, a, in[5] + 0xfc93a039, 21); + MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6); + MD5STEP(F4, d, a, b, c, in[3] + 0x8f0ccc92, 10); + MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15); + MD5STEP(F4, b, c, d, a, in[1] + 0x85845dd1, 21); + MD5STEP(F4, a, b, c, d, in[8] + 0x6fa87e4f, 6); + MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10); + MD5STEP(F4, c, d, a, b, in[6] + 0xa3014314, 15); + MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21); + MD5STEP(F4, a, b, c, d, in[4] + 0xf7537e82, 6); + MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10); + MD5STEP(F4, c, d, a, b, in[2] + 0x2ad7d2bb, 15); + MD5STEP(F4, b, c, d, a, in[9] + 0xeb86d391, 21); + + digest[0]+=a; + digest[1]+=b; + digest[2]+=c; + digest[3]+=d; +} + +NAMESPACE_END --- cryptopp/md5.h +++ cryptopp/md5.h @@ -0,0 +1,20 @@ +#ifndef CRYPTOPP_MD5_H +#define CRYPTOPP_MD5_H + +#include "iterhash.h" + +NAMESPACE_BEGIN(CryptoPP) + +//! MD5 +/*! 128 Bit Hash */ +class MD5 : public IteratedHashWithStaticTransform +{ +public: + static void InitState(HashWordType *state); + static void Transform(word32 *digest, const word32 *data); + static const char * StaticAlgorithmName() {return "MD5";} +}; + +NAMESPACE_END + +#endif --- cvs_client.cc +++ cvs_client.cc @@ -0,0 +1,1448 @@ +// copyright (C) 2005 Christof Petig +// all rights reserved. +// licensed to the public under the terms of the GNU GPL (>= 2) +// see the file COPYING for details + +#include +#include +#include +#include +#include +#include +#include +#include +#include "sanity.hh" +#include "cvs_client.hh" +#include + +// copied from netsync.cc from the ssh branch +static pid_t pipe_and_fork(int *fd1,int *fd2) +{ pid_t result=-1; + fd1[0]=-1; fd1[1]=-1; + fd2[0]=-1; fd2[1]=-1; +#ifndef __WIN32__ + if (pipe(fd1)) return -1; + if (pipe(fd2)) + { close(fd1[0]); close(fd1[1]); return -1; } + result=fork(); + if (result<0) + { close(fd1[0]); close(fd1[1]); + close(fd2[0]); close(fd2[1]); + return -1; + } + else if (!result) + { // fd1[1] for writing, fd2[0] for reading + close(fd1[0]); + close(fd2[1]); + if (dup2(fd2[0],0)!=0 || dup2(fd1[1],1)!=1) + { perror("dup2"); + exit(-1); // kill the useless child + } + close(fd1[1]); + close(fd2[0]); + } + else + { // fd1[0] for reading, fd2[1] for writing + close(fd1[1]); + close(fd2[0]); + } +#endif + return result; +} + +void cvs_client::writestr(const std::string &s, bool flush) +{ if (s.size()) L(F("writestr(%s") % s); // s mostly contains the \n char + if (!gzip_level) + { if (s.size() && byte_out_ticker.get()) + (*byte_out_ticker)+=write(writefd,s.c_str(),s.size()); + return; + } + char outbuf[1024]; + compress.next_in=(Bytef*)s.c_str(); + compress.avail_in=s.size(); + for (;;) + // the zlib headers say that avail_out is the only criterion for looping + { compress.next_out=(Bytef*)outbuf; + compress.avail_out=sizeof outbuf; + int err=deflate(&compress,flush?Z_SYNC_FLUSH:Z_NO_FLUSH); + if (err!=Z_OK && err!=Z_BUF_ERROR) + { throw oops("deflate error "+ boost::lexical_cast(err)); + } + unsigned written=sizeof(outbuf)-compress.avail_out; + if (written && byte_out_ticker.get()) + (*byte_out_ticker)+=write(writefd,outbuf,written); + else break; + } +} + +std::string cvs_client::readline() +{ // flush + writestr(std::string(),true); + + // read input + std::string result; + for (;;) + { if (inputbuffer.empty()) underflow(); + if (inputbuffer.empty()) throw oops("no data avail"); + std::string::size_type eol=inputbuffer.find('\n'); + if (eol==std::string::npos) + { result+=inputbuffer; + inputbuffer.clear(); + } + else + { result+=inputbuffer.substr(0,eol); + inputbuffer.erase(0,eol+1); + L(F("readline result '%s'\n") % result); + return result; + } + } +} + +std::string cvs_client::read_n(unsigned len) +{ // no flush necessary + std::string result; + while (len) + { if (inputbuffer.empty()) underflow(); + I(!inputbuffer.empty()); + unsigned avail=inputbuffer.size(); + if (len(err)); + } + unsigned bytes_in=sizeof(buf2)-decompress.avail_out; + if (bytes_in) inputbuffer+=std::string(buf2,buf2+bytes_in); + else break; + } + if (inputbuffer.empty()) goto try_again; +} + +// this mutable/const oddity is to avoid an +// "invalid initialization of non-const reference from a temporary" warning +// when passing this class to stringtok (we cheat by using a const reference) +template + class push_back2insert_cl +{ mutable Container &c; +public: + push_back2insert_cl(Container &_c) : c(_c) {} + template + void push_back(const T &t) const { c.insert(t); } +}; + +// the creator function (so you don't need to specify the type +template + const push_back2insert_cl push_back2insert(Container &c) +{ return push_back2insert_cl(c); +} + +// inspired by code from Marcelo E. Magallon and the libstdc++ doku +template +void +stringtok (Container &container, std::string const &in, + const char * const delimiters = " \t\n") +{ + const std::string::size_type len = in.length(); + std::string::size_type i = 0; + + while ( i < len ) + { + // eat leading whitespace + // i = in.find_first_not_of (delimiters, i); + // if (i == std::string::npos) + // return; // nothing left but white space + + // find the end of the token + std::string::size_type j = in.find_first_of (delimiters, i); + + // push token + if (j == std::string::npos) { + container.push_back (in.substr(i)); + return; + } else + container.push_back (in.substr(i, j-i)); + + // set up for next loop + i = j + 1; + } +} + +// " AA " s=2 e=3 +std::string trim(const std::string &s) +{ std::string::size_type start=s.find_first_not_of(" "); + if (start==std::string::npos) return std::string(); + std::string::size_type end=s.find_last_not_of(" "); + if (end==std::string::npos) end=s.size(); + else ++end; + return s.substr(start,end-start); +} + +void cvs_client::SendCommand(const char *cmd,...) +{ va_list ap; + va_start(ap, cmd); + SendCommand(cmd,ap); + va_end(ap); +} + +void cvs_client::SendCommand(const char *cmd,va_list ap) +{ const char *arg; + while ((arg=va_arg(ap,const char *))) + { writestr("Argument "+std::string(arg)+"\n"); + } + writestr(cmd+std::string("\n")); +} + +bool cvs_client::begins_with(const std::string &s, const std::string &sub, unsigned &len) +{ std::string::size_type sublen=sub.size(); + if (s.size()", 256)); + byte_out_ticker.reset(new ticker("bytes out", "<", 256)); + + memset(&compress,0,sizeof compress); + memset(&decompress,0,sizeof decompress); + + if (pserver) + { // it looks like I run into the same problems on Win32 again and again: + // pipes and sockets, so this is not portable except by using the + // Netxx::PipeStream from the ssh branch ... postponed + static const int pserver_port=2401; + writefd = socket(PF_INET, SOCK_STREAM, 0); + I(writefd>=0); + struct hostent *ptHost = gethostbyname(host.c_str()); + if (!ptHost) + { L(F("Can't find address for host %s\n") % host); + throw oops("gethostbyname " + host + " failed"); + } + struct sockaddr_in tAddr; + tAddr.sin_family = AF_INET; + tAddr.sin_port = htons(pserver_port); + tAddr.sin_addr = *(struct in_addr *)(ptHost->h_addr); + if (::connect(writefd, (struct sockaddr*)&tAddr, sizeof(tAddr))) + { L(F("Can't connect to port %d on %s\n") % pserver_port % host); + throw oops("connect to port "+boost::lexical_cast(pserver_port) + +" on "+host+" failed"); + } + readfd=writefd; + fcntl(readfd,F_SETFL,fcntl(readfd,F_GETFL)|O_NONBLOCK); + writestr("BEGIN AUTH REQUEST\n"); + writestr(root+"\n"); + writestr(user+"\n"); + writestr(pserver_password(":pserver:"+user+"@"+host+":"+root)+"\n"); + writestr("END AUTH REQUEST\n"); + std::string answer=readline(); + if (answer!="I LOVE YOU") + { L(F("pserver Authentification failed\n")); + throw oops("pserver auth failed: "+answer); + } + } + else // rsh + { std::string localhost_name; + { // get localhost's name + char domainname[1024]; + *domainname=0; + if (gethostname(domainname,sizeof domainname)) + throw oops("gethostname "+std::string(strerror(errno))); + domainname[sizeof(domainname)-1]=0; + unsigned len=strlen(domainname); + if (len && len(newargv)); + perror(newargv[0]); + exit(errno); + } + readfd=fd1[0]; + writefd=fd2[1]; + fcntl(readfd,F_SETFL,fcntl(readfd,F_GETFL)|O_NONBLOCK); + + if (host.empty()) host=localhost_name; + } + + InitZipStream(0); + writestr("Root "+root+"\n"); + writestr("Valid-responses ok error Valid-requests Checked-in " + "New-entry Checksum Copy-file Updated Created Update-existing " + "Merged Patched Rcs-diff Mode Mod-time Removed Remove-entry " + "Set-static-directory Clear-static-directory Set-sticky " + "Clear-sticky Template Clear-template Notified Module-expansion " + "Wrapper-rcsOption M Mbinary E F MT\n"); + + writestr("valid-requests\n"); + std::string answer=readline(); + I(begins_with(answer,"Valid-requests ")); + // boost::tokenizer does not provide the needed functionality (e.g. preserve -) + stringtok(push_back2insert(Valid_requests),answer.substr(15)); + answer=readline(); + I(answer=="ok"); + + I(CommandValid("UseUnchanged")); + writestr("UseUnchanged\n"); + + writestr("Global_option -q\n"); // -Q? +} + +void cvs_client::drop_connection() +{ byte_in_ticker.reset(); + byte_out_ticker.reset(); + deflateEnd(&compress); + inflateEnd(&decompress); + gzip_level=0; + if (readfd!=-1) + { close(readfd); readfd=-1; } + if (writefd!=-1) + { close(writefd); writefd=-1; } +} + +cvs_client::~cvs_client() +{ drop_connection(); +} + +void cvs_client::reconnect() +{ drop_connection(); + connect(); +} + +void cvs_client::InitZipStream(int level) +{ int error=deflateInit(&compress,level); + if (error!=Z_OK) throw oops("deflateInit "+boost::lexical_cast(error)); + error=inflateInit(&decompress); + if (error!=Z_OK) throw oops("inflateInit "+boost::lexical_cast(error)); +} + +void cvs_client::GzipStream(int level) +{ if (!CommandValid("Gzip-stream")) return; + std::string cmd="Gzip-stream "; + cmd+=char('0'+level); + cmd+='\n'; + writestr(cmd); + int error=deflateParams(&compress,level,Z_DEFAULT_STRATEGY); + if (error!=Z_OK) throw oops("deflateParams "+boost::lexical_cast(error)); + gzip_level=level; +} + +bool cvs_client::fetch_result(std::string &result) +{ std::vector > res; + if (!fetch_result(res) || res.empty()) return false; + result=combine_result(res); + return true; +} + +std::string cvs_client::combine_result(const std::vector > &res) +{ if (res.empty()) return std::string(); + // optimized for the single entry case + std::vector >::const_iterator i=res.begin(); + std::string result=i->second; + for (++i;i!=res.end();++i) result+=i->second; + return result; +} + +bool cvs_client::fetch_result(std::vector > &result) +{ result.clear(); + std::list active_tags; +loop: + std::string x=readline(); + unsigned len=0; + if (x.size()<2) goto error; + if (begins_with(x,"E ",len)) + { W(F("%s\n") % x.substr(len)); + goto loop; + } + if (begins_with(x,"M ",len)) + { result.push_back(std::make_pair(std::string(),x.substr(len))); + return true; + } + if (active_tags.empty() && x=="MT newline") return true; + if (begins_with(x,"MT ",len)) + { if (x[len]=='+') + { active_tags.push_back(x.substr(len+1)); + result.push_back(std::make_pair(std::string(),x.substr(len))); + goto loop; + } + if (x[len]=='-') + { I(!active_tags.empty()); + I(active_tags.back()==x.substr(len+1)); + active_tags.pop_back(); + result.push_back(std::make_pair(std::string(),x.substr(len))); + if (active_tags.empty()) return true; + goto loop; + } + std::string::size_type sep=x.find_first_of(" ",len); + if (sep==std::string::npos) + result.push_back(std::make_pair(std::string(),x.substr(len))); + else + result.push_back(std::make_pair(x.substr(len,sep-len),x.substr(sep+1))); + goto loop; + } + if (x=="ok") return false; + if (!result.empty()) goto error; + // more complex results + if (begins_with(x,"Clear-sticky ",len) + || begins_with(x,"Set-static-directory ",len) + || begins_with(x,"Clear-static-directory ",len) + || begins_with(x,"Clear-template ",len) + || begins_with(x,"Removed ",len) + || begins_with(x,"Remove-entry ",len)) + { result.push_back(std::make_pair("CMD",x.substr(0,len-1))); + result.push_back(std::make_pair("dir",x.substr(len))); + result.push_back(std::make_pair("rcs",readline())); + return true; + } + if (begins_with(x,"Mod-time ",len)) + { result.push_back(std::make_pair("CMD",x.substr(0,len-1))); + result.push_back(std::make_pair("date",x.substr(len))); + return true; + } + if (begins_with(x,"Mode ",len)) + { result.push_back(std::make_pair("CMD",x.substr(0,len-1))); + result.push_back(std::make_pair("mode",x.substr(len))); + return true; + } + if (begins_with(x,"Copy-file ",len)) + { result.push_back(std::make_pair("CMD",x.substr(0,len-1))); + result.push_back(std::make_pair("dir",x.substr(len))); + result.push_back(std::make_pair("file",readline())); + result.push_back(std::make_pair("new-file",readline())); + return true; + } + if (begins_with(x,"Checksum ",len)) + { result.push_back(std::make_pair("CMD",x.substr(0,len-1))); + result.push_back(std::make_pair("data",x.substr(len))); + return true; + } + if (begins_with(x,"Module-expansion ",len)) + { result.push_back(std::make_pair("CMD",x.substr(0,len-1))); + result.push_back(std::make_pair("dir",x.substr(len))); + return true; + } + if (begins_with(x,"Checked-in ",len)) + { result.push_back(std::make_pair("CMD",x.substr(0,len-1))); + result.push_back(std::make_pair("dir",x.substr(len))); + result.push_back(std::make_pair("rcs",readline())); + result.push_back(std::make_pair("new entries line",readline())); + return true; + } + if (begins_with(x,"Set-sticky ",len)) + { result.push_back(std::make_pair("CMD",x.substr(0,len-1))); + result.push_back(std::make_pair("dir",x.substr(len))); + result.push_back(std::make_pair("rcs",readline())); + result.push_back(std::make_pair("tag",readline())); + return true; + } + if (begins_with(x,"Created ",len) || begins_with(x,"Update-existing ",len) + || begins_with(x,"Rcs-diff ",len) || begins_with(x,"Merged ",len)) + { result.push_back(std::make_pair("CMD",x.substr(0,len-1))); + result.push_back(std::make_pair("dir",x.substr(len))); + result.push_back(std::make_pair("rcs",readline())); + result.push_back(std::make_pair("new entries line",readline())); + result.push_back(std::make_pair("mode",readline())); + std::string length=readline(); + result.push_back(std::make_pair("length",length)); + result.push_back(std::make_pair("data",read_n(atol(length.c_str())))); + return true; + } + if (x=="error ") + { result.push_back(std::make_pair("CMD","error")); + return true; + } + +error: + W(F("unhandled server response \"%s\"\n") % x); + exit(1); +} + +static time_t timezone2time_t(const struct tm &tm, int offset_min) +{ I(!offset_min); + time_t result=-1; +#if 0 // non portable + result=timegm(&tm); +#else // ugly + const char *tz=getenv("TZ"); + setenv("TZ","",true); + tzset(); + result=mktime(const_cast(&tm)); + if (tz) setenv("TZ", tz, true); + else unsetenv("TZ"); + tzset(); +#endif +// L(F("result %ld\n") % result); + return result; +} + +static time_t cvs111date2time_t(const std::string &t) +{ // 2000/11/10 14:43:25 + I(t.size()==19); + I(t[4]=='/' && t[7]=='/'); + I(t[10]==' ' && t[13]==':'); + I(t[16]==':'); + struct tm tm; + memset(&tm,0,sizeof tm); + tm.tm_year=atoi(t.substr(0,4).c_str())-1900; + tm.tm_mon=atoi(t.substr(5,2).c_str())-1; + tm.tm_mday=atoi(t.substr(8,2).c_str()); + tm.tm_hour=atoi(t.substr(11,2).c_str()); + tm.tm_min=atoi(t.substr(14,2).c_str()); + tm.tm_sec=atoi(t.substr(17,2).c_str()); + // on my debian/woody server (1.11) this is UTC ... + return timezone2time_t(tm,0); +} + +static time_t rls_l2time_t(const std::string &t) +{ // 2003-11-26 09:20:57 +0000 + I(t.size()==25); + I(t[4]=='-' && t[7]=='-'); + I(t[10]==' ' && t[13]==':'); + I(t[16]==':' && t[19]==' '); + I(t[20]=='+' || t[20]=='-'); + struct tm tm; + memset(&tm,0,sizeof tm); + tm.tm_year=atoi(t.substr(0,4).c_str())-1900; + tm.tm_mon=atoi(t.substr(5,2).c_str())-1; + tm.tm_mday=atoi(t.substr(8,2).c_str()); + tm.tm_hour=atoi(t.substr(11,2).c_str()); + tm.tm_min=atoi(t.substr(14,2).c_str()); + tm.tm_sec=atoi(t.substr(17,2).c_str()); + int dst_offs=atoi(t.substr(20,5).c_str()); +// L(F("%d-%d-%d %d:%02d:%02d %04d") % tm.tm_year % tm.tm_mon % tm.tm_mday +// % tm.tm_hour % tm.tm_min % tm.tm_sec % dst_offs ); + tm.tm_isdst=0; + return timezone2time_t(tm,dst_offs); +} + +// third format: +// 19 Nov 1996 11:22:50 -0000 +// 4 Jun 1999 12:00:41 -0000 +// 19 Jul 2002 07:33:26 -0000 + +// Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec +// Apr,Aug,Dec,Feb,Jan,Jul,Jun,Mar,May,Nov,Oct,Sep +static time_t monname2month(const std::string &x) +{ I(x.size()==3); + // I hope this will never get internationalized + if (x[0]=='O') return 10; + if (x[0]=='S') return 9; + if (x[0]=='D') return 12; + if (x[0]=='F') return 2; + if (x[0]=='N') return 11; + if (x[0]=='A') return x[1]=='p'?4:8; + if (x[0]=='M') return x[2]=='r'?3:5; + I(x[0]=='J'); + return x[1]=='a'?1:(x[2]=='n'?6:7); + return 0; +} + +static time_t mod_time2time_t(const std::string &t) +{ std::vector parts; + stringtok(parts,t); + I(parts.size()==5); + struct tm tm; + memset(&tm,0,sizeof tm); + I(parts[3][2]==':' && parts[3][5]==':'); + I(parts[4][0]=='+' || parts[4][0]=='-'); + tm.tm_year=atoi(parts[2].c_str())-1900; + tm.tm_mon=monname2month(parts[1])-1; + tm.tm_mday=atoi(parts[0].c_str()); + tm.tm_hour=atoi(parts[3].substr(0,2).c_str()); + tm.tm_min=atoi(parts[3].substr(3,2).c_str()); + tm.tm_sec=atoi(parts[3].substr(6,2).c_str()); + int dst_offs=atoi(parts[4].c_str()); + tm.tm_isdst=0; + return timezone2time_t(tm,dst_offs); +} + +std::string cvs_client::time_t2rfc822(time_t t) +{ static const char * const months[12] = + {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}; + struct tm *tm=gmtime(&t); + I(tm); + return (F("%02d %s %d %02d:%02d:%02d +0000") + % tm->tm_mday % months[tm->tm_mon] % (tm->tm_year+1900) + % tm->tm_hour % tm->tm_min % tm->tm_sec).str(); +} + +void cvs_client::Directory(const std::string &path) +{ if (path.empty() || path==".") // ??? + { std::map::const_iterator i=server_dir.find(""); + I(i!=server_dir.end()); + writestr("Directory .\n"+i->second+"\n"); + } + else + { std::map::reverse_iterator i; + std::string path_with_slash=path+"/"; + unsigned len=0; + for (i=server_dir.rbegin();i!=server_dir.rend();++i) + { if (begins_with(path_with_slash,i->first,len)) break; + } + I(!server_dir.empty()); +// if (i==server_dir.rend()) { --i; len=0; } // take the last one + I(i!=server_dir.rend()); +// if (path[len]=='/') ++len; + I(!i->second.empty()); + I(i->second[i->second.size()-1]=='/'); + std::string rcspath=i->second; + if (len > lresult; + enum { st_dir, st_file } state=st_dir; + std::string directory; + while (fetch_result(lresult)) + { switch(state) + { case st_dir: + { std::string result=combine_result(lresult); + I(result.size()>=2); + I(result[result.size()-1]==':'); + directory=result.substr(0,result.size()-1); + state=st_file; + break; + } + case st_file: + if (lresult.empty() || lresult[0].second.empty()) state=st_dir; + else + { I(lresult.size()==3); + I(lresult[0].first=="text"); + I(lresult[1].first=="date"); + I(lresult[2].first=="text"); + std::string keyword=trim(lresult[0].second); + std::string date=trim(lresult[1].second); + std::string version=trim(lresult[2].second.substr(1,10)); + std::string dead=trim(lresult[2].second.substr(12,4)); + std::string name=lresult[2].second.substr(17); + + I(keyword[0]=='-' || keyword[0]=='d'); + I(dead.empty() || dead=="dead"); + I(!name.empty()); + + if (keyword=="----") keyword.clear(); + if (keyword!="d---") + { //std::cerr << (directory+"/"+name) << " V" + // << version << " from " << date << " " << dead + // << " " << keyword << '\n'; + time_t t=rls_l2time_t(date); + cb.file(directory+"/"+name,t,version,!dead.empty()); + } + // construct manifest + // search for a matching revision + // - do that later when all files are known ??? + } + break; + } + } +} + +static std::string basename(const std::string &s) +{ std::string::size_type lastslash=s.rfind("/"); + if (lastslash==std::string::npos) return s; + return s.substr(lastslash+1); +} + +static std::string dirname(const std::string &s) +{ std::string::size_type lastslash=s.rfind("/"); + if (lastslash==std::string::npos) return "."; + if (!lastslash) return "/"; + return s.substr(0,lastslash); +} + +void cvs_client::Log_internal(const rlog_callbacks &cb,const std::string &file,va_list ap) +{ Directory(dirname(std::string(file))); + std::string bname=basename(std::string(file)); + writestr("Entry /"+bname+"/1.1.1.1//-kb/\n"); + writestr("Unchanged "+bname+"\n"); + { const char *arg; + while ((arg=va_arg(ap,const char *))) + { writestr("Argument "+std::string(arg)+"\n"); + } + } + writestr("Argument --\n" + "Argument "+bname+"\n" + "log\n"); + processLogOutput(cb); +} + +void cvs_client::Log(const rlog_callbacks &cb,const char *file,...) +{ primeModules(); + va_list ap,ap2; + va_start(ap,file); + va_copy(ap2,ap); + try { + Log_internal(cb,file,ap); + } catch (oops &e) + { W(F("trying to reconnect, perhaps the server is confused\n")); + reconnect(); + Log_internal(cb,file,ap2); + } + va_end(ap); +} + +// dummy is needed to satisfy va_start (cannot pass objects of non-POD type) +void cvs_client::RLog(const rlog_callbacks &cb,bool dummy,...) +{ primeModules(); + { va_list ap; + va_start(ap,dummy); + SendCommand("rlog",ap); + va_end(ap); + } + processLogOutput(cb); +} + +void cvs_client::processLogOutput(const rlog_callbacks &cb) +{ + static const char * const fileend="============================================================================="; + static const char * const revisionend="----------------------------"; + enum { st_head, st_tags, st_desc, st_rev, st_msg, st_date_author + } state=st_head; + std::vector > lresult; + std::string file; + std::string revision,head_rev; + std::string message; + std::string author; + std::string description; + std::string dead; + time_t checkin_time=0; + while (fetch_result(lresult)) + {reswitch: + L(F("state %d\n") % int(state)); + I(!lresult.empty()); + if (lresult[0].first=="CMD" && lresult[0].second=="error") + { throw oops("log failed"); + } + switch(state) + { case st_head: + { std::string result=combine_result(lresult); + unsigned len; + if (result.empty()) break; // accept a (first) empty line + if (result==fileend) + { cb.file(file,head_rev); + } + else if (begins_with(result,"RCS file: ",len)) + { file=rcs_file2path(result.substr(len)); + } + else if (begins_with(result,"head: ",len)) + { head_rev=result.substr(len); + } + else if (begins_with(result,"branch:") || + begins_with(result,"locks: ") || + begins_with(result,"access list:") || + begins_with(result,"keyword substitution: ") || + begins_with(result,"Working file: ") || + begins_with(result,"total revisions: ")) + ; + else if (result=="description:") + { state=st_desc; + description.clear(); + } + else if (result=="symbolic names:") + state=st_tags; + else + { W(F("unknown rcs head '%s'\n") % result); + } + break; + } + case st_tags: + { std::string result=combine_result(lresult); + I(!result.empty()); + if (result[0]!='\t') + { L(F("result[0] %d %d\n") % result.size() % int(result[0])); state=st_head; goto reswitch; } + I(result.find_first_not_of("\t ")==1); + std::string::size_type colon=result.find(':'); + I(colon!=std::string::npos); + cb.tag(file,result.substr(1,colon-1),result.substr(colon+2)); + break; + } + case st_desc: + { std::string result=combine_result(lresult); + if (result==revisionend) + { state=st_rev; + // cb.file(file,description); + } + else + { if (!description.empty()) description+='\n'; + description+=result; + } + break; + } + case st_rev: + { std::string result=combine_result(lresult); + unsigned len=0; + I(begins_with(result,"revision ",len)); + revision=result.substr(len); + state=st_date_author; + break; + } + case st_date_author: + { if (lresult.size()==1) // M ... (cvs 1.11.1p1) + { std::string result=combine_result(lresult); + unsigned len=0; + I(begins_with(result,"date: ",len)); + std::string::size_type authorpos=result.find("; author: ",len); + I(authorpos!=std::string::npos); + std::string::size_type authorbegin=authorpos+11; + std::string::size_type statepos=result.find("; state: ",authorbegin); + I(statepos!=std::string::npos); + std::string::size_type statebegin=statepos+10; + std::string::size_type linespos=result.find(";",statebegin); + // "; lines: " + I(linespos!=std::string::npos); + checkin_time=cvs111date2time_t(result.substr(len,authorpos-len)); + author=result.substr(authorbegin,statepos-authorbegin); + dead=result.substr(statebegin,linespos-statebegin); + } + else // MT ... (cvs 1.12.9) + { I(lresult.size()==11 || lresult.size()==7); + I(lresult[0].first=="text"); + I(lresult[0].second=="date: "); + I(lresult[1].first=="date"); + checkin_time=rls_l2time_t(lresult[1].second); + I(lresult[2].first=="text"); + I(lresult[2].second=="; author: "); + I(lresult[3].first=="text"); + author=lresult[3].second; + I(lresult[4].first=="text"); + I(lresult[4].second=="; state: "); + I(lresult[5].first=="text"); + dead=lresult[5].second; + } + state=st_msg; + message.clear(); + break; + } + case st_msg: + { std::string result=combine_result(lresult); + // evtl überprüfen, ob das nicht nur ein fake war ... + if (result==revisionend || result==fileend) + { cb.revision(file,checkin_time,revision,author,dead,message); + if (result==fileend) + { state=st_head; + goto reswitch; // emit file cb + } + state=st_rev; + } + else + { if (!message.empty()) message+='\n'; + message+=result; + } + break; + } + } + } +} + +cvs_client::checkout cvs_client::CheckOut(const std::string &_file, const std::string &revision) +{ primeModules(); + std::string file=_file; + struct checkout result; + // Directory(""); + std::string usemodule=module; + { std::map::reverse_iterator i; + unsigned len=0; + for (i=server_dir.rbegin();i!=server_dir.rend();++i) + { if (begins_with(file,i->first,len)) break; + } + I(i!=server_dir.rend()); + if (!i->first.empty()) + { usemodule=i->first; + if (usemodule[usemodule.size()-1]=='/') + usemodule.erase(usemodule.size()-1,1); + usemodule=basename(usemodule); + file.erase(0,i->first.size()); + L(F("usemodule %s @%s %s /%s\n") % _file % i->first % usemodule % file); + } + } + SendCommand("co",/*"-N","-P",*/"-r",revision.c_str(),"--",(usemodule+"/"+file).c_str(),(void*)0); + enum { st_co + } state=st_co; + std::vector > lresult; + std::string dir,dir2,rcsfile; + while (fetch_result(lresult)) + { switch(state) + { case st_co: + { I(!lresult.empty()); + if (lresult[0].first=="CMD") + { if (lresult[0].second=="Clear-sticky") + { I(lresult.size()==3); + I(lresult[1].first=="dir"); + dir=lresult[1].second; + } + else if (lresult[0].second=="Set-static-directory") + { I(lresult.size()==3); + I(lresult[1].first=="dir"); + dir2=lresult[1].second; + } + else if (lresult[0].second=="Remove-entry" + || lresult[0].second=="Removed") + { I(lresult.size()==3); + result.dead=true; + } + else if (lresult[0].second=="Mod-time") + { I(lresult.size()==2); + I(lresult[1].first=="date"); + // this is 18 Nov 1996 14:39:40 -0000 format - strange ... + result.mod_time=mod_time2time_t(lresult[1].second); + } + else if (lresult[0].second=="Created" + || lresult[0].second=="Update-existing") + // Update-existing results after crossing a dead state + { // std::cerr << combine_result(lresult) << '\n'; + I(lresult.size()==7); + I(lresult[6].first=="data"); + I(lresult[3].first=="new entries line"); + std::string new_revision; + parse_entry(lresult[3].second,new_revision,result.keyword_substitution); + result.mode=lresult[4].second; + result.contents=lresult[6].second; + L(F("file %s revision %s: %d bytes\n") % file + % revision % lresult[6].second.size()); + } + else if (lresult[0].second=="error") + { throw oops("failed to check out "+file); + } + else + { W(F("CheckOut: unrecognized CMD %s\n") % lresult[0].second); + } + } + else if (lresult[0].second=="+updated") + { // std::cerr << combine_result(lresult) << '\n'; + } + else + { W(F("CheckOut: unrecognized response %s\n") % lresult[0].second); + } + break; + } + } + } + return result; +} + +std::string cvs_client::pserver_password(const std::string &root) +{ const char *home=getenv("HOME"); + if (!home) home=""; + std::ifstream fs((std::string(home)+"/.cvspass").c_str()); + while (fs.good()) + { char buf[1024]; + if (fs.getline(buf,sizeof buf).good()) + { std::string line=buf; + if (line.substr(0,3)=="/1 ") line.erase(0,3); + if (line.size()>=root.size()+2 && begins_with(line,root) + && line[root.size()]==' ') + return line.substr(root.size()+1); + } + } + return "A"; // empty password +} + +std::string cvs_client::shorten_path(const std::string &p) const +{ unsigned len=0; + if (cvs_client::begins_with(p,module,len)) + { if (p[len]=='/') ++len; + return p.substr(len); + } + return p; +} + +std::string cvs_client::rcs_file2path(std::string file) const +{ // try to guess a sane file name (e.g. on cvs.gnome.org) + for (std::map::const_reverse_iterator i=server_dir.rbegin(); + i!=server_dir.rend();++i) + { if (begins_with(file,i->second)) + { file.replace(0,i->second.size(),i->first); + break; + } + } + if (file.size()>2 && file.substr(file.size()-2)==",v") file.erase(file.size()-2,2); + std::string::size_type lastslash=file.rfind('/'); + if (lastslash!=std::string::npos && lastslash>=5 + && file.substr(lastslash-5,6)=="Attic/") + file.erase(lastslash-5,6); + return file; +} + +namespace { +struct store_here : cvs_client::update_callbacks +{ cvs_client::update &store; + store_here(cvs_client::update &s) : store(s) {} + virtual void operator()(const cvs_client::update &u) const + { const_cast(store)=u; + } +}; +} + +cvs_client::update cvs_client::Update(const std::string &file, + const std::string &old_revision, const std::string &new_revision, + const std::string &keyword_expansion) +{ + struct update result; + std::vector file_revision; + file_revision.push_back(update_args(file,old_revision,new_revision,keyword_expansion)); + Update(file_revision,store_here(result)); + return result; +} + +// we have to update, status will give us only strange strings (and uses too +// much bandwidth?) [is too verbose] +void cvs_client::Update(const std::vector &file_revisions, + const update_callbacks &cb) +{ primeModules(); + struct update result; + I(!file_revisions.empty()); + std::string olddir; + for (std::vector::const_iterator i=file_revisions.begin(); + i!=file_revisions.end(); ++i) + { if (dirname(i->file)!=olddir) + { olddir=dirname(i->file); + Directory(olddir); + } + std::string bname=basename(i->file); + writestr("Entry /"+bname+"/"+i->old_revision+"//"+i->keyword_substitution+"/\n"); + writestr("Unchanged "+bname+"\n"); + } + if (file_revisions.size()==1 && !file_revisions.begin()->new_revision.empty()) + { + SendCommand("update","-d","-C","-u", + "-r",file_revisions.begin()->new_revision.c_str(), + "--",basename(file_revisions.begin()->file).c_str(),(void*)0); + } + else + { // needed for 1.11 + Directory("."); + SendCommand("update","-d","-C","-u",(void*)0); + } + std::vector > lresult; + std::string dir,dir2,rcsfile; + enum { st_normal, st_merge } state=st_normal; + + std::vector bugged; + while (fetch_result(lresult)) + { I(!lresult.empty()); + unsigned len=0; + if (lresult[0].first=="CMD") + { if (lresult[0].second=="Created" || lresult[0].second=="Update-existing") + { I(lresult.size()==7); + I(lresult[6].first=="data"); + dir=lresult[1].second; + result.file=lresult[2].second; + I(!result.file.empty()); + if (result.file[0]=='/') result.file=rcs_file2path(result.file); + result.contents=lresult[6].second; + parse_entry(lresult[3].second,result.new_revision,result.keyword_substitution); + cb(result); + result=update(); + state=st_normal; + } + else if (lresult[0].second=="Rcs-diff") + { I(lresult.size()==7); + I(lresult[6].first=="data"); + dir=lresult[1].second; + result.file=lresult[2].second; + I(!result.file.empty()); + if (result.file[0]=='/') result.file=rcs_file2path(result.file); + result.patch=lresult[6].second; + parse_entry(lresult[3].second,result.new_revision,result.keyword_substitution); + cb(result); + result=update(); + state=st_normal; + } + else if (lresult[0].second=="Checksum") + { I(lresult.size()==2); + I(lresult[1].first=="data"); + result.checksum=lresult[1].second; + } + else if (lresult[0].second=="Removed") + { I(lresult.size()==3); + result.file=lresult[2].second; + I(!result.file.empty()); + if (result.file[0]=='/') result.file=rcs_file2path(result.file); + result.removed=true; + cb(result); + result=update(); + state=st_normal; + } + else if (lresult[0].second=="Clear-static-directory" + || lresult[0].second=="Clear-template" + || lresult[0].second=="Clear-sticky") + { + } + else if (lresult[0].second=="Copy-file") + { I(state==st_merge); + } + else if (lresult[0].second=="Mod-time") + { result.mod_time=mod_time2time_t(lresult[1].second); + } + else if (lresult[0].second=="Merged") + { I(state==st_merge); + I(lresult.size()==7); + I(lresult[6].first=="data"); + dir=lresult[1].second; + result.file=lresult[2].second; + I(!result.file.empty()); + if (result.file[0]=='/') result.file=rcs_file2path(result.file); + result.contents=lresult[6].second; // strictly this is unnecessary ... + parse_entry(lresult[3].second,result.new_revision,result.keyword_substitution); + W(F("Update ->%s of %s exposed CVS bug\n") + % result.new_revision % result.file); + bugged.push_back(update_args(result.file,std::string(), + result.new_revision,result.keyword_substitution)); + result=update(); + state=st_normal; + } + else if (lresult[0].second=="error") + { I(state==st_merge); + break; + } + else + { W(F("Update: unrecognized CMD %s\n") % lresult[0].second); + } + } + else if (lresult[0].second=="+updated") + { // std::cerr << combine_result(lresult) << '\n'; + state=st_normal; + } + else if (lresult[0].second=="P ") + { // std::cerr << combine_result(lresult) << '\n'; + I(lresult.size()==2); + I(lresult[1].first=="fname"); + } + else if (lresult[0].second=="M ") + { I(lresult.size()==2); + I(lresult[1].first=="fname"); + state=st_merge; + } + else if (lresult[0].second=="? ") + { I(lresult.size()==2); + I(lresult[1].first=="fname"); + W(F("cvs erraneously reports ? %s\n") % lresult[1].second); + } + else if (begins_with(lresult[0].second,"RCS file: ",len)) + { I(state==st_normal); + state=st_merge; + } + else if (begins_with(lresult[0].second,"retrieving revision ",len)) + { I(state==st_merge); + } + else if (begins_with(lresult[0].second,"Merging ",len)) + { I(state==st_merge); + } + else if (begins_with(lresult[0].second,"C ",len)) + { state=st_merge; + I(lresult.size()==2); + I(lresult[1].first=="fname"); + } + else + { W(F("Update: unrecognized response %s\n") % lresult[0].second); + } + } + +// cater for encountered bugs ... + for (std::vector::const_iterator i=bugged.begin(); + i!=bugged.end();++i) + { result=update(); + checkout result2=CheckOut(i->file,i->new_revision); + result.contents=result2.contents; + result.patch=std::string(); + result.checksum=std::string(); + result.removed=result2.dead; + result.new_revision=i->new_revision; + result.keyword_substitution=result2.keyword_substitution; + result.file=i->file; + cb(result); + } +} + +void cvs_client::parse_entry(const std::string &line, std::string &new_revision, + std::string &keyword_substitution) +{ + std::vector parts; + stringtok(parts,line,"/"); + // empty last part will not get created + if (parts.size()==5) parts.push_back(std::string()); + I(parts.size()==6); + new_revision=parts[2]; + keyword_substitution=parts[4]; +} + +std::map > + cvs_client::Commit(const std::string &changelog, time_t when, + const std::vector &commits) +{ primeModules(); + std::string olddir; + I(!commits.empty()); +// undocumented ... +// if (CommandValid("Command-prep")) writestr("Command-prep commit\n"); + for (std::vector::const_iterator i=commits.begin(); + i!=commits.end(); ++i) + { if (dirname(i->file)!=olddir) + { olddir=dirname(i->file); + Directory(olddir); + } + std::string bname=basename(i->file); + writestr("Entry /"+bname+"/"+(i->removed?"-":"") + +i->old_revision+"//"+i->keyword_substitution+"/\n"); + if (!i->removed) + { writestr("Checkin-time "+time_t2rfc822(when)+"\n"); + writestr("Modified "+bname+"\n"); + writestr("u=rw,g=r,o=r\n"); // standard mode + writestr((F("%d\n") % i->new_content.size()).str()); + writestr(i->new_content); + } + } + Directory("."); + writestr("Argument -m\n"); + SendArgument(changelog); + writestr("Argument --\n"); + for (std::vector::const_iterator i=commits.begin(); + i!=commits.end(); ++i) + writestr("Argument "+shorten_path(i->file)+"\n"); + writestr("ci\n"); + std::map > result; + // process result + std::vector > lresult; + + while (fetch_result(lresult)) + { I(!lresult.empty()); + unsigned len=0; + if (lresult[0].first=="CMD") + { if (lresult[0].second=="Mode") + ; // who cares + else if (lresult[0].second=="Checked-in") + { I(lresult.size()==4); + I(lresult[2].first=="rcs"); + I(lresult[3].first=="new entries line"); + std::pair p; + std::string file=lresult[2].second; + I(!file.empty()); + if (file[0]=='/') file=rcs_file2path(file); + parse_entry(lresult[3].second,p.first,p.second); + result[file]=p; + } + else if (lresult[0].second=="Remove-entry") + { I(lresult.size()==3); + I(lresult[2].first=="rcs"); + std::string file=lresult[2].second; + I(!file.empty()); + if (file[0]=='/') file=rcs_file2path(file); + result[file]=std::make_pair(std::string(),std::string()); + } + else if (lresult[0].second=="error") + return std::map >(); + else + { W(F("Commit: unrecognized CMD %s\n") % lresult[0].second); + } + } + else if (lresult[0].second.empty()) + { I(!lresult[0].second.empty()); + } + else if (lresult[0].second[0]=='/') + // /cvsroot/test/F,v <-- F + { L(F("%s\n") % lresult[0].second); + } + else if (begins_with(lresult[0].second,"new revision:",len) + || begins_with(lresult[0].second,"initial revision:",len) + || begins_with(lresult[0].second,"RCS file:",len) + || begins_with(lresult[0].second,"done",len) + || begins_with(lresult[0].second,"Removing ",len) + || begins_with(lresult[0].second,"Checking in ",len)) + { L(F("%s\n") % lresult[0].second); + } + else + { W(F("Commit: unrecognized response %s\n") % lresult[0].second); + } + } + return result; +} + +void cvs_client::SendArgument(const std::string &a) +{ // send each line separately (Argument,[Argumentx[,...]]) + std::string::size_type newline=0,start=0; + std::string::size_type size_of_a=a.size(); + while ((newline=a.find('\n',start))!=std::string::npos) + { writestr("Argument"+std::string(start?"x":"")+" "+a.substr(start,newline-start)+"\n"); + start=newline+1; + if (start==size_of_a) break; + } + writestr("Argument"+std::string(start?"x":"")+" "+a.substr(start)+"\n"); +} + +std::vector cvs_client::ExpandModules() +{ SendCommand("expand-modules",module.c_str(),(void*)0); + std::vector result; + std::vector > lresult; + while (fetch_result(lresult)) + { I(lresult.size()==2); + I(lresult[0].second=="Module-expansion"); + result.push_back(lresult[1].second); + } + return result; +} + +// if you know a more efficient way to get this, feel free to replace it +std::map cvs_client::RequestServerDir() +{ if (server_dir.size()<=1) + SendCommand("co","-l","-r9999",module.c_str(),(void*)0); + else SendCommand("co","-r9999",module.c_str(),(void*)0); + std::string last_local,last_rcs; + std::map result; + std::vector > lresult; + while (fetch_result(lresult)) + { I(!lresult.empty()); + I(lresult[0].first=="CMD"); + if (lresult[0].second=="Set-sticky" + || lresult[0].second=="Clear-template") continue; + I(lresult[0].second=="Clear-static-directory"); + I(lresult.size()==3); + if (!last_rcs.empty() && begins_with(lresult[2].second,last_rcs) + && lresult[1].second.substr(0,last_local.size())==last_local) + { I(lresult[2].second.substr(last_rcs.size()) + ==lresult[1].second.substr(last_local.size())); + continue; + } + result[shorten_path(lresult[1].second)]=lresult[2].second; + last_local=lresult[1].second; + last_rcs=lresult[2].second; + } + return result; +} + +void cvs_client::SetServerDir(const std::map &m) +{ server_dir=m; +} + +void cvs_client::primeModules() +{ if (!server_dir.empty()) return; + std::vector modules=ExpandModules(); + for (std::vector::const_iterator i=modules.begin(); + i!=modules.end();++i) + { server_dir[shorten_path(*i)]; + } + server_dir=RequestServerDir(); + for (std::map::const_iterator i=server_dir.begin(); + i!=server_dir.end();++i) + L(F("server dir %s -> %s") % i->first % i->second); +} --- cvs_client.hh +++ cvs_client.hh @@ -0,0 +1,158 @@ +// copyright (C) 2005 Christof Petig +// all rights reserved. +// licensed to the public under the terms of the GNU GPL (>= 2) +// see the file COPYING for details + +#include +#include +#include +#include +#include +#include "ui.hh" + + +class cvs_client +{public: + struct update + { std::string contents; + std::string checksum; + std::string patch; + std::string keyword_substitution; + std::string new_revision; + std::string file; + time_t mod_time; + bool removed; + update() : mod_time(-1),removed() {} + }; + struct rlog_callbacks + { // virtual void file(const std::string &file,)=0; + virtual void file(const std::string &file, + const std::string &head_rev) const=0; + virtual void tag(const std::string &file,const std::string &tag, + const std::string &revision) const=0; + virtual void revision(const std::string &file,time_t checkin_date, + const std::string &rev,const std::string &author, + const std::string &state,const std::string &log) const=0; + }; + + struct rlist_callbacks + { virtual void file(const std::string &name, time_t last_change, + const std::string &last_rev, bool dead) const=0; + }; + + struct checkout + { time_t mod_time; + std::string contents; + std::string mode; + bool dead; + std::string keyword_substitution; + + checkout() : mod_time(-1), dead() {} + }; + struct update_callbacks + { virtual void operator()(const update &) const=0; + }; + struct update_args + { std::string file, old_revision, new_revision, keyword_substitution; + update_args(const std::string &f, const std::string &o, + const std::string &n,const std::string &k) + : file(f), old_revision(o), new_revision(n), keyword_substitution(k) {} + update_args(const std::string &f, const std::string &o) + : file(f), old_revision(o) {} + }; + struct commit_arg + { std::string file; + std::string old_revision; // newly_added => "0" + std::string keyword_substitution; + // actually these two form a tristate ;-) + bool removed; + std::string new_content; + + commit_arg() : old_revision("0"), removed() {} +// commit_arg(const std::string &rev) : old_revision(rev), removed() {} + }; + +private: + int readfd,writefd; +// size_t bytes_read,bytes_written; + std::auto_ptr byte_in_ticker; + std::auto_ptr byte_out_ticker; + typedef std::set stringset_t; + stringset_t Valid_requests; + int gzip_level; + z_stream compress,decompress; + std::string inputbuffer; + std::map server_dir; // local path -> rcs path + std::string user; + bool pserver; + + void InitZipStream(int level); + void underflow(); // fetch new characters from stream + static bool begins_with(const std::string &s, const std::string &sub); + std::string rcs_file2path(std::string s) const; + void processLogOutput(const rlog_callbacks &cb); + void connect(); + void primeModules(); + void Log_internal(const rlog_callbacks &cb,const std::string &file,va_list ap); + void reconnect(); + + void writestr(const std::string &s, bool flush=false); + std::string readline(); + std::string read_n(unsigned size); + +// void ticker(bool newline=true) const; + void SendCommand(const char *cmd,...); + void SendCommand(const char *cmd, va_list ap); + void SendArgument(const std::string &a); + // false if none available + bool fetch_result(std::string &result); + // semi internal helper to get one result line from a list + static std::string combine_result(const std::vector > &result); + + // MT style + bool fetch_result(std::vector > &result); + + static std::string pserver_password(const std::string &root); + + std::string shorten_path(const std::string &p) const; + static void parse_entry(const std::string &line, std::string &new_revision, + std::string &keyword_substitution); + void Directory(const std::string &path); + std::vector ExpandModules(); + + std::map RequestServerDir(); +protected: + std::string root; + std::string module; +// std::string rcs_root; // the real directory of the root (ask cvs.gnome.org) + std::string host; // for author certification + +public: + cvs_client(const std::string &repository, const std::string &module, bool connect=true); + ~cvs_client(); + + static bool begins_with(const std::string &s, const std::string &sub, unsigned &len); + + void GzipStream(int level); + + void RLog(const rlog_callbacks &cb,bool dummy,...); + void Log(const rlog_callbacks &cb,const char *file,...); + void RList(const rlist_callbacks &cb,bool dummy,...); + struct checkout CheckOut(const std::string &file, const std::string &revision); + struct update Update(const std::string &file, + const std::string &old_revision, const std::string &new_revision, + const std::string &keyword_substitution); + void Update(const std::vector &args, const update_callbacks &cb); + // returns ("" on remove)> + std::map > + Commit(const std::string &changelog, time_t when, + const std::vector &commits); + + bool CommandValid(const std::string &cmd) const + { return Valid_requests.find(cmd)!=Valid_requests.end(); } + void SetServerDir(const std::map &m); + + void drop_connection(); + static std::string time_t2rfc822(time_t t); +}; + --- cvs_repository.cc +++ cvs_repository.cc @@ -0,0 +1,1531 @@ +// copyright (C) 2005 Christof Petig +// all rights reserved. +// licensed to the public under the terms of the GNU GPL (>= 2) +// see the file COPYING for details + +#include "cvs_sync.hh" +#include "keys.hh" +#include "transforms.hh" +#include +#include +#include "cryptopp/md5.h" +#include +#include + +using namespace std; + +// since the piece methods in rcs_import depend on rcs_file I cannot reuse them +// I rely on string handling reference counting (which is not that bad IIRC) +// -> investigate under which conditions a string gets copied +namespace cvs_sync +{ +static std::string const cvs_cert_name="cvs-revisions"; + +struct +piece +{ + piece(string::size_type p, string::size_type l, const std::string &_s) : + pos(p), len(l), s(_s) {} + string::size_type pos; + string::size_type len; + string s; + const string operator*() const + { return s.substr(pos,len); } +}; + + +static void +index_deltatext(std::string const & dt, vector & pieces); +static void +process_one_hunk(vector< piece > const & source, + vector< piece > & dest, + vector< piece >::const_iterator & i, + int & cursor); +static void +build_string(vector const & pieces, string & out); +static void +construct_version(vector< piece > const & source_lines, + vector< piece > & dest_lines, + string const & deltatext); + +} + +using namespace cvs_sync; + +#if 0 +std::ostream &operator<<(std::ostream &o, const file_state &f) +{ return o << f.since_when << ' ' << f.cvs_version << ' ' << f.dead; +} +#endif + +bool file_state::operator<(const file_state &b) const +{ return since_when const & source, + vector< piece > & dest, + vector< piece >::const_iterator & i, + int & cursor) +{ + string directive = **i; + assert(directive.size() > 1); + ++i; + + char code; + int pos, len; + sscanf(directive.c_str(), " %c %d %d", &code, &pos, &len); + + try + { + if (code == 'a') + { + // 'ax y' means "copy from source to dest until cursor == x, then + // copy y lines from delta, leaving cursor where it is" + while (cursor < pos) + dest.push_back(source.at(cursor++)); + I(cursor == pos); + while (len--) + dest.push_back(*i++); + } + else if (code == 'd') + { + // 'dx y' means "copy from source to dest until cursor == x-1, + // then increment cursor by y, ignoring those y lines" + while (cursor < (pos - 1)) + dest.push_back(source.at(cursor++)); + I(cursor == pos - 1); + cursor += len; + } + else + throw oops("unknown directive '" + directive + "'"); + } + catch (std::out_of_range & oor) + { + throw oops("std::out_of_range while processing " + directive + + " with source.size() == " + + boost::lexical_cast(source.size()) + + " and cursor == " + + boost::lexical_cast(cursor)); + } +} + +static void +cvs_sync::build_string(vector const & pieces, string & out) +{ + out.clear(); + out.reserve(pieces.size() * 60); + for(vector::const_iterator i = pieces.begin(); + i != pieces.end(); ++i) + out.append(i->s, i->pos, i->len); +} + +static void +cvs_sync::index_deltatext(std::string const & dt, vector & pieces) +{ + pieces.clear(); + pieces.reserve(dt.size() / 30); + string::size_type begin = 0; + string::size_type end = dt.find('\n'); + while(end != string::npos) + { + // nb: the piece includes the '\n' + pieces.push_back(piece(begin, (end - begin) + 1, dt)); + begin = end + 1; + end = dt.find('\n', begin); + } + if (begin != dt.size()) + { + // the text didn't end with '\n', so neither does the piece + end = dt.size(); + pieces.push_back(piece(begin, end - begin, dt)); + } +} + +static void +cvs_sync::construct_version(vector< piece > const & source_lines, + vector< piece > & dest_lines, + string const & deltatext) +{ + dest_lines.clear(); + dest_lines.reserve(source_lines.size()); + + vector deltalines; + index_deltatext(deltatext, deltalines); + + int cursor = 0; + for (vector::const_iterator i = deltalines.begin(); + i != deltalines.end(); ) + process_one_hunk(source_lines, dest_lines, i, cursor); + while (cursor < static_cast(source_lines.size())) + dest_lines.push_back(source_lines[cursor++]); +} + +/* supported by the woody version: +Root Valid-responses valid-requests Repository Directory Max-dotdot +Static-directory Sticky Checkin-prog Update-prog Entry Kopt Checkin-time +Modified Is-modified UseUnchanged Unchanged Notify Questionable Case +Argument Argumentx Global_option Gzip-stream wrapper-sendme-rcsOptions Set +expand-modules ci co update diff log rlog add remove update-patches +gzip-file-contents status rdiff tag rtag import admin export history release +watch-on watch-off watch-add watch-remove watchers editors annotate +rannotate noop version +*/ + +//--------------------- implementation ------------------------------- + +size_t const cvs_edge::cvs_window; + +bool cvs_revision_nr::operator==(const cvs_revision_nr &b) const +{ return parts==b.parts; +} + +// is this strictly correct? feels ok for now (and this is last ressort) +bool cvs_revision_nr::operator<(const cvs_revision_nr &b) const +{ return parts::const_iterator i=parts.begin();i!=parts.end();) + { result+= (F("%d") % *i).str(); + ++i; + if (i!=parts.end()) result+","; + } + return result; +} + +bool cvs_revision_nr::is_parent_of(const cvs_revision_nr &child) const +{ unsigned cps=child.parts.size(); + unsigned ps=parts.size(); + if (cps branch tag +bool cvs_revision_nr::is_branch() const +{ return parts.size()&1; +} + +// cvs_repository ---------------------- + +struct cvs_repository::get_all_files_log_cb : rlog_callbacks +{ cvs_repository &repo; + get_all_files_log_cb(cvs_repository &r) : repo(r) {} + virtual void file(const std::string &file,const std::string &head_rev) const + { L(F("get_all_files_log_cb %s") % file); + repo.files[file]; + } + virtual void tag(const std::string &file,const std::string &tag, + const std::string &revision) const {} + virtual void revision(const std::string &file,time_t t, + const std::string &rev,const std::string &author, + const std::string &state,const std::string &log) const {} +}; + +#if 0 +struct cvs_repository::get_all_files_list_cb : rlist_callbacks +{ cvs_repository &repo; + get_all_files_list_cb(cvs_repository &r) : repo(r) {} + virtual void file(const std::string &name, time_t last_change, + const std::string &last_rev, bool dead) const + { repo.files[name].known_states.insert(file_state(last_change,last_rev,dead)); +// this does hurt more than help +// repo.edges.insert(cvs_edge(last_change)); + } +}; +#endif + +// get all available files and their newest revision +void cvs_repository::get_all_files() +{ if (edges.empty()) + { +#if 0 // seems to be more efficient but it's hard to guess the directory the + // server talks about + if (CommandValid("rlist")) + { RList(get_all_files_list_cb(*this),false,"-l","-R","-d","--",module.c_str(),(void*)0); + } + else // less efficient? ... +#endif + { I(CommandValid("rlog")); + RLog(get_all_files_log_cb(*this),false,"-N","-h","--",module.c_str(),(void*)0); + } + } +} + +std::string debug_manifest(const cvs_manifest &mf) +{ std::string result; + for (cvs_manifest::const_iterator i=mf.begin(); i!=mf.end(); ++i) + { result+= i->first + " " + i->second->cvs_version + " " + + (i->second->dead?"dead ":"") + i->second->sha1sum() + "\n"; + } + return result; +} + +std::string debug_files(const std::map &files) +{ std::string result; + for (std::map::const_iterator i=files.begin(); + i!=files.end();++i) + { result += i->first; + result += " ("; + for (std::set::const_iterator j=i->second.known_states.begin(); + j!=i->second.known_states.end();) + { result += boost::lexical_cast(j->since_when%1000) + ":" + j->cvs_version + "="; + if (j->dead) result += "dead"; + else if (j->size) result += boost::lexical_cast(j->size); + else if (j->patchsize) result += 'p' + boost::lexical_cast(j->patchsize); + else if (!j->sha1sum().empty()) result += j->sha1sum().substr(0,4) + j->keyword_substitution; + ++j; + if (j!=i->second.known_states.end()) result += ","; + } + result += ")\n"; + } + return result; +} + +std::string cvs_repository::debug() const +{ std::string result; + + // edges set + result+= "Edges :\n"; + for (std::set::const_iterator i=edges.begin(); + i!=edges.end();++i) + { result+= "[" + boost::lexical_cast(i->time); + if (i->time!=i->time2) result+= "+" + boost::lexical_cast(i->time2-i->time); + if (!i->revision().empty()) result+= "," + i->revision().substr(0,4); + if (!i->xfiles.empty()) + result+= "," + boost::lexical_cast(i->xfiles.size()) + + (i->delta_base.inner()().empty()?"files":"deltas"); + result+= "," + i->author + ","; + std::string::size_type nlpos=i->changelog.find_first_of("\n\r"); + if (nlpos>50) nlpos=50; + result+= i->changelog.substr(0,nlpos) + "]\n"; + } + result+= "Files :\n"; + for (std::map::const_iterator i=files.begin(); + i!=files.end();++i) + { result+= i->first; + result+= " ("; + for (std::set::const_iterator j=i->second.known_states.begin(); + j!=i->second.known_states.end();) + { if (j->dead) result+= "dead"; + else if (j->size) result+= boost::lexical_cast(j->size); + else if (j->patchsize) result+= "p" + boost::lexical_cast(j->patchsize); + else if (!j->sha1sum().empty()) result+= j->sha1sum().substr(0,4) + j->keyword_substitution; + ++j; + if (j!=i->second.known_states.end()) result+= ","; + } + result+= ")\n"; + } + result+= "Tags :\n"; + for (std::map >::const_iterator i=tags.begin(); + i!=tags.end();++i) + { result+= i->first + "(" + boost::lexical_cast(i->second.size()) + " files)\n"; + } + return result; +} + +struct cvs_repository::prime_log_cb : rlog_callbacks +{ cvs_repository &repo; + std::map::iterator i; + time_t override_time; + prime_log_cb(cvs_repository &r,const std::map::iterator &_i + ,time_t overr_time=-1) + : repo(r), i(_i), override_time(overr_time) {} + virtual void tag(const std::string &file,const std::string &tag, + const std::string &revision) const; + virtual void revision(const std::string &file,time_t t, + const std::string &rev,const std::string &author, + const std::string &state,const std::string &log) const; + virtual void file(const std::string &file,const std::string &head_rev) const + { } +}; + +void cvs_repository::prime_log_cb::tag(const std::string &file,const std::string &tag, + const std::string &revision) const +{ I(i->first==file); + std::map &tagslot=repo.tags[tag]; + tagslot[file]=revision; +} + +void cvs_repository::prime_log_cb::revision(const std::string &file,time_t checkin_time, + const std::string &revision,const std::string &_author, + const std::string &dead,const std::string &_message) const +{ L(F("prime_log_cb %s:%s %d %s %d %s\n") % file % revision % checkin_time + % _author % _message.size() % dead); + std::string author=_author; + std::string message=_message; + I(i->first==file); + if (override_time!=-1) + { checkin_time=override_time; + message="initial state for cvs_pull --since"; + author=repo.app.signing_key(); + } + std::pair::iterator,bool> iter= + i->second.known_states.insert + (file_state(checkin_time,revision,dead=="dead")); + // I(iter.second==false); + // set iterators are read only to prevent you from destroying the order + file_state &fs=const_cast(*(iter.first)); + fs.log_msg=message; + fs.author=author; + std::pair::iterator,bool> iter2= + repo.edges.insert(cvs_edge(message,checkin_time,author)); + if (iter2.second && repo.cvs_edges_ticker.get()) ++(*repo.cvs_edges_ticker); +} + +bool cvs_edge::similar_enough(cvs_edge const & other) const +{ + if (changelog != other.changelog) + return false; + if (author != other.author) + return false; + if (labs(time - other.time) > cvs_window + && labs(time2 - other.time) > cvs_window) + return false; + return true; +} + +bool cvs_edge::operator<(cvs_edge const & other) const +{ + return time < other.time || + + (time == other.time + && author < other.author) || + + (time == other.time + && author == other.author + && changelog < other.changelog); +} + +void cvs_repository::store_contents(const std::string &contents, hexenc &sha1sum) +{ + data dat(contents); + calculate_ident(dat,sha1sum); + if (!app.db.file_version_exists(sha1sum)) + { base64 > packed; + pack(dat, packed); + file_data fdat=packed; + app.db.put_file(sha1sum, fdat); + if (file_id_ticker.get()) ++(*file_id_ticker); + } +} + +static void apply_delta(vector &contents, const std::string &patch) +{ vector after; + construct_version(contents,after,patch); + std::swap(contents,after); +} + +void cvs_repository::store_delta(const std::string &new_contents, + const std::string &old_contents, + // this argument is unused since we can no longer use the rcs patch + const std::string &dummy, + const hexenc &from, hexenc &to) +{ if (old_contents.empty()) + { store_contents(new_contents, to); + return; + } + data dat(new_contents); + calculate_ident(dat,to); + if (!app.db.file_version_exists(to)) + { + base64< gzip > del; + diff(data(old_contents), data(new_contents), del); + base64< gzip > packed; + pack(data(new_contents), packed); + if (packed().size()<=del().size()) + // the data is smaller or of equal size to the patch + app.db.put_file(to, packed); + else + app.db.put_file_version(from,to,del); + if (file_id_ticker.get()) ++(*file_id_ticker); + } +} + +static bool +build_change_set(const cvs_client &c, const cvs_manifest &oldm, cvs_manifest &newm, + change_set & cs, cvs_file_state remove_state) +{ + cs = change_set(); + cvs_manifest cvs_delta; + + L(F("build_change_set(%d,%d,)\n") % oldm.size() % newm.size()); + + for (cvs_manifest::const_iterator f = oldm.begin(); f != oldm.end(); ++f) + { + cvs_manifest::const_iterator fn = newm.find(f->first); + if (fn==newm.end()) + { + L(F("deleting file '%s'\n") % f->first); + cs.delete_file(f->first); + cvs_delta[f->first]=remove_state; + } + else + { if (f->second->sha1sum == fn->second->sha1sum) + { +// L(F("skipping preserved entry state '%s' on '%s'\n") +// % fn->second->sha1sum % fn->first); + } + else + { + L(F("applying state delta on '%s' : '%s' -> '%s'\n") + % fn->first % f->second->sha1sum % fn->second->sha1sum); + I(!fn->second->sha1sum().empty()); + cs.apply_delta(fn->first, f->second->sha1sum, fn->second->sha1sum); + cvs_delta[f->first]=fn->second; + } + } + } + for (cvs_manifest::const_iterator f = newm.begin(); f != newm.end(); ++f) + { + cvs_manifest::const_iterator fo = oldm.find(f->first); + if (fo==oldm.end()) + { + L(F("adding file '%s' as '%s'\n") % f->second->sha1sum % f->first); + I(!f->second->sha1sum().empty()); + cs.add_file(f->first, f->second->sha1sum); + cvs_delta[f->first]=f->second; + } + } + if (!oldm.empty() && cvs_delta.size()::iterator &e) +{ cvs_file_state s2=s; + ++s2; + if (s2==end) return; + I(s->since_when!=s2->since_when); + // checkins must not overlap (next revision must lie beyond edge) + if ((*s2) <= (*e)) + { W(F("splitting edge %ld-%ld at %ld\n") % e->time % e->time2 % s2->since_when); + cvs_edge new_edge=*e; + I(s2->since_when-1>=e->time); + e->time2=s2->since_when-1; + new_edge.time=s2->since_when; + edges.insert(new_edge); + } +} + +void cvs_repository::join_edge_parts(std::set::iterator i) +{ for (;i!=edges.end();) + { std::set::iterator j=i; + j++; // next one + if (j==edges.end()) break; + + I(j->time2==j->time); // make sure we only do this once + I(i->time2<=j->time); // should be sorted ... + if (!i->similar_enough(*j)) + { ++i; continue; } + I((j->time-i->time2)<=time_t(cvs_edge::cvs_window)); // just to be sure + I(i->author==j->author); + I(i->changelog==j->changelog); + I(i->time2time); // should be non overlapping ... + L(F("joining %ld-%ld+%ld\n") % i->time % i->time2 % j->time); + i->time2=j->time; + edges.erase(j); + } +} + +void cvs_repository::store_update(std::set::const_iterator s, + std::set::iterator s2,const cvs_client::update &u, + std::string &contents) +{ + if (u.removed) + { const_cast(s2->dead)=true; + } + else if (!u.checksum.empty()) + { // const_cast(s2->rcs_patch)=u.patch; + const_cast(s2->md5sum)=u.checksum; + const_cast(s2->patchsize)=u.patch.size(); + const_cast(s2->keyword_substitution)=u.keyword_substitution; + // I(s2->since_when==u.mod_time); + if (u.mod_time!=s2->since_when && u.mod_time!=-1) + { W(F("update time %ld and log time %ld disagree\n") % u.mod_time % s2->since_when); + } + std::string old_contents=contents; + { std::vector file_contents; + index_deltatext(contents,file_contents); + apply_delta(file_contents, u.patch); + build_string(file_contents, contents); + } + // check md5 + CryptoPP::MD5 hash; + std::string md5sum=xform(u.checksum); + I(md5sum.size()==CryptoPP::MD5::DIGESTSIZE); + if (hash.VerifyDigest(reinterpret_cast(md5sum.c_str()), + reinterpret_cast(contents.c_str()), + contents.size())) + { store_delta(contents, old_contents, u.patch, s->sha1sum, const_cast&>(s2->sha1sum)); + } + else + { throw oops("MD5 sum wrong"); + } + } + else + { if (!s->sha1sum().empty()) + // we default to patch if it's at all possible + store_delta(u.contents, contents, std::string(), s->sha1sum, const_cast&>(s2->sha1sum)); + else + store_contents(u.contents, const_cast&>(s2->sha1sum)); + const_cast(s2->size)=u.contents.size(); + contents=u.contents; + const_cast(s2->keyword_substitution)=u.keyword_substitution; + } +} + +// s2 gets changed +void cvs_repository::update(std::set::const_iterator s, + std::set::iterator s2,const std::string &file, + std::string &contents) +{ + cvs_revision_nr srev(s->cvs_version); + I(srev.is_parent_of(s2->cvs_version)); + if (s->dead) + { cvs_client::checkout c=CheckOut(file,s2->cvs_version); + I(!c.dead); // dead->dead is no change, so shouldn't get a number + I(!s2->dead); + // I(s2->since_when==c.mod_time); + if (c.mod_time!=s2->since_when && c.mod_time!=-1 && s2->since_when!=sync_since) + { W(F("checkout time %ld and log time %ld disagree\n") % c.mod_time % s2->since_when); + } + store_contents(c.contents, const_cast&>(s2->sha1sum)); + const_cast(s2->size)=c.contents.size(); + contents=c.contents; + const_cast(s2->keyword_substitution)=c.keyword_substitution; + } + else if (s2->dead) // short circuit if we already know it's dead + { L(F("file %s: revision %s already known to be dead\n") % file % s2->cvs_version); + } + else + { cvs_client::update u=Update(file,s->cvs_version,s2->cvs_version,s->keyword_substitution); + store_update(s,s2,u,contents); + } +} + +void cvs_repository::store_checkout(std::set::iterator s2, + const cvs_client::checkout &c, std::string &file_contents) +{ const_cast(s2->dead)=c.dead; + if (!c.dead) + { // I(c.mod_time==s2->since_when); + if (c.mod_time!=s2->since_when && c.mod_time!=-1 && s2->since_when!=sync_since) + { W(F("checkout time %ld and log time %ld disagree\n") % c.mod_time % s2->since_when); + } + store_contents(c.contents, const_cast&>(s2->sha1sum)); + const_cast(s2->size)=c.contents.size(); + file_contents=c.contents; + const_cast(s2->keyword_substitution)=c.keyword_substitution; + } +} + +void cvs_repository::fill_manifests(std::set::iterator e) +{ cvs_manifest current_manifest; + if (e!=edges.begin()) + { std::set::const_iterator before=e; + --before; + current_manifest=get_files(*before); + } + for (;e!=edges.end();++e) + { std::set::iterator next_edge=e; + ++next_edge; + for (std::map::const_iterator f=files.begin();f!=files.end();++f) + { I(!f->second.known_states.empty()); + if (!(*(f->second.known_states.begin()) <= (*e))) + // the file does not exist yet (first is not below/equal current edge) + { L(F("%s before beginning %d/%d+%d\n") % f->first + % f->second.known_states.begin()->since_when + % e->time % (e->time2-e->time)); + continue; + } +#if 0 + if ((*(f->second.known_states.rend()) < (*e))) + // the last revision was already processed (it remains this state) + { W(F("%s beyond end %d/%d+%d\n") % f->first + % f->second.known_states.rend()->since_when + % e->time % (e->time2-e->time)); + continue; + } +#endif + cvs_manifest::iterator mi=current_manifest.find(f->first); + if (mi==current_manifest.end()) // the file is currently dead + { cvs_file_state s=f->second.known_states.end(); + // find last revision that fits but does not yet belong to next edge + // use until end, or above range, or belongs to next edge + for (cvs_file_state s2=f->second.known_states.begin(); + s2!=f->second.known_states.end() + && (*s2)<=(*e) + && ( next_edge==edges.end() || ((*s2)<(*next_edge)) ); + ++s2) + { L(F("%s matches %d/%d+%d\n") % f->first + % s2->since_when + % e->time % (e->time2-e->time)); + s=s2; + } + + if (s!=f->second.known_states.end() && !s->dead) + // a matching revision was found + { current_manifest[f->first]=s; + I(!s->sha1sum().empty()); + check_split(s,f->second.known_states.end(),e); + } + } + else // file was present in last manifest, check whether next revision already fits + { cvs_file_state s=mi->second; + ++s; + if (s!=f->second.known_states.end() + && (*s)<=(*e) + && ( next_edge==edges.end() || ((*s)<(*next_edge)) ) ) + { if (s->dead) current_manifest.erase(mi); + else + { mi->second=s; + I(!s->sha1sum().empty()); + } + check_split(s,f->second.known_states.end(),e); + } + } + } + e->xfiles=current_manifest; + } +} + +void cvs_repository::commit_revisions(std::set::iterator e) +{ cvs_manifest parent_manifest; + revision_id parent_rid; + manifest_id parent_mid; + manifest_map parent_map; + manifest_map child_map=parent_map; + packet_db_writer dbw(app); + + cvs_edges_ticker.reset(0); + revision_ticker.reset(new ticker("revisions", "R", 3)); +// const cvs_manifest *oldmanifestp=∅ + if (e!=edges.begin()) + { std::set::const_iterator before=e; + --before; + I(!before->revision().empty()); + parent_rid=before->revision; + app.db.get_revision_manifest(parent_rid,parent_mid); + app.db.get_manifest(parent_mid,parent_map); + child_map=parent_map; + parent_manifest=get_files(*before); + } + for (; e!=edges.end(); ++e) + { boost::shared_ptr cs(new change_set()); + I(e->delta_base.inner()().empty()); // no delta yet + cvs_manifest child_manifest=get_files(*e); + if (build_change_set(*this,parent_manifest,e->xfiles,*cs,remove_state)) + e->delta_base=parent_rid; + if (cs->empty()) + { W(F("null edge (empty cs) @%ld skipped\n") % e->time); + continue; + } + I(!e->xfiles.empty()); + apply_change_set(*cs, child_map); + if (child_map.empty()) + { W(F("empty edge (no files in manifest) @%ld skipped\n") % e->time); + // perhaps begin a new tree: + // parent_rid=revision_id(); + // parent_mid=manifest_id(); +// parent_manifest=cvs_manifest(); + continue; + } + manifest_id child_mid; + calculate_ident(child_map, child_mid); + + revision_set rev; + rev.new_manifest = child_mid; + rev.edges.insert(std::make_pair(parent_rid, make_pair(parent_mid, cs))); + revision_id child_rid; + calculate_ident(rev, child_rid); + L(F("CVS Sync: Inserting revision %s (%s) into repository\n") % child_rid % child_mid); + + if (app.db.manifest_version_exists(child_mid)) + { + L(F("existing path to %s found, skipping\n") % child_mid); + } + else if (parent_mid.inner()().empty()) + { + manifest_data mdat; + I(!child_map.empty()); + write_manifest_map(child_map, mdat); + app.db.put_manifest(child_mid, mdat); + } + else + { + base64< gzip > del; + I(!child_map.empty()); + diff(parent_map, child_map, del); + app.db.put_manifest_version(parent_mid, child_mid, del); + } + e->revision=child_rid.inner(); + if (! app.db.revision_exists(child_rid)) + { app.db.put_revision(child_rid, rev); + if (revision_ticker.get()) ++(*revision_ticker); + } + cert_revision_in_branch(child_rid, app.branch_name(), app, dbw); + std::string author=e->author; + if (author.find('@')==std::string::npos) author+="@"+host; + cert_revision_author(child_rid, author, app, dbw); + cert_revision_changelog(child_rid, e->changelog, app, dbw); + cert_revision_date_time(child_rid, e->time, app, dbw); + cert_cvs(*e, dbw); + + // now apply same change set to parent_map, making parent_map == child_map + apply_change_set(*cs, parent_map); + parent_mid = child_mid; + parent_rid = child_rid; + parent_manifest=child_manifest; + } +} + +void cvs_repository::prime() +{ get_all_files(); + revision_ticker.reset(0); + cvs_edges_ticker.reset(new ticker("edges", "E", 10)); + for (std::map::iterator i=files.begin();i!=files.end();++i) + { // -d D< (sync_since) + if (sync_since!=-1) + { Log(prime_log_cb(*this,i,sync_since),i->first.c_str(), + "-d",(time_t2rfc822(sync_since)).c_str(), + "-b",(void*)0); + Log(prime_log_cb(*this,i),i->first.c_str(), + "-d",(time_t2rfc822(sync_since)+"<").c_str(), + "-b",(void*)0); + } + else Log(prime_log_cb(*this,i),i->first.c_str(),"-b",(void*)0); + } + // remove duplicate states (because some edges were added by the + // get_all_files method + for (std::set::iterator i=edges.begin();i!=edges.end();) + { if (i->changelog_valid || i->author.size()) { ++i; continue; } + std::set::iterator j=i; + j++; + I(j!=edges.end()); + I(j->time==i->time); + I(i->xfiles.empty()); +// I(i->revision.empty()); + edges.erase(i); + if (cvs_edges_ticker.get()) --(*cvs_edges_ticker); + i=j; + } + + // join adjacent check ins (same author, same changelog) + join_edge_parts(edges.begin()); + + // get the contents + for (std::map::iterator i=files.begin();i!=files.end();++i) + { std::string file_contents; + I(!i->second.known_states.empty()); + { std::set::iterator s2=i->second.known_states.begin(); + cvs_client::checkout c=CheckOut(i->first,s2->cvs_version); + store_checkout(s2,c,file_contents); + } + for (std::set::iterator s=i->second.known_states.begin(); + s!=i->second.known_states.end();++s) + { std::set::iterator s2=s; + ++s2; + if (s2==i->second.known_states.end()) break; + update(s,s2,i->first,file_contents); + } + } + drop_connection(); + +#if 0 + if (sync_since!=-1 && edges.empty() && !files.empty()) + // no change happened since sync_since, so we didn't see an edge, + // fake one + { cvs_edge new_edge("initial state for cvs_pull --since",sync_since,app.signing_key()); + edges.insert(new_edge); + } +#endif + // fill in file states at given point + fill_manifests(edges.begin()); + + // commit them all + commit_revisions(edges.begin()); +} + +void cvs_repository::cert_cvs(const cvs_edge &e, packet_consumer & pc) +{ std::string content=host+":"+root+"/"+module+"\n"; + if (!e.delta_base.inner()().empty()) + { //hexenc h; +// encode_hexenc(e.delta_base.inner(),h); + content+="+"+e.delta_base.inner()()+"\n"; + } + for (cvs_manifest::const_iterator i=e.xfiles.begin(); i!=e.xfiles.end(); ++i) + { content+=i->second->cvs_version; + if (!i->second->keyword_substitution.empty()) + content+="/"+i->second->keyword_substitution; + content+=" "+i->first+"\n"; + } + cert t; + make_simple_cert(e.revision, cert_name(cvs_cert_name), content, app, t); + revision cc(t); + pc.consume_revision_cert(cc); +} + +cvs_repository::cvs_repository(app_state &_app, const std::string &repository, + const std::string &module, bool connect) + : cvs_client(repository,module,connect), app(_app), file_id_ticker(), + revision_ticker(), cvs_edges_ticker(), remove_state(), sync_since(-1) +{ + file_id_ticker.reset(new ticker("file ids", "F", 10)); + remove_state=remove_set.insert(file_state(0,"-",true)).first; + if (!app.sync_since().empty()) + sync_since=posix2time_t(app.sync_since()); +} + +static void test_key_availability(app_state &app) +{ + // early short-circuit to avoid failure after lots of work + rsa_keypair_id key; + N(guess_default_key(key,app), F("could not guess default signing key")); + // Require the password early on, so that we don't do lots of work + // and then die. + app.signing_key = key; + + N(app.lua.hook_persist_phrase_ok(), + F("need permission to store persistent passphrase (see hook persist_phrase_ok())")); + require_password(key, app); +} + +std::set::iterator cvs_repository::last_known_revision() +{ I(!edges.empty()); + std::set::iterator now_iter=edges.end(); + --now_iter; + return now_iter; +} + +time_t cvs_repository::posix2time_t(std::string posix_format) +{ std::string::size_type next_illegal=0; + while ((next_illegal=posix_format.find_first_of("-:"))!=std::string::npos) + posix_format.erase(next_illegal,1); + boost::posix_time::ptime tmp= boost::posix_time::from_iso_string(posix_format); + boost::posix_time::time_duration dur= tmp + -boost::posix_time::ptime(boost::gregorian::date(1970,1,1), + boost::posix_time::time_duration(0,0,0,0)); + return dur.total_seconds(); +} + +cvs_edge::cvs_edge(const revision_id &rid, app_state &app) + : changelog_valid(), time(), time2() +{ revision=hexenc(rid.inner()); + // get author + date + std::vector< ::revision > edge_certs; + app.db.get_revision_certs(rid,edge_certs); + // erase_bogus_certs ? + for (std::vector< ::revision >::const_iterator c=edge_certs.begin(); + c!=edge_certs.end();++c) + { cert_value value; + decode_base64(c->inner().value, value); + if (c->inner().name()==date_cert_name) + { L(F("date cert %s\n")%value()); + time=time2=cvs_repository::posix2time_t(value()); + } + else if (c->inner().name()==author_cert_name) + { author=value(); + } + else if (c->inner().name()==changelog_cert_name) + { changelog=value(); + changelog_valid=true; + } + } +} + +#if 0 +std::ostream &operator<<(std::ostream &o, const cvs_manifest &mf) +{ for (cvs_manifest::const_iterator i=mf.begin(); i!=mf.end(); ++i) + { o << i->first << ' ' << i->second->cvs_version << ','; + } + return o; +} +#endif + +std::set::iterator cvs_repository::commit( + std::set::iterator parent, const revision_id &rid) +{ // check that it's the last one + L(F("commit %s -> %s\n") % parent->revision % rid); + { std::set::iterator test=parent; + ++test; + I(test==edges.end()); + } + // a bit like process_certs + cvs_edge e(rid,app); + + revision_set rs; + app.db.get_revision(rid, rs); + std::vector commits; + + for (edge_map::const_iterator j = rs.edges.begin(); + j != rs.edges.end(); + ++j) + { if (!(edge_old_revision(j) == parent->revision)) + { L(F("%s != %s\n") % edge_old_revision(j) % parent->revision); + continue; + } + const change_set &cs=edge_changes(j); + N(cs.rearrangement.renamed_dirs.empty(), + F("I can't commit directory renames yet\n")); + N(cs.rearrangement.deleted_dirs.empty(), + F("I can't commit directory deletions yet\n")); + cvs_manifest parent_manifest=get_files(*parent); + + for (std::set::const_iterator i=cs.rearrangement.deleted_files.begin(); + i!=cs.rearrangement.deleted_files.end(); ++i) + { commit_arg a; + a.file=(*i)(); + cvs_manifest::const_iterator old=parent_manifest.find(a.file); + I(old!=parent_manifest.end()); + a.removed=true; + a.old_revision=old->second->cvs_version; + a.keyword_substitution=old->second->keyword_substitution; + commits.push_back(a); + L(F("delete %s -%s %s\n") % a.file % a.old_revision % a.keyword_substitution); + } + + for (std::map::const_iterator i + =cs.rearrangement.renamed_files.begin(); + i!=cs.rearrangement.renamed_files.end(); ++i) + { commit_arg a; // remove + a.file=i->first(); + cvs_manifest::const_iterator old=parent_manifest.find(a.file); + I(old!=parent_manifest.end()); + a.removed=true; + a.old_revision=old->second->cvs_version; + a.keyword_substitution=old->second->keyword_substitution; + commits.push_back(a); + L(F("rename from %s -%s %s\n") % a.file % a.old_revision % a.keyword_substitution); + + a=commit_arg(); // add + a.file=i->second(); + I(!old->second->sha1sum().empty()); + file_data dat; + app.db.get_file_version(old->second->sha1sum,dat); + data unpacked; + unpack(dat.inner(), unpacked); + a.new_content=unpacked(); + commits.push_back(a); + L(F("rename to %s %d\n") % a.file % a.new_content.size()); + } + + // added files also have a delta, so we can ignore this list + + for (change_set::delta_map::const_iterator i=cs.deltas.begin(); + i!=cs.deltas.end(); ++i) + { + commit_arg a; + a.file=i->first(); + cvs_manifest::const_iterator old=parent_manifest.find(a.file); + if (old!=parent_manifest.end()) + { a.old_revision=old->second->cvs_version; + a.keyword_substitution=old->second->keyword_substitution; + } + file_data dat; + app.db.get_file_version(i->second.second,dat); + data unpacked; + unpack(dat.inner(), unpacked); + a.new_content=unpacked(); + commits.push_back(a); + L(F("delta %s %s %s %d\n") % a.file % a.old_revision % a.keyword_substitution + % a.new_content.size()); + } + + I(!commits.empty()); + std::map > result + =Commit(e.changelog,e.time,commits); + if (result.empty()) return edges.end(); + + e.delta_base=parent->revision; + + for (std::map >::const_iterator + i=result.begin(); i!=result.end(); ++i) + { if (i->second.first.empty()) + { e.xfiles[i->first]=remove_state; + } + else + { file_state fs(e.time,i->second.first); + fs.log_msg=e.changelog; + fs.author=e.author; + fs.keyword_substitution=i->second.second; + change_set::delta_map::const_iterator mydelta=cs.deltas.find(i->first); + I(mydelta!=cs.deltas.end()); + fs.sha1sum=mydelta->second.second.inner(); + std::pair::iterator,bool> newelem= + files[i->first].known_states.insert(fs); + I(newelem.second); + e.xfiles[i->first]=newelem.first; + } + } + packet_db_writer dbw(app); + cert_cvs(e, dbw); + revision_lookup[e.revision]=edges.insert(e).first; + if (global_sanity.debug) L(F("%s") % debug()); + return --(edges.end()); + } + W(F("no matching parent found\n")); + return edges.end(); +} + +void cvs_repository::commit() +{ + std::set::iterator now_iter=last_known_revision(); + while (now_iter!=edges.end()) + { const cvs_edge &now=*now_iter; + I(!now.revision().empty()); + + L(F("looking for children of revision %s\n") % now.revision); + std::set children; + app.db.get_revision_children(now.revision, children); + + if (!app.branch_name().empty()) + { base64 value; + encode_base64(cert_value(app.branch_name()), value); + // ignore revisions not belonging to the specified branch + for (std::set::iterator i=children.begin(); + i!=children.end();) + { std::vector< revision > certs; + app.db.get_revision_certs(*i,branch_cert_name,value,certs); + std::set::iterator help=i; + ++help; + if (certs.empty()) children.erase(i); + i=help; + } + } + if (children.empty()) return; + if (children.size()>1) + { W(F("several children found for %s:\n") % now.revision); + for (std::set::const_iterator i=children.begin(); + i!=children.end();++i) + { W(F("%s\n") % *i); + } + return; + } + now_iter=commit(now_iter,*children.begin()); + + P(F("checked %s into cvs repository") % now.revision); + // we'd better seperate the commits so that ordering them is possible + if (now_iter!=edges.end()) sleep(2); + } +} + +// this is somewhat clumsy ... rethink it +static void guess_repository(std::string &repository, std::string &module, + std::vector< revision > &certs, app_state &app) +{ I(!app.branch_name().empty()); + app.db.get_revision_certs(cvs_cert_name, certs); + // erase_bogus_certs ? + std::vector< revision > branch_certs; + base64 branch_value; + encode_base64(cert_value(app.branch_name()), branch_value); + app.db.get_revision_certs(branch_cert_name, branch_value, branch_certs); + // use a set to gain speed? scan the smaller vector to gain speed? + for (std::vector< revision >::const_iterator ci=certs.begin(); + ci!=certs.end();++ci) + for (std::vector< revision >::const_iterator bi=branch_certs.begin(); + bi!=branch_certs.end();++bi) + { // actually this finds an arbitrary element of the set intersection + if (ci->inner().ident==bi->inner().ident) + { cert_value value; + decode_base64(ci->inner().value, value); + std::string::size_type nlpos=value().find('\n'); + I(nlpos!=std::string::npos); + std::string repo=value().substr(0,nlpos); + std::string::size_type lastslash=repo.rfind('/'); + I(lastslash!=std::string::npos); + // this is naive ... but should work most of the time + // we should not separate repo and module by '/' + // but I do not know a much better separator + repository=repo.substr(0,lastslash); + module=repo.substr(lastslash+1); + goto break_outer; + } + } + break_outer: ; + N(!module.empty(), F("No cvs cert in this branch, please specify repository and module")); +} + +void cvs_sync::push(const std::string &_repository, const std::string &_module, + app_state &app) +{ test_key_availability(app); + // make the variables changeable + std::string repository=_repository, module=_module; + std::vector< revision > certs; + if (repository.empty() || module.empty()) + guess_repository(repository, module, certs, app); + cvs_sync::cvs_repository repo(app,repository,module); +// turned off for DEBUGGING + if (!getenv("CVS_CLIENT_LOG")) + repo.GzipStream(3); + transaction_guard guard(app.db); + + if (certs.empty()) + app.db.get_revision_certs(cvs_cert_name, certs); + repo.process_certs(certs); + + N(!repo.empty(), + F("no revision certs for this repository/module\n")); + + repo.commit(); + + guard.commit(); +} + +void cvs_sync::pull(const std::string &_repository, const std::string &_module, + app_state &app) +{ test_key_availability(app); + // make the variables changeable + std::string repository=_repository, module=_module; + + std::vector< revision > certs; + + if (repository.empty() || module.empty()) + guess_repository(repository, module, certs, app); + cvs_sync::cvs_repository repo(app,repository,module); +// turned off for DEBUGGING + if (!getenv("CVS_CLIENT_LOG")) + repo.GzipStream(3); + transaction_guard guard(app.db); + + if (certs.empty()) app.db.get_revision_certs(cvs_cert_name, certs); + repo.process_certs(certs); + + // initial checkout + if (repo.empty()) + repo.prime(); + else repo.update(); + + guard.commit(); +} + +cvs_file_state cvs_repository::remember(std::set &s,const file_state &fs) +{ for (std::set::iterator i=s.begin();i!=s.end();++i) + { if (i->cvs_version==fs.cvs_version) + { if (i->since_when>fs.since_when) + const_cast(i->since_when)=fs.since_when; + return i; + } + } + std::pair iter=s.insert(fs); + I(iter.second); + return iter.first; +} + +void cvs_repository::process_certs(const std::vector< revision > &certs) +{ + std::auto_ptr cert_ticker; + cert_ticker.reset(new ticker("cvs certs", "C", 10)); + + std::string needed_cert=host+":"+root+"/"+module+"\n"; + for (vector >::const_iterator i=certs.begin(); i!=certs.end(); ++i) + { // populate data structure using these certs + cert_value cvs_revisions; + decode_base64(i->inner().value, cvs_revisions); + if (cvs_revisions().size()>needed_cert.size() + && cvs_revisions().substr(0,needed_cert.size())==needed_cert) + { // parse and add the cert + ++(*cert_ticker); + cvs_edge e(i->inner().ident,app); + + std::vector pieces; + // in Zeilen aufteilen + index_deltatext(cvs_revisions(),pieces); + I(!pieces.empty()); + manifest_id mid; + app.db.get_revision_manifest(i->inner().ident,mid); + manifest_map manifest; + app.db.get_manifest(mid,manifest); + // manifest; + std::vector::const_iterator p=pieces.begin()+1; + if ((**p)[0]=='+') // this is a delta encoded manifest + { hexenc h=(**p).substr(1,40); // remember to omit the trailing \n + e.delta_base=revision_id(h); + ++p; + } + for (;p!=pieces.end();++p) + { std::string line=**p; + I(!line.empty()); + I(line[line.size()-1]=='\n'); + line.erase(line.size()-1,1); + // the format is "[/] \n" + // e.g. "1.1 .cvsignore", "1.43/-kb test.png" + std::string::size_type space=line.find(' '); + I(space!=std::string::npos); + std::string monotone_path=line.substr(space+1); + std::string path=monotone_path; + // look for the optional initial slash separating the keyword mode + std::string::size_type slash=line.find('/'); + if (slash==std::string::npos || slash>space) + slash=space; + + file_state fs; + fs.since_when=e.time; + fs.cvs_version=line.substr(0,slash); + if (space!=slash) + fs.keyword_substitution=line.substr(slash+1,space-(slash+1)); + if (fs.cvs_version=="-") // delta encoded: remove + { I(!e.delta_base.inner()().empty()); + e.xfiles.insert(std::make_pair(path,remove_state)); + } + else + { manifest_map::const_iterator iter_file_id=manifest.find(monotone_path); + I(iter_file_id!=manifest.end()); + fs.sha1sum=iter_file_id->second.inner(); + fs.log_msg=e.changelog; + fs.author=e.author; + cvs_file_state cfs=remember(files[path].known_states,fs); + e.xfiles.insert(std::make_pair(path,cfs)); + } + } + revision_lookup[e.revision]=edges.insert(e).first; + } + } + if (global_sanity.debug) L(F("%s") % debug()); +} + +struct cvs_repository::update_cb : cvs_client::update_callbacks +{ cvs_repository &repo; + std::vector &results; + + update_cb(cvs_repository &r, std::vector &re) + : repo(r), results(re) {} + virtual void operator()(const cvs_client::update &u) const + { results.push_back(u); + // perhaps store the file contents into the db to save storage + } +}; + +void cvs_repository::update() +{ std::set::iterator now_iter=last_known_revision(); + const cvs_edge &now=*now_iter; + I(!now.revision().empty()); + std::vector file_revisions; + std::vector results; + const cvs_manifest &m=get_files(now); + file_revisions.reserve(m.size()); + for (cvs_manifest::const_iterator i=m.begin();i!=m.end();++i) + file_revisions.push_back(update_args(i->first,i->second->cvs_version, + std::string(),i->second->keyword_substitution)); + Update(file_revisions,update_cb(*this,results)); + for (std::vector::const_iterator i=results.begin();i!=results.end();++i) + { // 2do: use tags + cvs_manifest::const_iterator now_file=m.find(i->file); + std::string last_known_revision; + std::map::iterator f=files.find(i->file); + + if (now_file!=m.end()) + { last_known_revision=now_file->second->cvs_version; + I(f!=files.end()); + } + else // the file is not present in our last import + // e.g. the file is currently dead but we know an old revision + { if (f!=files.end() // we know anything about this file + && !f->second.known_states.empty()) // we have at least one known file revision + { std::set::const_iterator last=f->second.known_states.end(); + --last; + last_known_revision=last->cvs_version; + } + else f=files.insert(std::make_pair(i->file,file_history())).first; + } + if (last_known_revision=="1.1.1.1") + last_known_revision="1.1"; + std::set::const_iterator last=f->second.known_states.end(); + if (last!=f->second.known_states.begin()) --last; + + if (last_known_revision.empty()) + Log(prime_log_cb(*this,f),i->file.c_str(),"-b","-N",(void*)0); + else + // -b causes -r to get ignored on 0.12 + Log(prime_log_cb(*this,f),i->file.c_str(),/*"-b",*/"-N", + ("-r"+last_known_revision+"::").c_str(),(void*)0); + + std::string file_contents,initial_contents; + if(last==f->second.known_states.end()) + { last=f->second.known_states.begin(); + I(last!=f->second.known_states.end()); + std::set::iterator s2=last; + cvs_client::checkout c=CheckOut(i->file,s2->cvs_version); + store_checkout(s2,c,file_contents); + } + else + { I(!last->sha1sum().empty()); + file_data dat; + app.db.get_file_version(last->sha1sum,dat); + data unpacked; + unpack(dat.inner(), unpacked); + file_contents=unpacked(); + initial_contents=file_contents; + } + for (std::set::const_iterator s=last; + s!=f->second.known_states.end();++s) + { std::set::const_iterator s2=s; + ++s2; + if (s2==f->second.known_states.end()) break; + if (s2->cvs_version==i->new_revision) + { // we do not need to ask the host, we already did ... + store_update(last,s2,*i,initial_contents); + break; + } + else + { update(s,s2,i->file,file_contents); + } + } + } + drop_connection(); + + std::set::iterator dummy_iter=now_iter; + ++dummy_iter; + join_edge_parts(dummy_iter); + + fill_manifests(dummy_iter); + if (global_sanity.debug) L(F("%s") % debug()); + commit_revisions(dummy_iter); +} + +static void apply_manifest_delta(cvs_manifest &base,const cvs_manifest &delta) +{ L(F("apply_manifest_delta: base %d delta %d\n") % base.size() % delta.size()); + for (cvs_manifest::const_iterator i=delta.begin(); i!=delta.end(); ++i) + { if (i->second->dead) + { cvs_manifest::iterator to_remove=base.find(i->first); + I(to_remove!=base.end()); + base.erase(to_remove); + } + else + base[i->first]=i->second; + } + L(F("apply_manifest_delta: result %d\n") % base.size()); +} + +const cvs_manifest &cvs_repository::get_files(const cvs_edge &e) +{ L(F("get_files(%d %s) %s %d\n") % e.time % e.revision % e.delta_base % e.xfiles.size()); + if (!e.delta_base.inner()().empty()) + { cvs_manifest calculated_manifest; + // this is non-recursive by reason ... + const cvs_edge *current=&e; + std::vector deltas; + while (!current->delta_base.inner()().empty()) + { L(F("get_files: looking for base rev %s\n") % current->delta_base); + deltas.push_back(current); + std::map::iterator>::const_iterator + cache_item=revision_lookup.find(current->delta_base); + I(cache_item!=revision_lookup.end()); + current=&*(cache_item->second); + } + I(current->delta_base.inner()().empty()); + calculated_manifest=current->xfiles; + for (std::vector::const_reverse_iterator i=deltas.rbegin(); + i!=static_cast::const_reverse_iterator>(deltas.rend()); + ++i) + apply_manifest_delta(calculated_manifest,(*i)->xfiles); + e.xfiles=calculated_manifest; + e.delta_base=revision_id(); + } + return e.xfiles; +} + +void cvs_sync::admin(const std::string &command, const std::string &arg, + app_state &app) +{ +#if 0 // neither used (nor finished) command to migrate pre0.17 certs + if (command=="compact") + { test_key_availability(app); + std::string::size_type slash=arg.rfind('/'); + I(slash!=std::string::npos); + cvs_sync::cvs_repository repo(app,arg.substr(0,slash),arg.substr(slash+1),false); + transaction_guard guard(app.db); + + { std::vector< revision > certs; + app.db.get_revision_certs(cvs_cert_name, certs); + // erase_bogus_certs ? + repo.process_certs(certs); + } + + guard.commit(); + } +#endif + // we default to the first repository found (which might not be what you wanted) + if (command=="manifest" && arg.size()==constants::idlen) + { revision_id rid(arg); + // easy but not very efficient way, better would be to retrieve revisions + // recursively (perhaps?) + std::vector< revision > certs; + app.db.get_revision_certs(rid,cvs_cert_name,certs); + N(!certs.empty(),F("revision has no 'cvs-revisions' certificates\n")); + + cert_value cvs_revisions; + decode_base64(certs.front().inner().value, cvs_revisions); + std::string::size_type nl=cvs_revisions().find('\n'); + I(nl!=std::string::npos); + std::string line=cvs_revisions().substr(0,nl); + std::string::size_type slash=line.rfind('/'); + I(slash!=std::string::npos); + cvs_sync::cvs_repository repo(app,line.substr(0,slash),line.substr(slash+1),false); + app.db.get_revision_certs(cvs_cert_name, certs); + // erase_bogus_certs ? + repo.process_certs(certs); + std::cout << line << '\n'; + std::cout << debug_manifest(repo.get_files(rid)); + return; + } + +} + +const cvs_manifest &cvs_repository::get_files(const revision_id &rid) +{ std::map::iterator>::const_iterator + cache_item=revision_lookup.find(rid); + I(cache_item!=revision_lookup.end()); + return get_files(*(cache_item->second)); +} --- cvs_sync.hh +++ cvs_sync.hh @@ -0,0 +1,177 @@ +// copyright (C) 2005 Christof Petig +// all rights reserved. +// licensed to the public under the terms of the GNU GPL (>= 2) +// see the file COPYING for details + +#include +#include +#include +#include +#include "sanity.hh" +#include "cvs_client.hh" +#include "constants.hh" +#include "app_state.hh" +#include "packet.hh" + +namespace cvs_sync { +struct cvs_revision_nr +{ std::vector parts; + + cvs_revision_nr(const std::string &x); + void operator++(int); + std::string get_string() const; + bool is_branch() const; + bool is_parent_of(const cvs_revision_nr &child) const; + bool operator==(const cvs_revision_nr &b) const; + bool operator<(const cvs_revision_nr &b) const; +}; + +struct file_state +{ time_t since_when; + std::string cvs_version; // cvs_revision_nr ? + unsigned size; + unsigned patchsize; + bool dead; + std::string md5sum; + hexenc sha1sum; // make this a file_id + std::string log_msg; + std::string author; + std::string keyword_substitution; + + file_state() : since_when(), size(), patchsize(), dead() {} + file_state(time_t sw,const std::string &rev,bool d=false) + : since_when(sw), cvs_version(rev), size(), patchsize(), dead(d) {} + bool operator==(const file_state &b) const + { return since_when==b.since_when && cvs_version==cvs_version; } + bool operator<(const file_state &b) const; +}; + +struct file_history +{ std::set known_states; +}; + +typedef std::set::const_iterator cvs_file_state; + +// state of the files at a specific point in history, dead files do not occur here +typedef std::map cvs_manifest; + +struct cvs_edge // careful this name is also used in cvs_import +{ + std::string changelog; + bool changelog_valid; + std::string author; + time_t time; + mutable time_t time2; + mutable revision_id delta_base; + // delta encoded if !delta_base().empty() + mutable cvs_manifest xfiles; // manifest (or use cvs_manifest) + mutable hexenc revision; // monotone revision + // make this a revision_id + + // I do not want this to be 3 hours (how comes?) + static size_t const cvs_window = 5; + + cvs_edge() : changelog_valid(), time(), time2() {} + cvs_edge(time_t when) : changelog_valid(), time(when), time2(when) {} + cvs_edge(const std::string &log, time_t when, const std::string &auth) + : changelog(log), changelog_valid(true), author(auth), time(when), time2(when) + {} + cvs_edge(const revision_id &rid,app_state &app); + + bool similar_enough(cvs_edge const & other) const; + inline bool operator==(cvs_edge const & other) const + { + return // branch == other.branch && + changelog == other.changelog && + author == other.author && + time == other.time; + } + bool operator<(cvs_edge const & other) const; +}; + +bool operator<(const file_state &,const cvs_edge &); +bool operator<=(const file_state &,const cvs_edge &); + +class cvs_repository : public cvs_client +{ +public: + typedef cvs_manifest tree_state_t; + struct prime_log_cb; + struct get_all_files_log_cb; + struct get_all_files_list_cb; + struct update_cb; + +private: + std::set edges; + std::map::iterator> revision_lookup; + std::map files; + // tag,file,rev + std::map > tags; + + app_state &app; + std::auto_ptr file_id_ticker; + std::auto_ptr revision_ticker; + std::auto_ptr cvs_edges_ticker; + + // for delta encoding of files + std::set remove_set; // remove_state lives here + cvs_file_state remove_state; + + time_t sync_since; + + void check_split(const cvs_file_state &s, const cvs_file_state &end, + const std::set::iterator &e); + void get_all_files(); + void update(std::set::const_iterator s, + std::set::iterator s2,const std::string &file, + std::string &contents); + void store_checkout(std::set::iterator s2, + const cvs_client::checkout &file, std::string &file_contents); + void store_update(std::set::const_iterator s, + std::set::iterator s2,const cvs_client::update &u, + std::string &file_contents); + void fill_manifests(std::set::iterator e); + void commit_revisions(std::set::iterator e); + + void store_contents(const std::string &contents, hexenc &sha1sum); +// void apply_delta(std::string &contents, const std::string &patch); + void store_delta(const std::string &new_contents, const std::string &old_contents, const std::string &patch, const hexenc &from, hexenc &to); + + void cert_cvs(const cvs_edge &e, packet_consumer & pc); + cvs_file_state remember(std::set &s,const file_state &fs); + void join_edge_parts(std::set::iterator i); + std::set::iterator last_known_revision(); + std::set::iterator commit( + std::set::iterator parent, const revision_id &rid); + const cvs_manifest &get_files(const cvs_edge &e); + +public: // semi public interface for push/pull + void prime(); + void update(); + void commit(); + void process_certs(const std::vector< revision > &certs); + bool empty() const { return edges.empty() && files.empty(); } + + const cvs_manifest &get_files(const revision_id &e); + + static time_t posix2time_t(std::string s); + +public: + cvs_repository(app_state &app, const std::string &repository, const std::string &module, bool connect=true); + + std::string debug() const; + +#if 0 // yet unimplemented and unneeded ~nice~ API ideas + std::list get_modules(); + void set_branch(const std::string &tag); + const tree_state_t &find(const std::string &date,const std::string &changelog); + const tree_state_t &next(const tree_state_t &m) const; +#endif +}; + +void pull(const std::string &repository, const std::string &module, + app_state &app); +void push(const std::string &repository, const std::string &module, + app_state &app); +void admin(const std::string &command, const std::string &arg, app_state &app); +} // end namespace cvs_sync --- monotone.cc +++ monotone.cc @@ -44,6 +44,7 @@ #define OPT_MESSAGE 15 #define OPT_ROOT 16 #define OPT_DEPTH 17 +#define OPT_SINCE 9999 // use a custom number ... #define OPT_ARGFILE 18 // main option processing and exception handling code @@ -72,6 +73,7 @@ {"message", 'm', POPT_ARG_STRING, &argstr, OPT_MESSAGE, "set commit changelog message", NULL}, {"root", 0, POPT_ARG_STRING, &argstr, OPT_ROOT, "limit search for working copy to specified root", NULL}, {"depth", 0, POPT_ARG_LONG, &arglong, OPT_DEPTH, "limit the log output to the given number of entries", NULL}, + {"since", 0, POPT_ARG_STRING, &argstr, OPT_SINCE, "set history start for CVS sync", NULL}, {"xargs", '@', POPT_ARG_STRING, &argstr, OPT_ARGFILE, "insert command line arguments taken from the given file", NULL}, { NULL, 0, 0, NULL, 0 } }; @@ -332,6 +334,10 @@ app.set_depth(arglong); break; + case OPT_SINCE: + app.set_since(string(argstr)); + break; + case OPT_ARGFILE: sub_argvs.push_back(my_poptStuffArgFile(ctx(), utf8(string(argstr)))); --- tests.cvs/cleanup +++ tests.cvs/cleanup @@ -0,0 +1,4 @@ +#!/bin/sh +rm -rf *~ test.cvsroot test.db test.db-journal test.mt test.cvstemp +chmod u+x cleanup cvs_test_setup cvs_test_setup2 cvs_test_setup3 \ + db_setup db_setup2 show_disk_usage test_all --- tests.cvs/cvs_test_setup +++ tests.cvs/cvs_test_setup @@ -0,0 +1,59 @@ +#!/bin/sh +if [ -e test.cvsroot ] +then + echo "Test CVS root Directory already there." + exit 1 +fi +mkdir test.cvsroot || exit 1 +CVSROOT=$PWD/test.cvsroot +cvs -Q -d $CVSROOT init + +if [ -e test.cvstemp ] +then + echo "Test CVS temp Directory already there." + exit 1 +fi +mkdir test.cvstemp || exit 1 +mkdir test.cvstemp/import || exit 1 +cd $CVSROOT/../test.cvstemp/import +echo "initial import" >A +echo "initial "`date` `TZ=UTC date` `date +%s` +cvs -Q -d $CVSROOT import -m "initial import" test vendor_tag initial_import + +if [ "$1" = "import" ] +then + cd $CVSROOT/.. + rm -rf test.cvstemp || exit 1 + exit 0 +fi + +cd $CVSROOT/../test.cvstemp +cvs -Q -d $CVSROOT co test +cd test +echo "file added" >B +cvs -Q add B +sleep 1 +cvs -Q ci -m "B added" + +cvs -Q delete -f A +sleep 1 +cvs -Q ci -m "A removed" + +echo "something different" >B +sleep 1 +cvs -Q ci -m "B changed" + +mkdir dir +cvs -Q add dir +echo "some subdir" > dir/D +cvs -Q add dir/D +sleep 1 +cvs -Q ci -m "dir/D added" + +echo "revived" >A +cvs -Q add A +sleep 1 +cvs -Q ci -m "A readded" + +cd $CVSROOT/.. +rm -rf test.cvstemp || exit 1 --- tests.cvs/cvs_test_setup2 +++ tests.cvs/cvs_test_setup2 @@ -0,0 +1,33 @@ +#!/bin/sh +if [ ! -e test.cvsroot ] +then + echo "Test CVS root Directory not already there." + exit 1 +fi +CVSROOT=$PWD/test.cvsroot + +if [ -e test.cvstemp ] +then + echo "Test CVS working Directory already there." + exit 1 +fi + +mkdir test.cvstemp +cd test.cvstemp +cvs -Q -d $CVSROOT co test +cd test + +echo "yet another change" >B +echo "yet another change" >A +sleep 1 +cvs -Q ci -m "A+B changed 2" + +sleep 1 +cvs -Q ci -m "B changed 2" + +echo "yet another change 2" >B +sleep 1 +cvs -Q ci -m "B changed 3" + +cd $CVSROOT/.. +rm -rf test.cvstemp || exit 1 --- tests.cvs/cvs_test_setup3 +++ tests.cvs/cvs_test_setup3 @@ -0,0 +1,34 @@ +#!/bin/sh +if [ ! -e test.cvsroot ] +then + echo "Test CVS root Directory not already there." + exit 1 +fi +CVSROOT=$PWD/test.cvsroot + +if [ -e test.cvstemp ] +then + echo "Test CVS working Directory already there." + exit 1 +fi + +mkdir test.cvstemp +cd test.cvstemp +cvs -Q -d $CVSROOT co test +cd test + +echo "and now change D" >dir/D +sleep 1 +cvs -Q ci -m "D changed" + +cvs -Q delete -f B +sleep 1 +cvs -Q ci -m "B remove" + +echo "and now add C" >dir/C +cvs -Q add dir/C +sleep 1 +cvs -Q ci -m "C added" + +cd $CVSROOT/.. +rm -rf test.cvstemp || exit 1 --- tests.cvs/cvs_test_setup4 +++ tests.cvs/cvs_test_setup4 @@ -0,0 +1,31 @@ +#!/bin/sh +if [ ! -e test.cvsroot ] +then + echo "Test CVS root Directory not already there." + exit 1 +fi +CVSROOT=$PWD/test.cvsroot + +if [ -e test.cvstemp ] +then + echo "Test CVS working Directory already there." + exit 1 +fi + +mkdir test.cvstemp +cd test.cvstemp +cvs -Q -d $CVSROOT co test +cd test + +# rapidly check in with different logs to check the merge algorithm +echo "fast A" >A +echo "fast C" >dir/C +echo "fast D" >dir/D +cvs -Q add A +sleep 1 +cvs -Q ci -m "fast A change" A +cvs -Q ci -m "fast C change" dir/C +cvs -Q ci -m "fast D change" dir/D + +cd $CVSROOT/.. +rm -rf test.cvstemp || exit 1 --- tests.cvs/cvs_test_setup5 +++ tests.cvs/cvs_test_setup5 @@ -0,0 +1,43 @@ +#!/bin/sh +if [ ! -e test.cvsroot ] +then + echo "Test CVS root Directory not already there." + exit 1 +fi +CVSROOT=$PWD/test.cvsroot + +if [ -e test.cvstemp ] +then + echo "Test CVS working Directory already there." + exit 1 +fi + +mkdir $CVSROOT/test2 + +mkdir test.cvstemp +cd test.cvstemp +cvs -Q -d $CVSROOT co CVSROOT +cd CVSROOT +echo >>modules test1 test +echo >>modules test3 test2 '&test1' +cvs -Q ci -m "module setup" +cd .. + +cvs -Q -d $CVSROOT co test3 +cd test3 + +echo >AA initial AA +cvs -Q add AA +cvs -Q ci -m "AA added" + +mkdir sub2 +cvs -Q add sub2 +echo >sub2/BB initial BB +cvs -Q add sub2/BB +cvs -Q ci -m "sub2/BB added" + +echo >test1/A cross module change +cvs -Q ci -m "cross module change of A" + +cd $CVSROOT/.. +rm -rf test.cvstemp || exit 1 --- tests.cvs/db_setup +++ tests.cvs/db_setup @@ -0,0 +1,27 @@ +#!/bin/sh +../monotone --db test.db db init +../monotone --db test.db read <dir/E +../../monotone add dir/E || exit +../../monotone --key address@hidden --branch test --db ../test.db commit \ + --message "dir/E added" << EOF +x +EOF +if [ $? -ne 0 ] ; then exit ; fi + +echo "changed by monotone" >dir/C +sleep 1 +../../monotone --key address@hidden --branch test --db ../test.db commit \ + --message "dir/C changed" << EOF +x +EOF +if [ $? -ne 0 ] ; then exit ; fi + +../../monotone drop A +sleep 1 +../../monotone --key address@hidden --branch test --db ../test.db commit \ + --message "A removed" << EOF +x +EOF +if [ $? -ne 0 ] ; then exit ; fi --- tests.cvs/show_disk_usage +++ tests.cvs/show_disk_usage @@ -0,0 +1,42 @@ +#!/bin/sh +if [ -z "$1" ] +then + echo "USAGE: $0 " + exit 1 +fi + +sqlite3 $1 <