git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
* [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs
@ 2012-10-14  5:42 Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 01/20] cvsserver t9400: add basic 'cvs log' test Matthew Ogilvie
                   ` (20 more replies)
  0 siblings, 21 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

This patch series improves git-cvsserver's handling and support
of refspecs requested by the client.

Disclaimer: I don't actually intend to use any of this myself.
I suspect no one really cares about cvsserver (I don't even really
care myself).  See also below.

-----------
Features:
-----------

1. You can now switch branches with "cvs update -r REFSPEC",
   "cvs update -A", "cvs checkout -r ...", etc.  It becomes sticky for
   plain "cvs update" until reset (like standard CVS).  You can
   add/remove/commit to other branches updated this way.
      The old "cvs checkout moduleThatIsReallyABranch" technique
   still works; it becomes the main head branch from CVS's perspective.
   CVS revision number allocation is not synchronized between different
   modules checkout out in this old way.
2. You can create arbitrary diffs and patches with commands such as
   "cvs diff -r REFSPEC [-r REFSPEC]", over files, directories, or whole
   trees.  (It used to effectively be limited to individual files,
   and CVS-specific revision numbers (no tags).)
3. The REFSPECs can refer to CVS revision numbers, git commit IDs,
   git tags, git branches, or other git refspecs.  There is an escape
   mechanism you can use if your CVS client won't let you feed in
   invalid CVS tags that contain ".", "/", or similar characters.
4. No DB schema changes for now.  You can downgrade back to previous
   versions, and the only things that may break are existing sandboxes
   that were explicitly using the new features (had been set to a sticky
   branch with cvs update -r).

-----------
Limitations:
-----------

1. CVS revision numbers (other than those on the main
   branch) are not "real".  Instead, they are git commit ID's
   encoded to look like valid CVS revision numbers.  Subsequent versions
   do not have related CVS revision numbers at all.
   (Most of the code in this patch series would probably still be
   useful if someone enhances CVS revision numbers in the future.)
2. "CVS log" does not attempt to show versions on other than the "main"
   branch, even if a non-main branch is currently checked out.  It also
   doesn't show any branch or tag names.
3. Performance: When updating, it attempts to find a main-branch
   CVS revision number, or failing that, restrict itself to
   the specific git commit ID where the file most recently
   changed.  Searching for these (on an individual file
   basis) can currently be rather expensive, and probably
   doesn't scale well.  There are several possible ways it might be
   optimized, but no optimization has been done.  See comments in the
   patch.
4. I've done almost no testing, other than running both old and new
   unit tests (t9400, t9401, and t9402).
5. Testing Eclipse: I needed to make some changes to the
   details of what cvs update sends for the CVS protocol, even
   in the non-branch case.  I think I've made it closer to being
   the same as a standard CVS server (based on some captured
   conversations), but it still isn't "perfect".
   With luck, reputedly-finicky clients like Eclipse will still
   work correctly, but I haven't actually verified this.
6. Pre-existing issue: There are cases where the server will ask the
   client to replace user-modified files without saving those modifications
   off to the side first (.# files).  I haven't fixed these, but the fourth
   patch adds a comment about one such case.
7. Coding style: git-cvsserver mostly doesn't use git's main coding style.
   In fact, different parts of the file seem to use different
   coding styles.  I've made a superficial attempt to have each change
   keep relatively close to nearby coding style.  But larger chunks
   of code tend to stick to a hybrid of what seems most predominant
   in the file as a whole, and my own personally preferred style.

-----------
Disclaimer:
-----------

I don't actually intend to use any of this myself.

I started it a few years ago under the theory that a more accurate
emulation of CVS would make it easier to convince the team at $DAYJOB
to switch to git, but we eventually switched without using
git-cvsserver at all.  I've been working on this on and off (mostly
off) out of a vague sense of stubborness.

Depending on overall interest in this feature (most likely not
much) vs how many adjustments maintainers want, I might gradually get
this into a ready-to-include state (possibly weeks or months).  But
my primary goal here is just to have it available publicly
somewhere (like the mailing list) where someone who really
wants features like these can use these patches as a starting
point.  That said, perhaps some of the trivial cleanup
patches could go in now?

-------------------------------

Matthew Ogilvie (20):
  cvsserver t9400: add basic 'cvs log' test
  cvsserver: removed unused sha1Or-k mode from kopts_from_path
  cvsserver: add comments about database schema/usage
  cvsserver update: comment about how we shouldn't remove a
    user-modified file
  cvsserver: remove unused functions _headrev and gethistory
  cvsserver: clean up client request handler map comments
  cvsserver: split up long lines in req_{status,diff,log}
  cvsserver: use whole CVS rev number in-process; don't strip "1."
    prefix
  cvsserver: cvs add: do not expand directory arguments
  cvsserver status: provide real sticky info
  cvsserver: factor out git-log parsing logic
  cvsserver: cleanup extra slashes in filename arguments
  cvsserver: define a tag name character escape mechanism
  cvsserver: add misc commit lookup, file meta data, and file listing
    functions
  cvsserver: implement req_Sticky and related utilities
  cvsserver: generalize getmeta() to recognize commit refs
  cvsserver: Add version awareness to argsfromdir
  cvsserver: support -r and sticky tags for most operations
  cvsserver: add t9402 to test branch and tag refs
  cvsserver Documentation: new cvs ... -r support

 Documentation/git-cvsserver.txt |   37 +
 git-cvsserver.perl              | 2433 +++++++++++++++++++++++++++++++--------
 t/t9400-git-cvsserver-server.sh |   72 +-
 t/t9401-git-cvsserver-crlf.sh   |   35 +
 t/t9402-git-cvsserver-refs.sh   |  558 +++++++++
 5 files changed, 2624 insertions(+), 511 deletions(-)
 create mode 100755 t/t9402-git-cvsserver-refs.sh

-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply	[flat|nested] 22+ messages in thread

* [PATCH 01/20] cvsserver t9400: add basic 'cvs log' test
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 02/20] cvsserver: removed unused sha1Or-k mode from kopts_from_path Matthew Ogilvie
                   ` (19 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

'cvs log' output is arguably deficient in a number of ways
(see the comment added with the test), but add a test for
the current output to detect for accidental regressions.

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 t/t9400-git-cvsserver-server.sh | 70 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh
index 806623e..6c693ff 100755
--- a/t/t9400-git-cvsserver-server.sh
+++ b/t/t9400-git-cvsserver-server.sh
@@ -505,6 +505,76 @@ test_expect_success 'cvs co -c (shows module database)' '
 '
 
 #------------
+# CVS LOG
+#------------
+
+# Known issues with git-cvsserver current log output:
+#  - Hard coded "lines: +2 -3" placeholder, instead of real numbers.
+#  - CVS normally does not internally add a blank first line
+#    nor a last line with nothing but a space to log messages.
+#  - The latest cvs 1.12.x server sends +0000 timezone (with some hidden "MT"
+#    tagging in the protocol), and if cvs 1.12.x client sees the MT tags,
+#    it converts to local time zone.  git-cvsserver doesn't do the +0000
+#    or the MT tags...
+#  - The latest 1.12.x releases add a "commitid:" field on to the end of the
+#    "date:" line (after "lines:").  Maybe we could stick git's commit id
+#    in it?  Or does CVS expect a certain number of bits (too few for
+#    a full sha1)?
+#
+# Given the above, expect the following test to break if git-cvsserver's
+# log output is improved.  The test is just to ensure it doesn't
+# accidentally get worse.
+
+sed -e 's/^x//' -e 's/SP$/ /' > "$WORKDIR/expect" <<EOF
+x
+xRCS file: $WORKDIR/gitcvs.git/master/merge,v
+xWorking file: merge
+xhead: 1.4
+xbranch:
+xlocks: strict
+xaccess list:
+xsymbolic names:
+xkeyword substitution: kv
+xtotal revisions: 4;	selected revisions: 4
+xdescription:
+x----------------------------
+xrevision 1.4
+xdate: __DATE__;  author: author;  state: Exp;  lines: +2 -3
+x
+xMerge test (no-op)
+xSP
+x----------------------------
+xrevision 1.3
+xdate: __DATE__;  author: author;  state: Exp;  lines: +2 -3
+x
+xMerge test (conflict)
+xSP
+x----------------------------
+xrevision 1.2
+xdate: __DATE__;  author: author;  state: Exp;  lines: +2 -3
+x
+xMerge test (merge)
+xSP
+x----------------------------
+xrevision 1.1
+xdate: __DATE__;  author: author;  state: Exp;  lines: +2 -3
+x
+xMerge test (pre-merge)
+xSP
+x=============================================================================
+EOF
+expectStat="$?"
+
+cd "$WORKDIR"
+test_expect_success 'cvs log' '
+    cd cvswork &&
+    test x"$expectStat" = x"0" &&
+    GIT_CONFIG="$git_config" cvs log merge >../out &&
+    sed -e "s%2[0-9][0-9][0-9]/[01][0-9]/[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9]%__DATE__%" ../out > ../actual &&
+    test_cmp ../expect ../actual
+'
+
+#------------
 # CVS ANNOTATE
 #------------
 
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 02/20] cvsserver: removed unused sha1Or-k mode from kopts_from_path
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 01/20] cvsserver t9400: add basic 'cvs log' test Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 03/20] cvsserver: add comments about database schema/usage Matthew Ogilvie
                   ` (18 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

sha1Or-k was a vestige from an early, never-released
attempt to handle some oddball cases of CRLF conversion (-k option).
Ultimately it wasn't needed, and I should have gotten rid of it
before submitting the CRLF patch in the first place.

See also 90948a42892779 (add ability to guess -kb from contents).

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 38 +++++---------------------------------
 1 file changed, 5 insertions(+), 33 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index b8eddab..f43d287 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -2444,42 +2444,14 @@ sub kopts_from_path
         }
         elsif( ($cfg->{gitcvs}{allbinary} =~ /^\s*guess\s*$/i) )
         {
-            if( $srcType eq "sha1Or-k" &&
-                !defined($name) )
+            if( is_binary($srcType,$name) )
             {
-                my ($ret)=$state->{entries}{$path}{options};
-                if( !defined($ret) )
-                {
-                    $ret=$state->{opt}{k};
-                    if(defined($ret))
-                    {
-                        $ret="-k$ret";
-                    }
-                    else
-                    {
-                        $ret="";
-                    }
-                }
-                if( ! ($ret=~/^(|-kb|-kkv|-kkvl|-kk|-ko|-kv)$/) )
-                {
-                    print "E Bad -k option\n";
-                    $log->warn("Bad -k option: $ret");
-                    die "Error: Bad -k option: $ret\n";
-                }
-
-                return $ret;
+                $log->debug("... as binary");
+                return "-kb";
             }
             else
             {
-                if( is_binary($srcType,$name) )
-                {
-                    $log->debug("... as binary");
-                    return "-kb";
-                }
-                else
-                {
-                    $log->debug("... as text");
-                }
+                $log->debug("... as text");
             }
         }
     }
@@ -2586,7 +2558,7 @@ sub open_blob_or_die
             die "Unable to open file $name: $!\n";
         }
     }
-    elsif( $srcType eq "sha1" || $srcType eq "sha1Or-k" )
+    elsif( $srcType eq "sha1" )
     {
         unless ( defined ( $name ) and $name =~ /^[a-zA-Z0-9]{40}$/ )
         {
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 03/20] cvsserver: add comments about database schema/usage
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 01/20] cvsserver t9400: add basic 'cvs log' test Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 02/20] cvsserver: removed unused sha1Or-k mode from kopts_from_path Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 04/20] cvsserver update: comment about how we shouldn't remove a user-modified file Matthew Ogilvie
                   ` (17 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

No functionality changes, but these comments should make it easier to
understand how it works.

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 46 ++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 42 insertions(+), 4 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index f43d287..595865c 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -2129,6 +2129,10 @@ sub statecleanup
     $state->{entries} = {};
 }
 
+# Return working directory revision int "X" from CVS revision "1.X" out
+# of the the working directory "entries" state, for the given filename.
+# Return negative "X" to represent the file is scheduled for removal
+# when it is committed.
 sub revparse
 {
     my $filename = shift;
@@ -2901,6 +2905,16 @@ sub new
     }
 
     # Construct the revision table if required
+    # The revision table stores an entry for each file, each time that file
+    # changes.
+    #   numberOfRecords = O( numCommits * averageNumChangedFilesPerCommit )
+    # This is not sufficient to support "-r {commithash}" for any
+    # files except files that were modified by that commit (also,
+    # some places in the code ignore/effectively strip out -r in
+    # some cases, before it gets passed to getmeta()).
+    # The "filehash" field typically has a git blob hash, but can also
+    # be set to "dead" to indicate that the given version of the file
+    # should not exist in the sandbox.
     unless ( $self->{tables}{$self->tablename("revision")} )
     {
         my $tablename = $self->tablename("revision");
@@ -2928,6 +2942,15 @@ sub new
     }
 
     # Construct the head table if required
+    # The head table (along with the "last_commit" entry in the property
+    # table) is the persisted working state of the "sub update" subroutine.
+    # All of it's data is read entirely first, and completely recreated
+    # last, every time "sub update" runs.
+    # This is also used by "sub getmeta" when it is asked for the latest
+    # version of a file (as opposed to some specific version).
+    # Another way of thinking about it is as a single slice out of
+    # "revisions", giving just the most recent revision information for
+    # each file.
     unless ( $self->{tables}{$self->tablename("head")} )
     {
         my $tablename = $self->tablename("head");
@@ -2950,6 +2973,7 @@ sub new
     }
 
     # Construct the properties table if required
+    #  - "last_commit" - Used by "sub update".
     unless ( $self->{tables}{$self->tablename("properties")} )
     {
         my $tablename = $self->tablename("properties");
@@ -2962,6 +2986,11 @@ sub new
     }
 
     # Construct the commitmsgs table if required
+    # The commitmsgs table is only used for merge commits, since
+    # "sub update" will only keep one branch of parents.  Shortlogs
+    # for ignored commits (i.e. not on the chosen branch) will be used
+    # to construct a replacement "collapsed" merge commit message,
+    # which will be stored in this table.  See also "sub commitmessage".
     unless ( $self->{tables}{$self->tablename("commitmsgs")} )
     {
         my $tablename = $self->tablename("commitmsgs");
@@ -2993,6 +3022,14 @@ sub tablename
 
 =head2 update
 
+Bring the database up to date with the latest changes from
+the git repository.
+
+Internal working state is read out of the "head" table and the
+"last_commit" property, then it updates "revisions" based on that, and
+finally it writes the new internal state back to the "head" table
+so it can be used as a starting point the next time update is called.
+
 =cut
 sub update
 {
@@ -3106,17 +3143,18 @@ sub update
                 next;
             } elsif (@{$commit->{parents}} > 1) {
                 # it is a merge commit, for each parent that is
-                # not $lastpicked, see if we can get a log
+                # not $lastpicked (not given a CVS revision number),
+                # see if we can get a log
                 # from the merge-base to that parent to put it
                 # in the message as a merge summary.
                 my @parents = @{$commit->{parents}};
                 foreach my $parent (@parents) {
-                    # git-merge-base can potentially (but rarely) throw
-                    # several candidate merge bases. let's assume
-                    # that the first one is the best one.
                     if ($parent eq $lastpicked) {
                         next;
                     }
+                    # git-merge-base can potentially (but rarely) throw
+                    # several candidate merge bases. let's assume
+                    # that the first one is the best one.
 		    my $base = eval {
 			    safe_pipe_capture('git', 'merge-base',
 						 $lastpicked, $parent);
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 04/20] cvsserver update: comment about how we shouldn't remove a user-modified file
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (2 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 03/20] cvsserver: add comments about database schema/usage Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 05/20] cvsserver: remove unused functions _headrev and gethistory Matthew Ogilvie
                   ` (16 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Instead of a comment, we should really add test cases and actually fix it.

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index 595865c..cc39b6b 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -1144,6 +1144,10 @@ sub req_update
 
         if ( $meta->{filehash} eq "deleted" )
         {
+            # TODO: If it has been modified in the sandbox, error out
+            #   with the appropriate message, rather than deleting a modified
+            #   file.
+
             my ( $filepart, $dirpart ) = filenamesplit($filename,1);
 
             $log->info("Removing '$filename' from working copy (no longer in the repo)");
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 05/20] cvsserver: remove unused functions _headrev and gethistory
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (3 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 04/20] cvsserver update: comment about how we shouldn't remove a user-modified file Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 06/20] cvsserver: clean up client request handler map comments Matthew Ogilvie
                   ` (15 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Remove:
   - _headrev() - It uses similar functionality from getmeta() and gethead().
   - gethistory() - It uses similar functions gethistorydense() and getlog().

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 36 ++++--------------------------------
 1 file changed, 4 insertions(+), 32 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index cc39b6b..8b2accb 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -3440,19 +3440,6 @@ sub insert_head
     $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
 }
 
-sub _headrev
-{
-    my $self = shift;
-    my $filename = shift;
-    my $tablename = $self->tablename("head");
-
-    my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM $tablename WHERE name=?",{},1);
-    $db_query->execute($filename);
-    my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
-
-    return ( $hash, $revision, $mode );
-}
-
 sub _get_prop
 {
     my $self = shift;
@@ -3512,6 +3499,8 @@ sub gethead
 
 =head2 getlog
 
+See also gethistorydense().
+
 =cut
 
 sub getlog
@@ -3597,25 +3586,6 @@ sub commitmessage
     return $message;
 }
 
-=head2 gethistory
-
-This function takes a filename (with path) argument and returns an arrayofarrays
-containing revision,filehash,commithash ordered by revision descending
-
-=cut
-sub gethistory
-{
-    my $self = shift;
-    my $filename = shift;
-    my $tablename = $self->tablename("revision");
-
-    my $db_query;
-    $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
-    $db_query->execute($filename);
-
-    return $db_query->fetchall_arrayref;
-}
-
 =head2 gethistorydense
 
 This function takes a filename (with path) argument and returns an arrayofarrays
@@ -3625,6 +3595,8 @@ This version of gethistory skips deleted entries -- so it is useful for annotate
 The 'dense' part is a reference to a '--dense' option available for git-rev-list
 and other git tools that depend on it.
 
+See also getlog().
+
 =cut
 sub gethistorydense
 {
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 06/20] cvsserver: clean up client request handler map comments
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (4 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 05/20] cvsserver: remove unused functions _headrev and gethistory Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 07/20] cvsserver: split up long lines in req_{status,diff,log} Matthew Ogilvie
                   ` (14 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

  - Comment that it should not be considered a complete list.
  - #'annotate' comment  - Uncommented annotate line is 2 lines earlier.

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index 8b2accb..9e71f30 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -51,6 +51,10 @@ $| = 1;
 
 #### Definition and mappings of functions ####
 
+# NOTE: Despite the existence of req_CATCHALL and req_EMPTY unimplemented
+#  requests, this list is incomplete.  It is missing many rarer/optional
+#  requests.  Perhaps some clients require a claim of support for
+#  these specific requests for main functionality to work?
 my $methods = {
     'Root'            => \&req_Root,
     'Valid-responses' => \&req_Validresponses,
@@ -80,7 +84,6 @@ my $methods = {
     'noop'            => \&req_EMPTY,
     'annotate'        => \&req_annotate,
     'Global_option'   => \&req_Globaloption,
-    #'annotate'        => \&req_CATCHALL,
 };
 
 ##############################################
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 07/20] cvsserver: split up long lines in req_{status,diff,log}
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (5 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 06/20] cvsserver: clean up client request handler map comments Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 08/20] cvsserver: use whole CVS rev number in-process; don't strip "1." prefix Matthew Ogilvie
                   ` (13 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 220 ++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 159 insertions(+), 61 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index 9e71f30..eb8f138 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -1559,10 +1559,12 @@ sub req_status
     #$log->debug("status state : " . Dumper($state));
 
     # Grab a handle to the SQLite db and do any necessary updates
-    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    my $updater;
+    $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
     $updater->update();
 
-    # if no files were specified, we need to work out what files we should be providing status on ...
+    # if no files were specified, we need to work out what files we should
+    # be providing status on ...
     argsfromdir($updater);
 
     # foreach file specified on the command line ...
@@ -1570,14 +1572,19 @@ sub req_status
     {
         $filename = filecleanup($filename);
 
-        next if exists($state->{opt}{l}) && index($filename, '/', length($state->{prependdir})) >= 0;
+        if ( exists($state->{opt}{l}) &&
+             index($filename, '/', length($state->{prependdir})) >= 0 )
+        {
+           next;
+        }
 
         my $meta = $updater->getmeta($filename);
         my $oldmeta = $meta;
 
         my $wrev = revparse($filename);
 
-        # If the working copy is an old revision, lets get that version too for comparison.
+        # If the working copy is an old revision, lets get that
+        # version too for comparison.
         if ( defined($wrev) and $wrev != $meta->{revision} )
         {
             $oldmeta = $updater->getmeta($filename, $wrev);
@@ -1585,49 +1592,95 @@ sub req_status
 
         # TODO : All possible statuses aren't yet implemented
         my $status;
-        # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
-        $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
-                                    and
-                                    ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
-                                      or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
-                                   );
-
-        # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
-        $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
-                                          and
-                                          ( $state->{entries}{$filename}{unchanged}
-                                            or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
-                                        );
-
-        # Need checkout if it exists in the repo but doesn't have a working copy
-        $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
-
-        # Locally modified if working copy and repo copy have the same revision but there are local changes
-        $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
-
-        # Needs Merge if working copy revision is less than repo copy and there are local changes
-        $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
-
-        $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
-        $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
-        $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
-        $status ||= "File had conflicts on merge" if ( 0 );
+        # Files are up to date if the working copy and repo copy have
+        # the same revision, and the working copy is unmodified
+        if ( defined ( $wrev ) and defined($meta->{revision}) and
+             $wrev == $meta->{revision} and
+             ( ( $state->{entries}{$filename}{unchanged} and
+                 ( not defined ( $state->{entries}{$filename}{conflict} ) or
+                   $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
+               ( defined($state->{entries}{$filename}{modified_hash}) and
+                 $state->{entries}{$filename}{modified_hash} eq
+                          $meta->{filehash} ) ) )
+        {
+            $status = "Up-to-date";
+        }
+
+        # Need checkout if the working copy has an older revision than
+        # the repo copy, and the working copy is unmodified
+        if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
+             $meta->{revision} > $wrev and
+             ( $state->{entries}{$filename}{unchanged} or
+               ( defined($state->{entries}{$filename}{modified_hash}) and
+                 $state->{entries}{$filename}{modified_hash} eq
+                                $oldmeta->{filehash} ) ) )
+        {
+            $status ||= "Needs Checkout";
+        }
+
+        # Need checkout if it exists in the repo but doesn't have a working
+        # copy
+        if ( not defined ( $wrev ) and defined ( $meta->{revision} ) )
+        {
+            $status ||= "Needs Checkout";
+        }
+
+        # Locally modified if working copy and repo copy have the
+        # same revision but there are local changes
+        if ( defined ( $wrev ) and defined($meta->{revision}) and
+             $wrev == $meta->{revision} and
+             $state->{entries}{$filename}{modified_filename} )
+        {
+            $status ||= "Locally Modified";
+        }
+
+        # Needs Merge if working copy revision is less than repo copy
+        # and there are local changes
+        if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
+             $meta->{revision} > $wrev and
+             $state->{entries}{$filename}{modified_filename} )
+        {
+            $status ||= "Needs Merge";
+        }
+
+        if ( defined ( $state->{entries}{$filename}{revision} ) and
+             not defined ( $meta->{revision} ) )
+        {
+            $status ||= "Locally Added";
+        }
+        if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
+             -$wrev == $meta->{revision} )
+        {
+            $status ||= "Locally Removed";
+        }
+        if ( defined ( $state->{entries}{$filename}{conflict} ) and
+             $state->{entries}{$filename}{conflict} =~ /^\+=/ )
+        {
+            $status ||= "Unresolved Conflict";
+        }
+        if ( 0 )
+        {
+            $status ||= "File had conflicts on merge";
+        }
 
         $status ||= "Unknown";
 
         my ($filepart) = filenamesplit($filename);
 
-        print "M ===================================================================\n";
+        print "M =======" . ( "=" x 60 ) . "\n";
         print "M File: $filepart\tStatus: $status\n";
         if ( defined($state->{entries}{$filename}{revision}) )
         {
-            print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
+            print "M Working revision:\t" .
+                  $state->{entries}{$filename}{revision} . "\n";
         } else {
             print "M Working revision:\tNo entry for $filename\n";
         }
         if ( defined($meta->{revision}) )
         {
-            print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
+            print "M Repository revision:\t1." .
+                   $meta->{revision} .
+                   "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
             print "M Sticky Tag:\t\t(none)\n";
             print "M Sticky Date:\t\t(none)\n";
             print "M Sticky Options:\t\t(none)\n";
@@ -1661,13 +1714,17 @@ sub req_diff
     $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
     $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
 
-    $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
+    $log->debug("Diffing revisions " .
+                ( defined($revision1) ? $revision1 : "[NULL]" ) .
+                " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
 
     # Grab a handle to the SQLite db and do any necessary updates
-    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    my $updater;
+    $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
     $updater->update();
 
-    # if no files were specified, we need to work out what files we should be providing status on ...
+    # if no files were specified, we need to work out what files we should
+    # be providing status on ...
     argsfromdir($updater);
 
     # foreach file specified on the command line ...
@@ -1722,7 +1779,8 @@ sub req_diff
             $file2 = $state->{entries}{$filename}{modified_filename};
         }
 
-        # if we have been given -r, and we don't have a $file2 yet, lets get one
+        # if we have been given -r, and we don't have a $file2 yet, lets
+        # get one
         if ( defined ( $revision1 ) and not defined ( $file2 ) )
         {
             ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
@@ -1733,21 +1791,37 @@ sub req_diff
         # We need to have retrieved something useful
         next unless ( defined ( $meta1 ) );
 
-        # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
-        next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
-                  and
-                   ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
-                     or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
-                  );
+        # Files to date if the working copy and repo copy have the same
+        # revision, and the working copy is unmodified
+        if ( not defined ( $meta2 ) and $wrev == $meta1->{revision} and
+             ( ( $state->{entries}{$filename}{unchanged} and
+                 ( not defined ( $state->{entries}{$filename}{conflict} ) or
+                   $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
+               ( defined($state->{entries}{$filename}{modified_hash}) and
+                 $state->{entries}{$filename}{modified_hash} eq
+                        $meta1->{filehash} ) ) )
+        {
+            next;
+        }
 
         # Apparently we only show diffs for locally modified files
-        next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
+        unless ( defined($meta2) or
+                 defined ( $state->{entries}{$filename}{modified_filename} ) )
+        {
+            next;
+        }
 
         print "M Index: $filename\n";
-        print "M ===================================================================\n";
+        print "M =======" . ( "=" x 60 ) . "\n";
         print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
-        print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
-        print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
+        if ( defined ( $meta1 ) )
+        {
+            print "M retrieving revision 1.$meta1->{revision}\n"
+        }
+        if ( defined ( $meta2 ) )
+        {
+            print "M retrieving revision 1.$meta2->{revision}\n"
+        }
         print "M diff ";
         foreach my $opt ( keys %{$state->{opt}} )
         {
@@ -1759,18 +1833,27 @@ sub req_diff
                 }
             } else {
                 print "-$opt ";
-                print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
+                if ( defined ( $state->{opt}{$opt} ) )
+                {
+                    print "$state->{opt}{$opt} "
+                }
             }
         }
         print "$filename\n";
 
-        $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
+        $log->info("Diffing $filename -r $meta1->{revision} -r " .
+                   ( $meta2->{revision} or "workingcopy" ));
 
         ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
 
         if ( exists $state->{opt}{u} )
         {
-            system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
+            system("diff -u -L '$filename revision 1.$meta1->{revision}'" .
+                        " -L '$filename " .
+                        ( defined($meta2->{revision}) ?
+                                "revision 1.$meta2->{revision}" :
+                                "working copy" ) .
+                        "' $file1 $file2 > $filediff" );
         } else {
             system("diff $file1 $file2 > $filediff");
         }
@@ -1795,7 +1878,8 @@ sub req_log
     #$log->debug("log state : " . Dumper($state));
 
     my ( $minrev, $maxrev );
-    if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
+    if ( defined ( $state->{opt}{r} ) and
+         $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
     {
         my $control = $2;
         $minrev = $1;
@@ -1806,10 +1890,12 @@ sub req_log
     }
 
     # Grab a handle to the SQLite db and do any necessary updates
-    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    my $updater;
+    $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
     $updater->update();
 
-    # if no files were specified, we need to work out what files we should be providing status on ...
+    # if no files were specified, we need to work out what files we
+    # should be providing status on ...
     argsfromdir($updater);
 
     # foreach file specified on the command line ...
@@ -1825,7 +1911,8 @@ sub req_log
         if ( defined ( $minrev ) )
         {
             $log->debug("Removing revisions less than $minrev");
-            while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
+            while ( scalar(@$revisions) > 0 and
+                    $revisions->[-1]{revision} < $minrev )
             {
                 pop @$revisions;
             }
@@ -1833,7 +1920,8 @@ sub req_log
         if ( defined ( $maxrev ) )
         {
             $log->debug("Removing revisions greater than $maxrev");
-            while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
+            while ( scalar(@$revisions) > 0 and
+                    $revisions->[0]{revision} > $maxrev )
             {
                 shift @$revisions;
             }
@@ -1850,7 +1938,8 @@ sub req_log
         print "M access list:\n";
         print "M symbolic names:\n";
         print "M keyword substitution: kv\n";
-        print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
+        print "M total revisions: $totalrevisions;\tselected revisions: " .
+              scalar(@$revisions) . "\n";
         print "M description:\n";
 
         foreach my $revision ( @$revisions )
@@ -1858,14 +1947,23 @@ sub req_log
             print "M ----------------------------\n";
             print "M revision 1.$revision->{revision}\n";
             # reformat the date for log output
-            $revision->{modified} = sprintf('%04d/%02d/%02d %s', $3, $DATE_LIST->{$2}, $1, $4 ) if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and defined($DATE_LIST->{$2}) );
+            if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and
+                 defined($DATE_LIST->{$2}) )
+            {
+                $revision->{modified} = sprintf('%04d/%02d/%02d %s',
+                                            $3, $DATE_LIST->{$2}, $1, $4 );
+            }
             $revision->{author} = cvs_author($revision->{author});
-            print "M date: $revision->{modified};  author: $revision->{author};  state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . ";  lines: +2 -3\n";
-            my $commitmessage = $updater->commitmessage($revision->{commithash});
+            print "M date: $revision->{modified};" .
+                  "  author: $revision->{author};  state: " .
+                  ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) .
+                  ";  lines: +2 -3\n";
+            my $commitmessage;
+            $commitmessage = $updater->commitmessage($revision->{commithash});
             $commitmessage =~ s/^/M /mg;
             print $commitmessage . "\n";
         }
-        print "M =============================================================================\n";
+        print "M =======" . ( "=" x 70 ) . "\n";
     }
 
     print "ok\n";
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 08/20] cvsserver: use whole CVS rev number in-process; don't strip "1." prefix
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (6 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 07/20] cvsserver: split up long lines in req_{status,diff,log} Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 09/20] cvsserver: cvs add: do not expand directory arguments Matthew Ogilvie
                   ` (12 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Keep track of the whole CVS revision number in-process.  This will
clarify code when we start handling non-linear revision numbers later.

There is one externally visible change: conflict markers after
an update will now include the full CVS revision number,
including the "1." prefix.  It used to leave off the prefix.

Other than the conflict marker, this change doesn't effect
external functionality.  No new features, and the DB schema
is unchanged such that it continues to store just
the stripped rev numbers (without prefix).

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl              | 225 ++++++++++++++++++++++------------------
 t/t9400-git-cvsserver-server.sh |   2 +-
 2 files changed, 123 insertions(+), 104 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index eb8f138..8a7106d 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -554,10 +554,10 @@ sub req_add
         my $meta = $updater->getmeta($filename);
         my $wrev = revparse($filename);
 
-        if ($wrev && $meta && ($wrev < 0))
+        if ($wrev && $meta && ($wrev=~/^-/))
         {
             # previously removed file, add back
-            $log->info("added file $filename was previously removed, send 1.$meta->{revision}");
+            $log->info("added file $filename was previously removed, send $meta->{revision}");
 
             print "MT +updated\n";
             print "MT text U \n";
@@ -574,8 +574,8 @@ sub req_add
 
                 # this is an "entries" line
                 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-                $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
-                print "/$filepart/1.$meta->{revision}//$kopts/\n";
+                $log->debug("/$filepart/$meta->{revision}//$kopts/");
+                print "/$filepart/$meta->{revision}//$kopts/\n";
                 # permissions
                 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
                 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
@@ -683,13 +683,13 @@ sub req_remove
             next;
         }
 
-        if ( defined($wrev) and $wrev < 0 )
+        if ( defined($wrev) and ($wrev=~/^-/) )
         {
             print "E cvs remove: file `$filename' already scheduled for removal\n";
             next;
         }
 
-        unless ( $wrev == $meta->{revision} )
+        unless ( $wrev eq $meta->{revision} )
         {
             # TODO : not sure if the format of this message is quite correct.
             print "E cvs remove: Up to date check failed for `$filename'\n";
@@ -704,7 +704,7 @@ sub req_remove
         print "Checked-in $dirpart\n";
         print "$filename\n";
         my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-        print "/$filepart/-1.$wrev//$kopts/\n";
+        print "/$filepart/-$wrev//$kopts/\n";
 
         $rmcount++;
     }
@@ -995,7 +995,7 @@ sub req_co
 
         # this is an "entries" line
         my $kopts = kopts_from_path($fullName,"sha1",$git->{filehash});
-        print "/$git->{name}/1.$git->{revision}//$kopts/\n";
+        print "/$git->{name}/$git->{revision}//$kopts/\n";
         # permissions
         print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
 
@@ -1083,7 +1083,7 @@ sub req_update
         }
 
         my $meta;
-        if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
+        if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^(1\.\d+)$/ )
         {
             $meta = $updater->getmeta($filename, $1);
         } else {
@@ -1105,7 +1105,7 @@ sub req_update
 	{
 	    $meta = {
 	        name => $filename,
-	        revision => 0,
+	        revision => '0',
 	        filehash => 'added'
 	    };
 	}
@@ -1115,7 +1115,7 @@ sub req_update
         my $wrev = revparse($filename);
 
         # If the working copy is an old revision, lets get that version too for comparison.
-        if ( defined($wrev) and $wrev != $meta->{revision} )
+        if ( defined($wrev) and $wrev ne $meta->{revision} )
         {
             $oldmeta = $updater->getmeta($filename, $wrev);
         }
@@ -1126,7 +1126,7 @@ sub req_update
         # and the working copy is unmodified _and_ the user hasn't specified -C
         next if ( defined ( $wrev )
                   and defined($meta->{revision})
-                  and $wrev == $meta->{revision}
+                  and $wrev eq $meta->{revision}
                   and $state->{entries}{$filename}{unchanged}
                   and not exists ( $state->{opt}{C} ) );
 
@@ -1134,7 +1134,7 @@ sub req_update
         # but the working copy is modified, tell the client it's modified
         if ( defined ( $wrev )
              and defined($meta->{revision})
-             and $wrev == $meta->{revision}
+             and $wrev eq $meta->{revision}
              and defined($state->{entries}{$filename}{modified_hash})
              and not exists ( $state->{opt}{C} ) )
         {
@@ -1168,7 +1168,7 @@ sub req_update
         {
             # normal update, just send the new revision (either U=Update,
             # or A=Add, or R=Remove)
-	    if ( defined($wrev) && $wrev < 0 )
+	    if ( defined($wrev) && ($wrev=~/^-/) )
 	    {
 	        $log->info("Tell the client the file is scheduled for removal");
 		print "MT text R \n";
@@ -1176,7 +1176,8 @@ sub req_update
                 print "MT newline\n";
 		next;
 	    }
-	    elsif ( (!defined($wrev) || $wrev == 0) && (!defined($meta->{revision}) || $meta->{revision} == 0) )
+	    elsif ( (!defined($wrev) || $wrev eq '0') &&
+                    (!defined($meta->{revision}) || $meta->{revision} eq '0') )
 	    {
 	        $log->info("Tell the client the file is scheduled for addition");
 		print "MT text A \n";
@@ -1186,7 +1187,7 @@ sub req_update
 
 	    }
 	    else {
-                $log->info("Updating '$filename' to ".$meta->{revision});
+                $log->info("UpdatingX3 '$filename' to ".$meta->{revision});
                 print "MT +updated\n";
                 print "MT text U \n";
                 print "MT fname $filename\n";
@@ -1218,8 +1219,8 @@ sub req_update
 
 		# this is an "entries" line
 		my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-		$log->debug("/$filepart/1.$meta->{revision}//$kopts/");
-		print "/$filepart/1.$meta->{revision}//$kopts/\n";
+		$log->debug("/$filepart/$meta->{revision}//$kopts/");
+		print "/$filepart/$meta->{revision}//$kopts/\n";
 
 		# permissions
 		$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
@@ -1229,7 +1230,6 @@ sub req_update
 		transmitfile($meta->{filehash});
 	    }
         } else {
-            $log->info("Updating '$filename'");
             my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1);
 
             my $mergeDir = setupTmpDir();
@@ -1244,7 +1244,7 @@ sub req_update
 
             # we need to merge with the local changes ( M=successful merge, C=conflict merge )
             $log->info("Merging $file_local, $file_old, $file_new");
-            print "M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into $filename\n";
+            print "M Merging differences between $oldmeta->{revision} and $meta->{revision} into $filename\n";
 
             $log->debug("Temporary directory for merge is $mergeDir");
 
@@ -1267,8 +1267,8 @@ sub req_update
                     print $state->{CVSROOT} . "/$state->{module}/$filename\n";
                     my $kopts = kopts_from_path("$dirpart/$filepart",
                                                 "file",$mergedFile);
-                    $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
-                    print "/$filepart/1.$meta->{revision}//$kopts/\n";
+                    $log->debug("/$filepart/$meta->{revision}//$kopts/");
+                    print "/$filepart/$meta->{revision}//$kopts/\n";
                 }
             }
             elsif ( $return == 1 )
@@ -1284,7 +1284,7 @@ sub req_update
                     print $state->{CVSROOT} . "/$state->{module}/$filename\n";
                     my $kopts = kopts_from_path("$dirpart/$filepart",
                                                 "file",$mergedFile);
-                    print "/$filepart/1.$meta->{revision}/+/$kopts/\n";
+                    print "/$filepart/$meta->{revision}/+/$kopts/\n";
                 }
             }
             else
@@ -1387,11 +1387,12 @@ sub req_ci
 
         my $addflag = 0;
         my $rmflag = 0;
-        $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
+        $rmflag = 1 if ( defined($wrev) and ($wrev=~/^-/) );
         $addflag = 1 unless ( -e $filename );
 
         # Do up to date checking
-        unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
+        unless ( $addflag or $wrev eq $meta->{revision} or
+                 ( $rmflag and $wrev eq "-$meta->{revision}" ) )
         {
             # fail everything if an up to date check fails
             print "error 1 Up to date check failed for $filename\n";
@@ -1428,7 +1429,7 @@ sub req_ci
             $log->info("Adding file '$filename'");
             system("git", "update-index", "--add", $filename);
         } else {
-            $log->info("Updating file '$filename'");
+            $log->info("UpdatingX2 file '$filename'");
             system("git", "update-index", $filename);
         }
     }
@@ -1519,7 +1520,7 @@ sub req_ci
 
         my $meta = $updater->getmeta($filename);
 	unless (defined $meta->{revision}) {
-	  $meta->{revision} = 1;
+	  $meta->{revision} = "1.1";
 	}
 
         my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
@@ -1529,19 +1530,19 @@ sub req_ci
 	print "M $state->{CVSROOT}/$state->{module}/$filename,v  <--  $dirpart$filepart\n";
         if ( defined $meta->{filehash} && $meta->{filehash} eq "deleted" )
         {
-            print "M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";
+            print "M new revision: delete; previous revision: $oldmeta{$filename}{revision}\n";
             print "Remove-entry $dirpart\n";
             print "$filename\n";
         } else {
-            if ($meta->{revision} == 1) {
+            if ($meta->{revision} eq "1.1") {
 	        print "M initial revision: 1.1\n";
             } else {
-	        print "M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";
+	        print "M new revision: $meta->{revision}; previous revision: $oldmeta{$filename}{revision}\n";
             }
             print "Checked-in $dirpart\n";
             print "$filename\n";
             my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-            print "/$filepart/1.$meta->{revision}//$kopts/\n";
+            print "/$filepart/$meta->{revision}//$kopts/\n";
         }
     }
 
@@ -1585,7 +1586,7 @@ sub req_status
 
         # If the working copy is an old revision, lets get that
         # version too for comparison.
-        if ( defined($wrev) and $wrev != $meta->{revision} )
+        if ( defined($wrev) and $wrev ne $meta->{revision} )
         {
             $oldmeta = $updater->getmeta($filename, $wrev);
         }
@@ -1595,21 +1596,22 @@ sub req_status
         # Files are up to date if the working copy and repo copy have
         # the same revision, and the working copy is unmodified
         if ( defined ( $wrev ) and defined($meta->{revision}) and
-             $wrev == $meta->{revision} and
+             $wrev eq $meta->{revision} and
              ( ( $state->{entries}{$filename}{unchanged} and
                  ( not defined ( $state->{entries}{$filename}{conflict} ) or
                    $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
                ( defined($state->{entries}{$filename}{modified_hash}) and
                  $state->{entries}{$filename}{modified_hash} eq
-                          $meta->{filehash} ) ) )
+                        $meta->{filehash} ) ) )
         {
-            $status = "Up-to-date";
+            $status = "Up-to-date"
         }
 
-        # Need checkout if the working copy has an older revision than
-        # the repo copy, and the working copy is unmodified
+        # Need checkout if the working copy has a different (usually
+        # older) revision than the repo copy, and the working copy is
+        # unmodified
         if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
-             $meta->{revision} > $wrev and
+             $meta->{revision} ne $wrev and
              ( $state->{entries}{$filename}{unchanged} or
                ( defined($state->{entries}{$filename}{modified_hash}) and
                  $state->{entries}{$filename}{modified_hash} eq
@@ -1628,16 +1630,16 @@ sub req_status
         # Locally modified if working copy and repo copy have the
         # same revision but there are local changes
         if ( defined ( $wrev ) and defined($meta->{revision}) and
-             $wrev == $meta->{revision} and
+             $wrev eq $meta->{revision} and
              $state->{entries}{$filename}{modified_filename} )
         {
             $status ||= "Locally Modified";
         }
 
-        # Needs Merge if working copy revision is less than repo copy
-        # and there are local changes
+        # Needs Merge if working copy revision is different
+        # (usually older) than repo copy and there are local changes
         if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
-             $meta->{revision} > $wrev and
+             $meta->{revision} ne $wrev and
              $state->{entries}{$filename}{modified_filename} )
         {
             $status ||= "Needs Merge";
@@ -1649,7 +1651,7 @@ sub req_status
             $status ||= "Locally Added";
         }
         if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
-             -$wrev == $meta->{revision} )
+             $wrev eq "-$meta->{revision}" )
         {
             $status ||= "Locally Removed";
         }
@@ -1678,7 +1680,7 @@ sub req_status
         }
         if ( defined($meta->{revision}) )
         {
-            print "M Repository revision:\t1." .
+            print "M Repository revision:\t" .
                    $meta->{revision} .
                    "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
             print "M Sticky Tag:\t\t(none)\n";
@@ -1711,9 +1713,6 @@ sub req_diff
         $revision1 = $state->{opt}{r};
     }
 
-    $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
-    $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
-
     $log->debug("Diffing revisions " .
                 ( defined($revision1) ? $revision1 : "[NULL]" ) .
                 " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
@@ -1746,7 +1745,7 @@ sub req_diff
             $meta1 = $updater->getmeta($filename, $revision1);
             unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
             {
-                print "E File $filename at revision 1.$revision1 doesn't exist\n";
+                print "E File $filename at revision $revision1 doesn't exist\n";
                 next;
             }
             transmitfile($meta1->{filehash}, { targetfile => $file1 });
@@ -1767,7 +1766,7 @@ sub req_diff
 
             unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
             {
-                print "E File $filename at revision 1.$revision2 doesn't exist\n";
+                print "E File $filename at revision $revision2 doesn't exist\n";
                 next;
             }
 
@@ -1793,7 +1792,7 @@ sub req_diff
 
         # Files to date if the working copy and repo copy have the same
         # revision, and the working copy is unmodified
-        if ( not defined ( $meta2 ) and $wrev == $meta1->{revision} and
+        if ( not defined ( $meta2 ) and $wrev eq $meta1->{revision} and
              ( ( $state->{entries}{$filename}{unchanged} and
                  ( not defined ( $state->{entries}{$filename}{conflict} ) or
                    $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
@@ -1816,11 +1815,11 @@ sub req_diff
         print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
         if ( defined ( $meta1 ) )
         {
-            print "M retrieving revision 1.$meta1->{revision}\n"
+            print "M retrieving revision $meta1->{revision}\n"
         }
         if ( defined ( $meta2 ) )
         {
-            print "M retrieving revision 1.$meta2->{revision}\n"
+            print "M retrieving revision $meta2->{revision}\n"
         }
         print "M diff ";
         foreach my $opt ( keys %{$state->{opt}} )
@@ -1848,10 +1847,10 @@ sub req_diff
 
         if ( exists $state->{opt}{u} )
         {
-            system("diff -u -L '$filename revision 1.$meta1->{revision}'" .
+            system("diff -u -L '$filename revision $meta1->{revision}'" .
                         " -L '$filename " .
                         ( defined($meta2->{revision}) ?
-                                "revision 1.$meta2->{revision}" :
+                                "revision $meta2->{revision}" :
                                 "working copy" ) .
                         "' $file1 $file2 > $filediff" );
         } else {
@@ -1877,16 +1876,10 @@ sub req_log
     $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
     #$log->debug("log state : " . Dumper($state));
 
-    my ( $minrev, $maxrev );
-    if ( defined ( $state->{opt}{r} ) and
-         $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
-    {
-        my $control = $2;
-        $minrev = $1;
-        $maxrev = $3;
-        $minrev =~ s/^1\.// if ( defined ( $minrev ) );
-        $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
-        $minrev++ if ( defined($minrev) and $control eq "::" );
+    my ( $revFilter );
+    if ( defined ( $state->{opt}{r} ) )
+    {
+        $revFilter = $state->{opt}{r};
     }
 
     # Grab a handle to the SQLite db and do any necessary updates
@@ -1905,34 +1898,15 @@ sub req_log
 
         my $headmeta = $updater->getmeta($filename);
 
-        my $revisions = $updater->getlog($filename);
-        my $totalrevisions = scalar(@$revisions);
-
-        if ( defined ( $minrev ) )
-        {
-            $log->debug("Removing revisions less than $minrev");
-            while ( scalar(@$revisions) > 0 and
-                    $revisions->[-1]{revision} < $minrev )
-            {
-                pop @$revisions;
-            }
-        }
-        if ( defined ( $maxrev ) )
-        {
-            $log->debug("Removing revisions greater than $maxrev");
-            while ( scalar(@$revisions) > 0 and
-                    $revisions->[0]{revision} > $maxrev )
-            {
-                shift @$revisions;
-            }
-        }
+        my ($revisions,$totalrevisions) = $updater->getlog($filename,
+                                                           $revFilter);
 
         next unless ( scalar(@$revisions) );
 
         print "M \n";
         print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
         print "M Working file: $filename\n";
-        print "M head: 1.$headmeta->{revision}\n";
+        print "M head: $headmeta->{revision}\n";
         print "M branch:\n";
         print "M locks: strict\n";
         print "M access list:\n";
@@ -1945,7 +1919,7 @@ sub req_log
         foreach my $revision ( @$revisions )
         {
             print "M ----------------------------\n";
-            print "M revision 1.$revision->{revision}\n";
+            print "M revision $revision->{revision}\n";
             # reformat the date for log output
             if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and
                  defined($DATE_LIST->{$2}) )
@@ -2069,7 +2043,7 @@ sub req_annotate
                     $metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author});
                     $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
                 }
-                printf("M 1.%-5d      (%-8s %10s): %s\n",
+                printf("M %-7s      (%-8s %10s): %s\n",
                     $metadata->{$commithash}{revision},
                     $metadata->{$commithash}{author},
                     $metadata->{$commithash}{modified},
@@ -2190,7 +2164,7 @@ sub argsfromdir
     # push added files
     foreach my $file (keys %{$state->{entries}}) {
 	if ( exists $state->{entries}{$file}{revision} &&
-		$state->{entries}{$file}{revision} == 0 )
+		$state->{entries}{$file}{revision} eq '0' )
 	{
 	    push @gethead, { name => $file, filehash => 'added' };
 	}
@@ -2234,20 +2208,15 @@ sub statecleanup
     $state->{entries} = {};
 }
 
-# Return working directory revision int "X" from CVS revision "1.X" out
+# Return working directory CVS revision "1.X" out
 # of the the working directory "entries" state, for the given filename.
-# Return negative "X" to represent the file is scheduled for removal
+# This is prefixed with a dash if the file is scheduled for removal
 # when it is committed.
 sub revparse
 {
     my $filename = shift;
 
-    return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
-
-    return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
-    return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
-
-    return undef;
+    return $state->{entries}{$filename}{revision};
 }
 
 # This method takes a file hash and does a CVS "file transfer".  Its
@@ -3230,7 +3199,7 @@ sub update
     my $commitcount = 0;
 
     # Load the head table into $head (for cached lookups during the update process)
-    foreach my $file ( @{$self->gethead()} )
+    foreach my $file ( @{$self->gethead(1)} )
     {
         $head->{$file->{name}} = $file;
     }
@@ -3580,6 +3549,7 @@ sub _set_prop
 sub gethead
 {
     my $self = shift;
+    my $intRev = shift;
     my $tablename = $self->tablename("head");
 
     return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
@@ -3590,6 +3560,10 @@ sub gethead
     my $tree = [];
     while ( my $file = $db_query->fetchrow_hashref )
     {
+        if(!$intRev)
+        {
+            $file->{revision} = "1.$file->{revision}"
+        }
         push @$tree, $file;
     }
 
@@ -3608,18 +3582,49 @@ sub getlog
 {
     my $self = shift;
     my $filename = shift;
+    my $revFilter = shift;
+
     my $tablename = $self->tablename("revision");
 
+    # Filters:
+    # TODO: date, state, or by specific logins filters?
+    # TODO: Handle comma-separated list of revFilter items, each item
+    #   can be a range [only case currently handled] or individual
+    #   rev or branch or "branch.".
+    # TODO: Adjust $db_query WHERE clause based on revFilter, instead of
+    #   manually filtering the results of the query?
+    my ( $minrev, $maxrev );
+    if( defined($revFilter) and
+        $state->{opt}{r} =~ /^(1.(\d+))?(::?)(1.(\d.+))?$/ )
+    {
+        my $control = $3;
+        $minrev = $2;
+        $maxrev = $5;
+        $minrev++ if ( defined($minrev) and $control eq "::" );
+    }
+
     my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
     $db_query->execute($filename);
 
+    my $totalRevs=0;
     my $tree = [];
     while ( my $file = $db_query->fetchrow_hashref )
     {
+        $totalRevs++;
+        if( defined($minrev) and $file->{revision} < $minrev )
+        {
+            next;
+        }
+        if( defined($maxrev) and $file->{revision} > $maxrev )
+        {
+            next;
+        }
+
+        $file->{revision} = "1." . $file->{revision};
         push @$tree, $file;
     }
 
-    return $tree;
+    return ($tree,$totalRevs);
 }
 
 =head2 getmeta
@@ -3638,10 +3643,11 @@ sub getmeta
     my $tablename_head = $self->tablename("head");
 
     my $db_query;
-    if ( defined($revision) and $revision =~ /^\d+$/ )
+    if ( defined($revision) and $revision =~ /^1\.(\d+)$/ )
     {
+        my ($intRev) = $1;
         $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND revision=?",{},1);
-        $db_query->execute($filename, $revision);
+        $db_query->execute($filename, $intRev);
     }
     elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
     {
@@ -3652,7 +3658,12 @@ sub getmeta
         $db_query->execute($filename);
     }
 
-    return $db_query->fetchrow_hashref;
+    my $meta = $db_query->fetchrow_hashref;
+    if($meta)
+    {
+        $meta->{revision} = "1.$meta->{revision}";
+    }
+    return $meta;
 }
 
 =head2 commitmessage
@@ -3709,7 +3720,15 @@ sub gethistorydense
     $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
     $db_query->execute($filename);
 
-    return $db_query->fetchall_arrayref;
+    my $result = $db_query->fetchall_arrayref;
+
+    my $i;
+    for($i=0 ; $i<scalar(@$result) ; $i++)
+    {
+        $result->[$i][0]="1." . $result->[$i][0];
+    }
+
+    return $result;
 }
 
 =head2 in_array()
diff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh
index 6c693ff..9502f24 100755
--- a/t/t9400-git-cvsserver-server.sh
+++ b/t/t9400-git-cvsserver-server.sh
@@ -400,7 +400,7 @@ cat >expected.C <<EOF
 Line 0
 =======
 LINE 0
->>>>>>> merge.3
+>>>>>>> merge.1.3
 EOF
 
 for i in 1 2 3 4 5 6 7 8
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 09/20] cvsserver: cvs add: do not expand directory arguments
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (7 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 08/20] cvsserver: use whole CVS rev number in-process; don't strip "1." prefix Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 10/20] cvsserver status: provide real sticky info Matthew Ogilvie
                   ` (11 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Standard "cvs add" never does any recursion.  With standard
cvs, "cvs add dir" will either add just the "dir" to
the repository, or error out.  Prior to this change, git-cvsserver
would try to recurse (perhaps re-adding sandbox-removed files?) into
the existing directory instead.

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 2 --
 1 file changed, 2 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index 8a7106d..4d514b4 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -543,8 +543,6 @@ sub req_add
     my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
     $updater->update();
 
-    argsfromdir($updater);
-
     my $addcount = 0;
 
     foreach my $filename ( @{$state->{args}} )
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 10/20] cvsserver status: provide real sticky info
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (8 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 09/20] cvsserver: cvs add: do not expand directory arguments Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 11/20] cvsserver: factor out git-log parsing logic Matthew Ogilvie
                   ` (10 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl            | 24 ++++++++++++++++++++----
 t/t9401-git-cvsserver-crlf.sh | 35 +++++++++++++++++++++++++++++++++++
 2 files changed, 55 insertions(+), 4 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index 4d514b4..c5ebfa0 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -502,7 +502,7 @@ sub req_Entry
 
     #$log->debug("req_Entry : $data");
 
-    my @data = split(/\//, $data);
+    my @data = split(/\//, $data, -1);
 
     $state->{entries}{$state->{directory}.$data[1]} = {
         revision    => $data[2],
@@ -1681,9 +1681,25 @@ sub req_status
             print "M Repository revision:\t" .
                    $meta->{revision} .
                    "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
-            print "M Sticky Tag:\t\t(none)\n";
-            print "M Sticky Date:\t\t(none)\n";
-            print "M Sticky Options:\t\t(none)\n";
+            my($tagOrDate)=$state->{entries}{$filename}{tag_or_date};
+            my($tag)=($tagOrDate=~m/^T(.+)$/);
+            if( !defined($tag) )
+            {
+                $tag="(none)";
+            }
+            print "M Sticky Tag:\t\t$tag\n";
+            my($date)=($tagOrDate=~m/^D(.+)$/);
+            if( !defined($date) )
+            {
+                $date="(none)";
+            }
+            print "M Sticky Date:\t\t$date\n";
+            my($options)=$state->{entries}{$filename}{options};
+            if( $options eq "" )
+            {
+                $options="(none)";
+            }
+            print "M Sticky Options:\t\t$options\n";
         } else {
             print "M Repository revision:\tNo revision control file\n";
         }
diff --git a/t/t9401-git-cvsserver-crlf.sh b/t/t9401-git-cvsserver-crlf.sh
index ff6d6fb..7c3c39e 100755
--- a/t/t9401-git-cvsserver-crlf.sh
+++ b/t/t9401-git-cvsserver-crlf.sh
@@ -38,6 +38,25 @@ not_present() {
     fi
 }
 
+check_status_options() {
+    (cd "$1" &&
+    GIT_CONFIG="$git_config" cvs -Q status "$2" > "${WORKDIR}/status.out" 2>&1
+    )
+    if [ x"$?" != x"0" ] ; then
+        echo "Error from cvs status: $1 $2" >> "${WORKDIR}/marked.log"
+        return 1;
+    fi
+    got="$(sed -n -e 's/^\s*Sticky Options:\s*//p' "${WORKDIR}/status.out")"
+    expect="$3"
+    if [ x"$expect" = x"" ] ; then
+        expect="(none)"
+    fi
+    test x"$got" = x"$expect"
+    stat=$?
+    echo "cvs status: $1 $2 $stat '$3' '$got'" >> "${WORKDIR}/marked.log"
+    return $stat
+}
+
 cvs >/dev/null 2>&1
 if test $? -ne 1
 then
@@ -233,6 +252,22 @@ test_expect_success 'cvs co another copy (guess)' '
     marked_as cvswork2/subdir newfile.c ""
 '
 
+test_expect_success 'cvs status - sticky options' '
+    check_status_options cvswork2 textfile.c "" &&
+    check_status_options cvswork2 binfile.bin -kb &&
+    check_status_options cvswork2 .gitattributes "" &&
+    check_status_options cvswork2 mixedUp.c -kb &&
+    check_status_options cvswork2 multiline.c -kb &&
+    check_status_options cvswork2 multilineTxt.c "" &&
+    check_status_options cvswork2/subdir withCr.bin -kb &&
+    check_status_options cvswork2 subdir/withCr.bin -kb &&
+    check_status_options cvswork2/subdir file.h "" &&
+    check_status_options cvswork2 subdir/file.h "" &&
+    check_status_options cvswork2/subdir unspecified.other "" &&
+    check_status_options cvswork2/subdir newfile.bin "" &&
+    check_status_options cvswork2/subdir newfile.c ""
+'
+
 test_expect_success 'add text (guess)' '
     (cd cvswork &&
     echo "simpleText" > simpleText.c &&
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 11/20] cvsserver: factor out git-log parsing logic
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (9 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 10/20] cvsserver status: provide real sticky info Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 12/20] cvsserver: cleanup extra slashes in filename arguments Matthew Ogilvie
                   ` (9 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Some field conversion was already duplicated, and more calls will
be added soon.

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 176 ++++++++++++++++++++++++++++++++---------------------
 1 file changed, 105 insertions(+), 71 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index c5ebfa0..dca0ed6 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -3157,45 +3157,10 @@ sub update
         push @git_log_params, $self->{module};
     }
     # git-rev-list is the backend / plumbing version of git-log
-    open(GITLOG, '-|', 'git', 'rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
-
-    my @commits;
-
-    my %commit = ();
-
-    while ( <GITLOG> )
-    {
-        chomp;
-        if (m/^commit\s+(.*)$/) {
-            # on ^commit lines put the just seen commit in the stack
-            # and prime things for the next one
-            if (keys %commit) {
-                my %copy = %commit;
-                unshift @commits, \%copy;
-                %commit = ();
-            }
-            my @parents = split(m/\s+/, $1);
-            $commit{hash} = shift @parents;
-            $commit{parents} = \@parents;
-        } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
-            # on rfc822-like lines seen before we see any message,
-            # lowercase the entry and put it in the hash as key-value
-            $commit{lc($1)} = $2;
-        } else {
-            # message lines - skip initial empty line
-            # and trim whitespace
-            if (!exists($commit{message}) && m/^\s*$/) {
-                # define it to mark the end of headers
-                $commit{message} = '';
-                next;
-            }
-            s/^\s+//; s/\s+$//; # trim ws
-            $commit{message} .= $_ . "\n";
-        }
-    }
-    close GITLOG;
-
-    unshift @commits, \%commit if ( keys %commit );
+    open(my $gitLogPipe, '-|', 'git', 'rev-list', @git_log_params)
+                or die "Cannot call git-rev-list: $!";
+    my @commits=readCommits($gitLogPipe);
+    close $gitLogPipe;
 
     # Now all the commits are in the @commits bucket
     # ordered by time DESC. for each commit that needs processing,
@@ -3294,7 +3259,7 @@ sub update
         }
 
         # convert the date to CVS-happy format
-        $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
+        my $cvsDate = convertToCvsDate($commit->{date});
 
         if ( defined ( $lastpicked ) )
         {
@@ -3303,7 +3268,7 @@ sub update
             while ( <FILELIST> )
             {
 		chomp;
-                unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o )
+                unless ( /^:\d{6}\s+([0-7]{6})\s+[a-f0-9]{40}\s+([a-f0-9]{40})\s+(\w)$/o )
                 {
                     die("Couldn't process git-diff-tree line : $_");
                 }
@@ -3313,11 +3278,7 @@ sub update
 
                 # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
 
-                my $git_perms = "";
-                $git_perms .= "r" if ( $mode & 4 );
-                $git_perms .= "w" if ( $mode & 2 );
-                $git_perms .= "x" if ( $mode & 1 );
-                $git_perms = "rw" if ( $git_perms eq "" );
+                my $dbMode = convertToDbMode($mode);
 
                 if ( $change eq "D" )
                 {
@@ -3327,11 +3288,11 @@ sub update
                         revision => $head->{$name}{revision} + 1,
                         filehash => "deleted",
                         commithash => $commit->{hash},
-                        modified => $commit->{date},
+                        modified => $cvsDate,
                         author => $commit->{author},
-                        mode => $git_perms,
+                        mode => $dbMode,
                     };
-                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                 }
                 elsif ( $change eq "M" || $change eq "T" )
                 {
@@ -3341,11 +3302,11 @@ sub update
                         revision => $head->{$name}{revision} + 1,
                         filehash => $hash,
                         commithash => $commit->{hash},
-                        modified => $commit->{date},
+                        modified => $cvsDate,
                         author => $commit->{author},
-                        mode => $git_perms,
+                        mode => $dbMode,
                     };
-                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                 }
                 elsif ( $change eq "A" )
                 {
@@ -3355,11 +3316,11 @@ sub update
                         revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
                         filehash => $hash,
                         commithash => $commit->{hash},
-                        modified => $commit->{date},
+                        modified => $cvsDate,
                         author => $commit->{author},
-                        mode => $git_perms,
+                        mode => $dbMode,
                     };
-                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                 }
                 else
                 {
@@ -3382,7 +3343,7 @@ sub update
                     die("Couldn't process git-ls-tree line : $_");
                 }
 
-                my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
+                my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
 
                 $seen_files->{$git_filename} = 1;
 
@@ -3392,18 +3353,10 @@ sub update
                     $head->{$git_filename}{mode}
                 );
 
-                if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
-                {
-                    $git_perms = "";
-                    $git_perms .= "r" if ( $1 & 4 );
-                    $git_perms .= "w" if ( $1 & 2 );
-                    $git_perms .= "x" if ( $1 & 1 );
-                } else {
-                    $git_perms = "rw";
-                }
+                my $dbMode = convertToDbMode($mode);
 
                 # unless the file exists with the same hash, we need to update it ...
-                unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
+                unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $dbMode )
                 {
                     my $newrevision = ( $oldrevision or 0 ) + 1;
 
@@ -3412,13 +3365,13 @@ sub update
                         revision => $newrevision,
                         filehash => $git_hash,
                         commithash => $commit->{hash},
-                        modified => $commit->{date},
+                        modified => $cvsDate,
                         author => $commit->{author},
-                        mode => $git_perms,
+                        mode => $dbMode,
                     };
 
 
-                    $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                    $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                 }
             }
             close FILELIST;
@@ -3431,10 +3384,10 @@ sub update
                     $head->{$file}{revision}++;
                     $head->{$file}{filehash} = "deleted";
                     $head->{$file}{commithash} = $commit->{hash};
-                    $head->{$file}{modified} = $commit->{date};
+                    $head->{$file}{modified} = $cvsDate;
                     $head->{$file}{author} = $commit->{author};
 
-                    $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
+                    $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $cvsDate, $commit->{author}, $head->{$file}{mode});
                 }
             }
             # END : "Detect deleted files"
@@ -3472,6 +3425,87 @@ sub update
     $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
 }
 
+sub readCommits
+{
+    my $pipeHandle = shift;
+    my @commits;
+
+    my %commit = ();
+
+    while ( <$pipeHandle> )
+    {
+        chomp;
+        if (m/^commit\s+(.*)$/) {
+            # on ^commit lines put the just seen commit in the stack
+            # and prime things for the next one
+            if (keys %commit) {
+                my %copy = %commit;
+                unshift @commits, \%copy;
+                %commit = ();
+            }
+            my @parents = split(m/\s+/, $1);
+            $commit{hash} = shift @parents;
+            $commit{parents} = \@parents;
+        } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
+            # on rfc822-like lines seen before we see any message,
+            # lowercase the entry and put it in the hash as key-value
+            $commit{lc($1)} = $2;
+        } else {
+            # message lines - skip initial empty line
+            # and trim whitespace
+            if (!exists($commit{message}) && m/^\s*$/) {
+                # define it to mark the end of headers
+                $commit{message} = '';
+                next;
+            }
+            s/^\s+//; s/\s+$//; # trim ws
+            $commit{message} .= $_ . "\n";
+        }
+    }
+
+    unshift @commits, \%commit if ( keys %commit );
+
+    return @commits;
+}
+
+sub convertToCvsDate
+{
+    my $date = shift;
+    # Convert from: "git rev-list --pretty" formatted date
+    # Convert to: "the format specified by RFC822 as modified by RFC1123."
+    # Example: 26 May 1997 13:01:40 -0400
+    if( $date =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ )
+    {
+        $date = "$2 $1 $4 $3 $5";
+    }
+
+    return $date;
+}
+
+sub convertToDbMode
+{
+    my $mode = shift;
+
+    # NOTE: The CVS protocol uses a string similar "u=rw,g=rw,o=rw",
+    #  but the database "mode" column historically (and currently)
+    #  only stores the "rw" (for user) part of the string.
+    #    FUTURE: It might make more sense to persist the raw
+    #  octal mode (or perhaps the final full CVS form) instead of
+    #  this half-converted form, but it isn't currently worth the
+    #  backwards compatibility headaches.
+
+    $mode=~/^\d\d(\d)\d{3}$/;
+    my $userBits=$1;
+
+    my $dbMode = "";
+    $dbMode .= "r" if ( $userBits & 4 );
+    $dbMode .= "w" if ( $userBits & 2 );
+    $dbMode .= "x" if ( $userBits & 1 );
+    $dbMode = "rw" if ( $dbMode eq "" );
+
+    return $dbMode;
+}
+
 sub insert_rev
 {
     my $self = shift;
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 12/20] cvsserver: cleanup extra slashes in filename arguments
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (10 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 11/20] cvsserver: factor out git-log parsing logic Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 13/20] cvsserver: define a tag name character escape mechanism Matthew Ogilvie
                   ` (8 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index dca0ed6..1d929df 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -2309,6 +2309,9 @@ sub filenamesplit
     return ( $filepart, $dirpart );
 }
 
+# Cleanup various junk in filename (try to canonicalize it), and
+# add prependdir to accomodate running CVS client from a
+# subdirectory (so the output is relative to top directory of the project).
 sub filecleanup
 {
     my $filename = shift;
@@ -2320,11 +2323,36 @@ sub filecleanup
         return undef;
     }
 
+    if($filename eq ".")
+    {
+        $filename="";
+    }
     $filename =~ s/^\.\///g;
+    $filename =~ s%/+%/%g;
     $filename = $state->{prependdir} . $filename;
+    $filename =~ s%/$%%;
     return $filename;
 }
 
+# Remove prependdir from the path, so that is is relative to the directory
+# the CVS client was started from, rather than the top of the project.
+# Essentially the inverse of filecleanup().
+sub remove_prependdir
+{
+    my($path) = @_;
+    if(defined($state->{prependdir}) && $state->{prependdir} ne "")
+    {
+        my($pre)=$state->{prependdir};
+        $pre=~s%/$%%;
+        if(!($path=~s%^\Q$pre\E/?%%))
+        {
+            $log->fatal("internal error missing prependdir");
+            die("internal error missing prependdir");
+        }
+    }
+    return $path;
+}
+
 sub validateGitDir
 {
     if( !defined($state->{CVSROOT}) )
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 13/20] cvsserver: define a tag name character escape mechanism
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (11 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 12/20] cvsserver: cleanup extra slashes in filename arguments Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 14/20] cvsserver: add misc commit lookup, file meta data, and file listing functions Matthew Ogilvie
                   ` (7 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

CVS tags are officially only allowed to use [-_0-9A-Za-f].  Git
refs commonly uses other characters, especially [./].  Such characters
need to be escaped from CVS in order to be referenced.

This just defines functions to escape/unescape names.  The functions
are not used yet.

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 91 insertions(+)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index 1d929df..4eecc0b 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -3807,6 +3807,97 @@ sub gethistorydense
     return $result;
 }
 
+=head2 escapeRefName
+
+Apply an escape mechanism to compensate for characters that
+git ref names can have that CVS tags can not.
+
+=cut
+sub escapeRefName
+{
+    my($self,$refName)=@_;
+
+    # CVS officially only allows [-_A-Za-z0-9] in tag names (or in
+    # many contexts it can also be a CVS revision number).
+    #
+    # Git tags commonly use '/' and '.' as well, but also handle
+    # anything else just in case:
+    #
+    #   = "_-s-"  For '/'.
+    #   = "_-p-"  For '.'.
+    #   = "_-u-"  For underscore, in case someone wants a literal "_-" in
+    #     a tag name.
+    #   = "_-xx-" Where "xx" is the hexadecimal representation of the
+    #     desired ASCII character byte. (for anything else)
+
+    if(! $refName=~/^[1-9][0-9]*(\.[1-9][0-9]*)*$/)
+    {
+        $refName=~s/_-/_-u--/g;
+        $refName=~s/\./_-p-/g;
+        $refName=~s%/%_-s-%g;
+        $refName=~s/[^-_a-zA-Z0-9]/sprintf("_-%02x-",$1)/eg;
+    }
+}
+
+=head2 unescapeRefName
+
+Undo an escape mechanism to compensate for characters that
+git ref names can have that CVS tags can not.
+
+=cut
+sub unescapeRefName
+{
+    my($self,$refName)=@_;
+
+    # see escapeRefName() for description of escape mechanism.
+
+    $refName=~s/_-([spu]|[0-9a-f][0-9a-f])-/unescapeRefNameChar($1)/eg;
+
+    # allowed tag names
+    # TODO: Perhaps use git check-ref-format, with an in-process cache of
+    #  validated names?
+    if( !( $refName=~m%^[^-][-a-zA-Z0-9_/.]*$% ) ||
+        ( $refName=~m%[/.]$% ) ||
+        ( $refName=~/\.lock$/ ) ||
+        ( $refName=~m%\.\.|/\.|[[\\:?*~]|\@\{% ) )  # matching }
+    {
+        # Error:
+        $log->warn("illegal refName: $refName");
+        $refName=undef;
+    }
+    return $refName;
+}
+
+sub unescapeRefNameChar
+{
+    my($char)=@_;
+
+    if($char eq "s")
+    {
+        $char="/";
+    }
+    elsif($char eq "p")
+    {
+        $char=".";
+    }
+    elsif($char eq "u")
+    {
+        $char="_";
+    }
+    elsif($char=~/^[0-9a-f][0-9a-f]$/)
+    {
+        $char=chr(hex($char));
+    }
+    else
+    {
+        # Error case: Maybe it has come straight from user, and
+        # wasn't supposed to be escaped?  Restore it the way we got it:
+        $char="_-$char-";
+    }
+
+    return $char;
+}
+
 =head2 in_array()
 
 from Array::PAT - mimics the in_array() function
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 14/20] cvsserver: add misc commit lookup, file meta data, and file listing functions
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (12 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 13/20] cvsserver: define a tag name character escape mechanism Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 15/20] cvsserver: implement req_Sticky and related utilities Matthew Ogilvie
                   ` (6 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

These will be used soon, but not yet.

PERFORMANCE NOTE: getMetaFromCommithash() does not scale well as currently
implemented.  See comment for possible optimization strategies.
Fortunately, it will only be used in cases that would not have worked
at all before this change.

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 366 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 365 insertions(+), 1 deletion(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index 4eecc0b..16e2e1f 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -2986,6 +2986,9 @@ sub new
 
     die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
 
+    # Stores full sha1's for various branch/tag names, abbreviations, etc:
+    $self->{commitRefCache} = {};
+
     $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
         $cfg->{gitcvs}{dbdriver} || "SQLite";
     $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
@@ -3446,7 +3449,7 @@ sub update
         );
     }
     # invalidate the gethead cache
-    $self->{gethead_cache} = undef;
+    $self->clearCommitRefCaches();
 
 
     # Ending exclusive lock here
@@ -3648,6 +3651,169 @@ sub gethead
     return $tree;
 }
 
+=head2 getAnyHead
+
+Returns a reference to an array of getmeta structures, one
+per file in the specified tree hash.
+
+=cut
+
+sub getAnyHead
+{
+    my ($self,$hash) = @_;
+
+    if(!defined($hash))
+    {
+        return $self->gethead();
+    }
+
+    my @files;
+    {
+        open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
+                or die("Cannot call git-ls-tree : $!");
+        local $/ = "\0";
+        @files=<$filePipe>;
+        close $filePipe;
+    }
+
+    my $tree=[];
+    my($line);
+    foreach $line (@files)
+    {
+        $line=~s/\0$//;
+        unless ( $line=~/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
+        {
+            die("Couldn't process git-ls-tree line : $_");
+        }
+
+        my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
+        push @$tree, $self->getMetaFromCommithash($git_filename,$hash);
+    }
+
+    return $tree;
+}
+
+=head2 getRevisionDirMap
+
+A "revision dir map" contains all the plain-file filenames associated
+with a particular revision (treeish), organized by directory:
+
+  $type = $out->{$dir}{$fullName}
+
+The type of each is "F" (for ordinary file) or "D" (for directory,
+for which the map $out->{$fullName} will also exist).
+
+=cut
+
+sub getRevisionDirMap
+{
+    my ($self,$ver)=@_;
+
+    if(!defined($self->{revisionDirMapCache}))
+    {
+        $self->{revisionDirMapCache}={};
+    }
+
+        # Get file list (previously cached results are dependent on HEAD,
+        # but are early in each case):
+    my $cacheKey;
+    my (@fileList);
+    if( !defined($ver) || $ver eq "" )
+    {
+        $cacheKey="";
+        if( defined($self->{revisionDirMapCache}{$cacheKey}) )
+        {
+            return $self->{revisionDirMapCache}{$cacheKey};
+        }
+
+        my @head = @{$self->gethead()};
+        foreach my $file ( @head )
+        {
+            next if ( $file->{filehash} eq "deleted" );
+
+            push @fileList,$file->{name};
+        }
+    }
+    else
+    {
+        my ($hash)=$self->lookupCommitRef($ver);
+        if( !defined($hash) )
+        {
+            return undef;
+        }
+
+        $cacheKey=$hash;
+        if( defined($self->{revisionDirMapCache}{$cacheKey}) )
+        {
+            return $self->{revisionDirMapCache}{$cacheKey};
+        }
+
+        open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
+                or die("Cannot call git-ls-tree : $!");
+        local $/ = "\0";
+        while ( <$filePipe> )
+        {
+            chomp;
+            unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
+            {
+                die("Couldn't process git-ls-tree line : $_");
+            }
+
+            my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
+
+            push @fileList, $git_filename;
+        }
+        close $filePipe;
+    }
+
+        # Convert to normalized form:
+    my %revMap;
+    my $file;
+    foreach $file (@fileList)
+    {
+        my($dir) = ($file=~m%^(?:(.*)/)?([^/]*)$%);
+        $dir='' if(!defined($dir));
+
+            # parent directories:
+            # ... create empty dir maps for parent dirs:
+        my($td)=$dir;
+        while(!defined($revMap{$td}))
+        {
+            $revMap{$td}={};
+
+            my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
+            $tp='' if(!defined($tp));
+            $td=$tp;
+        }
+            # ... add children to parent maps (now that they exist):
+        $td=$dir;
+        while($td ne "")
+        {
+            my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
+            $tp='' if(!defined($tp));
+
+            if(defined($revMap{$tp}{$td}))
+            {
+                if($revMap{$tp}{$td} ne 'D')
+                {
+                    die "Weird file/directory inconsistency in $cacheKey";
+                }
+                last;   # loop exit
+            }
+            $revMap{$tp}{$td}='D';
+
+            $td=$tp;
+        }
+
+            # file
+        $revMap{$dir}{$file}='F';
+    }
+
+        # Save in cache:
+    $self->{revisionDirMapCache}{$cacheKey}=\%revMap;
+    return $self->{revisionDirMapCache}{$cacheKey};
+}
+
 =head2 getlog
 
 See also gethistorydense().
@@ -3742,6 +3908,204 @@ sub getmeta
     return $meta;
 }
 
+sub getMetaFromCommithash
+{
+    my $self = shift;
+    my $filename = shift;
+    my $revCommit = shift;
+
+    # NOTE: This function doesn't scale well (lots of forks), especially
+    #   if you have many files that have not been modified for many commits
+    #   (each git-rev-parse redoes a lot of work for each file
+    #   that theoretically could be done in parallel by smarter
+    #   graph traversal).
+    #
+    # TODO: Possible optimization strategies:
+    #   - Solve the issue of assigning and remembering "real" CVS
+    #     revision numbers for branches, and ensure the
+    #     data structure can do this efficiently.  Perhaps something
+    #     similar to "git notes", and carefully structured to take
+    #     advantage same-sha1-is-same-contents, to roll the same
+    #     unmodified subdirectory data onto multiple commits?
+    #   - Write and use a C tool that is like git-blame, but
+    #     operates on multiple files with file granularity, instead
+    #     of one file with line granularity.  Cache
+    #     most-recently-modified in $self->{commitRefCache}{$revCommit}.
+    #     Try to be intelligent about how many files we do with
+    #     one fork (perhaps one directory at a time, without recursion,
+    #     and/or include directory as one line item, recurse from here
+    #     instead of in C tool?).
+    #   - Perhaps we could ask the DB for (filename,fileHash),
+    #     and just guess that it is correct (that the file hadn't
+    #     changed between $revCommit and the found commit, then
+    #     changed back, confusing anything trying to interpret
+    #     history).  Probably need to add another index to revisions
+    #     DB table for this.
+    #   - NOTE: Trying to store all (commit,file) keys in DB [to
+    #     find "lastModfiedCommit] (instead of
+    #     just files that changed in each commit as we do now) is
+    #     probably not practical from a disk space perspective.
+
+        # Does the file exist in $revCommit?
+    # TODO: Include file hash in dirmap cache.
+    my($dirMap)=$self->getRevisionDirMap($revCommit);
+    my($dir,$file)=($filename=~m%^(?:(.*)/)?([^/]*$)%);
+    if(!defined($dir))
+    {
+        $dir="";
+    }
+    if( !defined($dirMap->{$dir}) ||
+        !defined($dirMap->{$dir}{$filename}) )
+    {
+        my($fileHash)="deleted";
+
+        my($retVal)={};
+        $retVal->{name}=$filename;
+        $retVal->{filehash}=$fileHash;
+
+            # not needed and difficult to compute:
+        $retVal->{revision}="0";  # $revision;
+        $retVal->{commithash}=$revCommit;
+        #$retVal->{author}=$commit->{author};
+        #$retVal->{modified}=convertToCvsDate($commit->{date});
+        #$retVal->{mode}=convertToDbMode($mode);
+
+        return $retVal;
+    }
+
+    my($fileHash)=safe_pipe_capture("git","rev-parse","$revCommit:$filename");
+    chomp $fileHash;
+    if(!($fileHash=~/^[0-9a-f]{40}$/))
+    {
+        die "Invalid fileHash '$fileHash' looking up"
+                    ." '$revCommit:$filename'\n";
+    }
+
+    # information about most recent commit to modify $filename:
+    open(my $gitLogPipe, '-|', 'git', 'rev-list',
+         '--max-count=1', '--pretty', '--parents',
+         $revCommit, '--', $filename)
+                or die "Cannot call git-rev-list: $!";
+    my @commits=readCommits($gitLogPipe);
+    close $gitLogPipe;
+    if(scalar(@commits)!=1)
+    {
+        die "Can't find most recent commit changing $filename\n";
+    }
+    my($commit)=$commits[0];
+    if( !defined($commit) || !defined($commit->{hash}) )
+    {
+        return undef;
+    }
+
+    # does this (commit,file) have a real assigned CVS revision number?
+    my $tablename_rev = $self->tablename("revision");
+    my $db_query;
+    $db_query = $self->{dbh}->prepare_cached(
+        "SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",
+        {},1);
+    $db_query->execute($filename, $commit->{hash});
+    my($meta)=$db_query->fetchrow_hashref;
+    if($meta)
+    {
+        $meta->{revision} = "1.$meta->{revision}";
+        return $meta;
+    }
+
+    # fall back on special revision number
+    my($revision)=$commit->{hash};
+    $revision=~s/(..)/'.' . (hex($1)+100)/eg;
+    $revision="2.1.1.2000$revision";
+
+    # meta data about $filename:
+    open(my $filePipe, '-|', 'git', 'ls-tree', '-z',
+                $commit->{hash}, '--', $filename)
+            or die("Cannot call git-ls-tree : $!");
+    local $/ = "\0";
+    my $line;
+    $line=<$filePipe>;
+    if(defined(<$filePipe>))
+    {
+        die "Expected only a single file for git-ls-tree $filename\n";
+    }
+    close $filePipe;
+
+    chomp $line;
+    unless ( $line=~m/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
+    {
+        die("Couldn't process git-ls-tree line : $line\n");
+    }
+    my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
+
+    # save result:
+    my($retVal)={};
+    $retVal->{name}=$filename;
+    $retVal->{revision}=$revision;
+    $retVal->{filehash}=$fileHash;
+    $retVal->{commithash}=$revCommit;
+    $retVal->{author}=$commit->{author};
+    $retVal->{modified}=convertToCvsDate($commit->{date});
+    $retVal->{mode}=convertToDbMode($mode);
+
+    return $retVal;
+}
+
+=head2 lookupCommitRef
+
+Convert tag/branch/abbreviation/etc into a commit sha1 hash.  Caches
+the result so looking it up again is fast.
+
+=cut
+
+sub lookupCommitRef
+{
+    my $self = shift;
+    my $ref = shift;
+
+    my $commitHash = $self->{commitRefCache}{$ref};
+    if(defined($commitHash))
+    {
+        return $commitHash;
+    }
+
+    $commitHash=safe_pipe_capture("git","rev-parse","--verify","--quiet",
+                                  $self->unescapeRefName($ref));
+    $commitHash=~s/\s*$//;
+    if(!($commitHash=~/^[0-9a-f]{40}$/))
+    {
+        $commitHash=undef;
+    }
+
+    if( defined($commitHash) )
+    {
+        my $type=safe_pipe_capture("git","cat-file","-t",$commitHash);
+        if( ! ($type=~/^commit\s*$/ ) )
+        {
+            $commitHash=undef;
+        }
+    }
+    if(defined($commitHash))
+    {
+        $self->{commitRefCache}{$ref}=$commitHash;
+    }
+    return $commitHash;
+}
+
+=head2 clearCommitRefCaches
+
+Clears cached commit cache (sha1's for various tags/abbeviations/etc),
+and related caches.
+
+=cut
+
+sub clearCommitRefCaches
+{
+    my $self = shift;
+    $self->{commitRefCache} = {};
+    $self->{revisionDirMapCache} = undef;
+    $self->{gethead_cache} = undef;
+}
+
 =head2 commitmessage
 
 this function takes a commithash and returns the commit message for that commit
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 15/20] cvsserver: implement req_Sticky and related utilities
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (13 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 14/20] cvsserver: add misc commit lookup, file meta data, and file listing functions Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 16/20] cvsserver: generalize getmeta() to recognize commit refs Matthew Ogilvie
                   ` (5 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Nothing sets sticky yet, or uses the values set by this, but soon...

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 169 insertions(+), 2 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index 16e2e1f..53d8de7 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -60,6 +60,7 @@ my $methods = {
     'Valid-responses' => \&req_Validresponses,
     'valid-requests'  => \&req_validrequests,
     'Directory'       => \&req_Directory,
+    'Sticky'          => \&req_Sticky,
     'Entry'           => \&req_Entry,
     'Modified'        => \&req_Modified,
     'Unchanged'       => \&req_Unchanged,
@@ -470,11 +471,19 @@ sub req_Directory
     {
         $log->info("Setting prepend to '$state->{path}'");
         $state->{prependdir} = $state->{path};
+        my %entries;
         foreach my $entry ( keys %{$state->{entries}} )
         {
-            $state->{entries}{$state->{prependdir} . $entry} = $state->{entries}{$entry};
-            delete $state->{entries}{$entry};
+            $entries{$state->{prependdir} . $entry} = $state->{entries}{$entry};
         }
+        $state->{entries}=\%entries;
+
+        my %dirMap;
+        foreach my $dir ( keys %{$state->{dirMap}} )
+        {
+            $dirMap{$state->{prependdir} . $dir} = $state->{dirMap}{$dir};
+        }
+        $state->{dirMap}=\%dirMap;
     }
 
     if ( defined ( $state->{prependdir} ) )
@@ -482,9 +491,60 @@ sub req_Directory
         $log->debug("Prepending '$state->{prependdir}' to state|directory");
         $state->{directory} = $state->{prependdir} . $state->{directory}
     }
+
+    if ( ! defined($state->{dirMap}{$state->{directory}}) )
+    {
+        $state->{dirMap}{$state->{directory}} =
+            {
+                'names' => {}
+                #'tagspec' => undef
+            };
+    }
+
     $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
 }
 
+# Sticky tagspec \n
+#     Response expected: no. Tell the server that the directory most
+#     recently specified with Directory has a sticky tag or date
+#     tagspec. The first character of tagspec is T for a tag, D for
+#     a date, or some other character supplied by a Set-sticky
+#     response from a previous request to the server. The remainder
+#     of tagspec contains the actual tag or date, again as supplied
+#     by Set-sticky.
+#          The server should remember Static-directory and Sticky requests
+#     for a particular directory; the client need not resend them each
+#     time it sends a Directory request for a given directory. However,
+#     the server is not obliged to remember them beyond the context
+#     of a single command.
+sub req_Sticky
+{
+    my ( $cmd, $tagspec ) = @_;
+
+    my ( $stickyInfo );
+    if($tagspec eq "")
+    {
+        # nothing
+    }
+    elsif($tagspec=~/^T([^ ]+)\s*$/)
+    {
+        $stickyInfo = { 'tag' => $1 };
+    }
+    elsif($tagspec=~/^D([0-9.]+)\s*$/)
+    {
+        $stickyInfo= { 'date' => $1 };
+    }
+    else
+    {
+        die "Unknown tag_or_date format\n";
+    }
+    $state->{dirMap}{$state->{directory}}{stickyInfo}=$stickyInfo;
+
+    $log->debug("req_Sticky : tagspec=$tagspec repository=$state->{repository}"
+                . " path=$state->{path} directory=$state->{directory}"
+                . " module=$state->{module}");
+}
+
 # Entry entry-line \n
 #     Response expected: no. Tell the server what version of a file is on the
 #     local machine. The name in entry-line is a name relative to the directory
@@ -511,6 +571,8 @@ sub req_Entry
         tag_or_date => $data[5],
     };
 
+    $state->{dirMap}{$state->{directory}}{names}{$data[1]} = 'F';
+
     $log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'");
 }
 
@@ -2213,6 +2275,110 @@ sub argsfromdir
     }
 }
 
+
+## look up directory sticky tag, of either fullPath or a parent:
+sub getDirStickyInfo
+{
+    my($fullPath)=@_;
+
+    $fullPath=~s%/+$%%;
+    while($fullPath ne "" && !defined($state->{dirMap}{"$fullPath/"}))
+    {
+        $fullPath=~s%/?[^/]*$%%;
+    }
+
+    if( !defined($state->{dirMap}{"$fullPath/"}) &&
+        ( $fullPath eq "" ||
+          $fullPath eq "." ) )
+    {
+        return $state->{dirMap}{""}{stickyInfo};
+    }
+    else
+    {
+        return $state->{dirMap}{"$fullPath/"}{stickyInfo};
+    }
+}
+
+# Resolve precedence of various ways of specifying which version of
+# a file you want.  Returns undef (for default head), or a ref to a hash
+# that contains "tag" and/or "date" keys.
+sub resolveStickyInfo
+{
+    my($filename,$stickyTag,$stickyDate,$reset) = @_;
+
+    # Order of precedence of sticky tags:
+    #    -A       [head]
+    #    -r /tag/
+    #    [file entry sticky tag]
+    #    [the tag specified in dir req_Sticky]
+    #    [the tag specified in a parent dir req_Sticky]
+    #    [head]
+
+    my $result;
+    if($reset)
+    {
+        # $result=undef;
+    }
+    elsif( defined($stickyTag) && $stickyTag ne "" )
+           # || ( defined($stickyDate) && $stickyDate ne "" )   # TODO
+    {
+        $result={ 'tag' => (defined($stickyTag)?$stickyTag:undef) };
+
+        # TODO: Convert -D value into the form 2011.04.10.04.46.57,
+        #   similar to an entry line's sticky date, without the D prefix.
+        #   It sometimes (always?) arrives as something more like
+        #   '10 Apr 2011 04:46:57 -0000'...
+        # $result={ 'date' => (defined($stickyDate)?$stickyDate:undef) };
+    }
+    elsif( defined($state->{entries}{$filename}) &&
+           defined($state->{entries}{$filename}{tag_or_date}) &&
+           $state->{entries}{$filename}{tag_or_date} ne "" )
+    {
+        my($tagOrDate)=$state->{entries}{$filename}{tag_or_date};
+        if($tagOrDate=~/^T([^ ]+)\s*$/)
+        {
+            $result = { 'tag' => $1 };
+        }
+        elsif($tagOrDate=~/^D([0-9.]+)\s*$/)
+        {
+            $result= { 'date' => $1 };
+        }
+        else
+        {
+            die "Unknown tag_or_date format\n";
+        }
+    }
+    else
+    {
+        $result=getDirStickyInfo($filename);
+    }
+
+    return $result;
+}
+
+# Convert a stickyInfo (ref to a hash) as returned by resolveStickyInfo into
+# a form appropriate for the sticky tag field of an Entries
+# line (field index 5, 0-based).
+sub getStickyTagOrDate
+{
+    my($stickyInfo)=@_;
+
+    my $result;
+    if(defined($stickyInfo) && defined($stickyInfo->{tag}))
+    {
+        $result="T$stickyInfo->{tag}";
+    }
+    # TODO: When/if we actually pick versions by {date} properly,
+    #   also handle it here:
+    #   "D$stickyInfo->{date}" (example: "D2011.04.13.20.37.07").
+    else
+    {
+        $result="";
+    }
+
+    return $result;
+}
+
 # This method cleans up the $state variable after a command that uses arguments has run
 sub statecleanup
 {
@@ -2220,6 +2386,7 @@ sub statecleanup
     $state->{args} = [];
     $state->{arguments} = [];
     $state->{entries} = {};
+    $state->{dirMap} = {};
 }
 
 # Return working directory CVS revision "1.X" out
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 16/20] cvsserver: generalize getmeta() to recognize commit refs
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (14 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 15/20] cvsserver: implement req_Sticky and related utilities Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 17/20] cvsserver: Add version awareness to argsfromdir Matthew Ogilvie
                   ` (4 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

This allows getmeta() to recognize any commitish (sha1,
tag/branch name, etc).

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 156 +++++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 145 insertions(+), 11 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index 53d8de7..7bb6f83 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -4041,6 +4041,19 @@ sub getlog
 This function takes a filename (with path) argument and returns a hashref of
 metadata for that file.
 
+There are several ways $revision can be specified:
+
+   - A reference to hash that contains a "tag" that is the
+     actual revision (one of the below).  TODO: Also allow it to
+     specify a "date" in the hash.
+   - undef, to refer to the latest version on the main branch.
+   - Full CVS client revision number (mapped to integer in DB, without the
+     "1." prefix),
+   - Complex CVS-compatible "special" revision number for
+     non-linear history (see comment below)
+   - git commit sha1 hash
+   - branch or tag name
+
 =cut
 
 sub getmeta
@@ -4051,23 +4064,144 @@ sub getmeta
     my $tablename_rev = $self->tablename("revision");
     my $tablename_head = $self->tablename("head");
 
-    my $db_query;
-    if ( defined($revision) and $revision =~ /^1\.(\d+)$/ )
+    if ( ref($revision) eq "HASH" )
     {
-        my ($intRev) = $1;
-        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND revision=?",{},1);
-        $db_query->execute($filename, $intRev);
+        $revision = $revision->{tag};
+    }
+
+    # Overview of CVS revision numbers:
+    #
+    # General CVS numbering scheme:
+    #   - Basic mainline branch numbers: "1.1", "1.2", "1.3", etc.
+    #   - Result of "cvs checkin -r" (possible, but not really
+    #     recommended): "2.1", "2.2", etc
+    #   - Branch tag: "1.2.0.n", where "1.2" is revision it was branched
+    #     from, "0" is a magic placeholder that identifies it as a
+    #     branch tag instead of a version tag, and n is 2 times the
+    #     branch number off of "1.2", starting with "2".
+    #   - Version on a branch: "1.2.n.x", where "1.2" is branch-from, "n"
+    #     is branch number off of "1.2" (like n above), and "x" is
+    #     the version number on the branch.
+    #   - Branches can branch off of branches: "1.3.2.7.4.1" (even number
+    #     of components).
+    #   - Odd "n"s are used by "vendor branches" that result
+    #     from "cvs import".  Vendor branches have additional
+    #     strangeness in the sense that the main rcs "head" of the main
+    #     branch will (temporarily until first normal commit) point
+    #     to the version on the vendor branch, rather than the actual
+    #     main branch.  (FUTURE: This may provide an opportunity
+    #     to use "strange" revision numbers for fast-forward-merged
+    #     branch tip when CVS client is asking for the main branch.)
+    #
+    # git-cvsserver CVS-compatible special numbering schemes:
+    #   - Currently git-cvsserver only tries to be identical to CVS for
+    #     simple "1.x" numbers on the "main" branch (as identified
+    #     by the module name that was originally cvs checkout'ed).
+    #   - The database only stores the "x" part, for historical reasons.
+    #     But most of the rest of the cvsserver preserves
+    #     and thinks using the full revision number.
+    #   - To handle non-linear history, it uses a version of the form
+    #     "2.1.1.2000.b.b.b."..., where the 2.1.1.2000 is to help uniquely
+    #     identify this as a special revision number, and there are
+    #     20 b's that together encode the sha1 git commit from which
+    #     this version of this file originated.  Each b is
+    #     the numerical value of the corresponding byte plus
+    #     100.
+    #      - "plus 100" avoids "0"s, and also reduces the
+    #        likelyhood of a collision in the case that someone someday
+    #        writes an import tool that tries to preserve original
+    #        CVS revision numbers, and the original CVS data had done
+    #        lots of branches off of branches and other strangeness to
+    #        end up with a real version number that just happens to look
+    #        like this special revision number form.  Also, if needed
+    #        there are several ways to extend/identify alternative encodings
+    #        within the "2.1.1.2000" part if necessary.
+    #      - Unlike real CVS revisions, you can't really reconstruct what
+    #        relation a revision of this form has to other revisions.
+    #   - FUTURE: TODO: Rework database somehow to make up and remember
+    #     fully-CVS-compatible branches and branch version numbers.
+
+    my $meta;
+    if ( defined($revision) )
+    {
+        if ( $revision =~ /^1\.(\d+)$/ )
+        {
+            my ($intRev) = $1;
+            my $db_query;
+            $db_query = $self->{dbh}->prepare_cached(
+                "SELECT * FROM $tablename_rev WHERE name=? AND revision=?",
+                {},1);
+            $db_query->execute($filename, $intRev);
+            $meta = $db_query->fetchrow_hashref;
+        }
+        elsif ( $revision =~ /^2\.1\.1\.2000(\.[1-3][0-9][0-9]){20}$/ )
+        {
+            my ($commitHash)=($revision=~/^2\.1\.1\.2000(.*)$/);
+            $commitHash=~s/\.([0-9]+)/sprintf("%02x",$1-100)/eg;
+            if($commitHash=~/^[0-9a-f]{40}$/)
+            {
+                return $self->getMetaFromCommithash($filename,$commitHash);
+            }
+
+            # error recovery: fall back on head version below
+            print "E Failed to find $filename version=$revision or commit=$commitHash\n";
+            $log->warning("failed get $revision with commithash=$commitHash");
+            undef $revision;
+        }
+        elsif ( $revision =~ /^[0-9a-f]{40}$/ )
+        {
+            # Try DB first.  This is mostly only useful for req_annotate(),
+            # which only calls this for stuff that should already be in
+            # the DB.  It is fairly likely to be a waste of time
+            # in most other cases [unless the file happened to be
+            # modified in $revision specifically], but
+            # it is probably in the noise compared to how long
+            # getMetaFromCommithash() will take.
+            my $db_query;
+            $db_query = $self->{dbh}->prepare_cached(
+                "SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",
+                {},1);
+            $db_query->execute($filename, $revision);
+            $meta = $db_query->fetchrow_hashref;
+
+            if(! $meta)
+            {
+                my($revCommit)=$self->lookupCommitRef($revision);
+                if($revCommit=~/^[0-9a-f]{40}$/)
+                {
+                    return $self->getMetaFromCommithash($filename,$revCommit);
+                }
+
+                # error recovery: nothing found:
+                print "E Failed to find $filename version=$revision\n";
+                $log->warning("failed get $revision");
+                return $meta;
+            }
+        }
+        else
+        {
+            my($revCommit)=$self->lookupCommitRef($revision);
+            if($revCommit=~/^[0-9a-f]{40}$/)
+            {
+                return $self->getMetaFromCommithash($filename,$revCommit);
+            }
+
+            # error recovery: fall back on head version below
+            print "E Failed to find $filename version=$revision\n";
+            $log->warning("failed get $revision");
+            undef $revision;  # Allow fallback
+        }
     }
-    elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
+
+    if(!defined($revision))
     {
-        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",{},1);
-        $db_query->execute($filename, $revision);
-    } else {
-        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_head WHERE name=?",{},1);
+        my $db_query;
+        $db_query = $self->{dbh}->prepare_cached(
+                "SELECT * FROM $tablename_head WHERE name=?",{},1);
         $db_query->execute($filename);
+        $meta = $db_query->fetchrow_hashref;
     }
 
-    my $meta = $db_query->fetchrow_hashref;
     if($meta)
     {
         $meta->{revision} = "1.$meta->{revision}";
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 17/20] cvsserver: Add version awareness to argsfromdir
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (15 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 16/20] cvsserver: generalize getmeta() to recognize commit refs Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 18/20] cvsserver: support -r and sticky tags for most operations Matthew Ogilvie
                   ` (3 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 228 ++++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 198 insertions(+), 30 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index 7bb6f83..5e558d1 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -2226,55 +2226,222 @@ sub argsplit
     }
 }
 
-# This method uses $state->{directory} to populate $state->{args} with a list of filenames
-sub argsfromdir
+# Used by argsfromdir
+sub expandArg
 {
-    my $updater = shift;
+    my ($updater,$outNameMap,$outDirMap,$path,$isDir) = @_;
 
-    $state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." );
+    my $fullPath = filecleanup($path);
 
-    return if ( scalar ( @{$state->{args}} ) > 1 );
+      # Is it a directory?
+    if( defined($state->{dirMap}{$fullPath}) ||
+        defined($state->{dirMap}{"$fullPath/"}) )
+    {
+          # It is a directory in the user's sandbox.
+        $isDir=1;
 
-    my @gethead = @{$updater->gethead};
+        if(defined($state->{entries}{$fullPath}))
+        {
+            $log->fatal("Inconsistent file/dir type");
+            die "Inconsistent file/dir type";
+        }
+    }
+    elsif(defined($state->{entries}{$fullPath}))
+    {
+          # It is a file in the user's sandbox.
+        $isDir=0;
+    }
+    my($revDirMap,$otherRevDirMap);
+    if(!defined($isDir) || $isDir)
+    {
+          # Resolve version tree for sticky tag:
+          # (for now we only want list of files for the version, not
+          # particular versions of those files: assume it is a directory
+          # for the moment; ignore Entry's stick tag)
+
+          # Order of precedence of sticky tags:
+          #    -A       [head]
+          #    -r /tag/
+          #    [file entry sticky tag, but that is only relevant to files]
+          #    [the tag specified in dir req_Sticky]
+          #    [the tag specified in a parent dir req_Sticky]
+          #    [head]
+          # Also, -r may appear twice (for diff).
+          #
+          # FUTURE: When/if -j (merges) are supported, we also
+          #  need to add relevant files from one or two
+          #  versions specified with -j.
+
+        if(exists($state->{opt}{A}))
+        {
+            $revDirMap=$updater->getRevisionDirMap();
+        }
+        elsif( defined($state->{opt}{r}) and
+               ref $state->{opt}{r} eq "ARRAY" )
+        {
+            $revDirMap=$updater->getRevisionDirMap($state->{opt}{r}[0]);
+            $otherRevDirMap=$updater->getRevisionDirMap($state->{opt}{r}[1]);
+        }
+        elsif(defined($state->{opt}{r}))
+        {
+            $revDirMap=$updater->getRevisionDirMap($state->{opt}{r});
+        }
+        else
+        {
+            my($sticky)=getDirStickyInfo($fullPath);
+            $revDirMap=$updater->getRevisionDirMap($sticky->{tag});
+        }
 
-    # push added files
-    foreach my $file (keys %{$state->{entries}}) {
-	if ( exists $state->{entries}{$file}{revision} &&
-		$state->{entries}{$file}{revision} eq '0' )
-	{
-	    push @gethead, { name => $file, filehash => 'added' };
-	}
+          # Is it a directory?
+        if( defined($revDirMap->{$fullPath}) ||
+            defined($otherRevDirMap->{$fullPath}) )
+        {
+            $isDir=1;
+        }
     }
 
-    if ( scalar(@{$state->{args}}) == 1 )
+      # What to do with it?
+    if(!$isDir)
     {
-        my $arg = $state->{args}[0];
-        $arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) );
-
-        $log->info("Only one arg specified, checking for directory expansion on '$arg'");
+        $outNameMap->{$fullPath}=1;
+    }
+    else
+    {
+        $outDirMap->{$fullPath}=1;
 
-        foreach my $file ( @gethead )
+        if(defined($revDirMap->{$fullPath}))
         {
-            next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
-            next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg  );
-            push @{$state->{args}}, $file->{name};
+            addDirMapFiles($updater,$outNameMap,$outDirMap,
+                           $revDirMap->{$fullPath});
         }
+        if( defined($otherRevDirMap) &&
+            defined($otherRevDirMap->{$fullPath}) )
+        {
+            addDirMapFiles($updater,$outNameMap,$outDirMap,
+                           $otherRevDirMap->{$fullPath});
+        }
+    }
+}
 
-        shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 );
-    } else {
-        $log->info("Only one arg specified, populating file list automatically");
+# Used by argsfromdir
+# Add entries from dirMap to outNameMap.  Also recurse into entries
+# that are subdirectories.
+sub addDirMapFiles
+{
+    my($updater,$outNameMap,$outDirMap,$dirMap)=@_;
 
-        $state->{args} = [];
+    my($fullName);
+    foreach $fullName (keys(%$dirMap))
+    {
+        my $cleanName=$fullName;
+        if(defined($state->{prependdir}))
+        {
+            if(!($cleanName=~s/^\Q$state->{prependdir}\E//))
+            {
+                $log->fatal("internal error stripping prependdir");
+                die "internal error stripping prependdir";
+            }
+        }
 
-        foreach my $file ( @gethead )
+        if($dirMap->{$fullName} eq "F")
+        {
+            $outNameMap->{$cleanName}=1;
+        }
+        elsif($dirMap->{$fullName} eq "D")
+        {
+            if(!$state->{opt}{l})
+            {
+                expandArg($updater,$outNameMap,$outDirMap,$cleanName,1);
+            }
+        }
+        else
         {
-            next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
-            next unless ( $file->{name} =~ s/^$state->{prependdir}// );
-            push @{$state->{args}}, $file->{name};
+            $log->fatal("internal error in addDirMapFiles");
+            die "internal error in addDirMapFiles";
         }
     }
 }
 
+# This method replaces $state->{args} with a directory-expanded
+# list of all relevant filenames (recursively unless -d), based
+# on $state->{entries}, and the "current" list of files in
+# each directory.  "Current" files as determined by
+# either the requested (-r/-A) or "req_Sticky" version of
+# that directory.
+#    Both the input args and the new output args are relative
+# to the cvs-client's CWD, although some of the internal
+# computations are relative to the top of the project.
+sub argsfromdir
+{
+    my $updater = shift;
+
+    # Notes about requirements for specific callers:
+    #   update # "standard" case (entries; a single -r/-A/default; -l)
+    #          # Special case: -d for create missing directories.
+    #   diff # 0 or 1 -r's: "standard" case.
+    #        # 2 -r's: We could ignore entries (just use the two -r's),
+    #        # but it doesn't really matter.
+    #   annotate # "standard" case
+    #   log # Punting: log -r has a more complex non-"standard"
+    #       # meaning, and we don't currently try to support log'ing
+    #       # branches at all (need a lot of work to
+    #       # support CVS-consistent branch relative version
+    #       # numbering).
+#HERE: But we still want to expand directories.  Maybe we should
+#  essentially force "-A".
+    #   status # "standard", except that -r/-A/default are not possible.
+    #          # Mostly only used to expand entries only)
+    #
+    # Don't use argsfromdir at all:
+    #   add # Explicit arguments required.  Directory args imply add
+    #       # the directory itself, not the files in it.
+    #   co  # Obtain list directly.
+    #   remove # HERE: TEST: MAYBE client does the recursion for us,
+    #          # since it only makes sense to remove stuff already in
+    #          # the sandobx?
+    #   ci # HERE: Similar to remove...
+    #      # Don't try to implement the confusing/weird
+    #      # ci -r bug er.."feature".
+
+    if(scalar(@{$state->{args}})==0)
+    {
+        $state->{args} = [ "." ];
+    }
+    my %allArgs;
+    my %allDirs;
+    for my $file (@{$state->{args}})
+    {
+        expandArg($updater,\%allArgs,\%allDirs,$file);
+    }
+
+    # Include any entries from sandbox.  Generally client won't
+    # send entries that shouldn't be used.
+    foreach my $file (keys %{$state->{entries}})
+    {
+        $allArgs{remove_prependdir($file)} = 1;
+    }
+
+    $state->{dirArgs} = \%allDirs;
+    $state->{args} = [
+        sort {
+                # Sort priority: by directory depth, then actual file name:
+            my @piecesA=split('/',$a);
+            my @piecesB=split('/',$b);
+
+            my $count=scalar(@piecesA);
+            my $tmp=scalar(@piecesB);
+            return $count<=>$tmp if($count!=$tmp);
+
+            for($tmp=0;$tmp<$count;$tmp++)
+            {
+                if($piecesA[$tmp] ne $piecesB[$tmp])
+                {
+                    return $piecesA[$tmp] cmp $piecesB[$tmp]
+                }
+            }
+            return 0;
+        } keys(%allArgs) ];
+}
 
 ## look up directory sticky tag, of either fullPath or a parent:
 sub getDirStickyInfo
@@ -2383,6 +2550,7 @@ sub getStickyTagOrDate
 sub statecleanup
 {
     $state->{files} = [];
+    $state->{dirArgs} = {};
     $state->{args} = [];
     $state->{arguments} = [];
     $state->{entries} = {};
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 18/20] cvsserver: support -r and sticky tags for most operations
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (16 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 17/20] cvsserver: Add version awareness to argsfromdir Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 19/20] cvsserver: add t9402 to test branch and tag refs Matthew Ogilvie
                   ` (2 subsequent siblings)
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

  - Split off prepDirForOutput for "update" and "commit".
    Some low level protocol details were changed to more closely
    resemble CVS even in non-tagged cases.  Hopefully it still works
    with finicky clients like Eclipse.
  - Substantial changes to "diff".  The output is now closer to
    standard CVS (including exit status), and can be used as
    a patch, but there are still a number of differences compared
    to CVS.
  - Tweaks to "add", "remove", "status", and "commit".
  - FUTURE: CVS revision numbers for branches simply encode git
    commit IDs in a way that resembles CVS revision numbers,
    dropping all normal CVS structural relations between different
    revision numbers.
  - FUTURE: "log" doesn't try to work properly at all with branches
    and tags.
  - FUTURE: "annotate" probably doesn't work with branches or
    tags either (untested)?

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 git-cvsserver.perl | 713 ++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 510 insertions(+), 203 deletions(-)

diff --git a/git-cvsserver.perl b/git-cvsserver.perl
index 5e558d1..3679074 100755
--- a/git-cvsserver.perl
+++ b/git-cvsserver.perl
@@ -611,7 +611,10 @@ sub req_add
     {
         $filename = filecleanup($filename);
 
-        my $meta = $updater->getmeta($filename);
+        # no -r, -A, or -D with add
+        my $stickyInfo = resolveStickyInfo($filename);
+
+        my $meta = $updater->getmeta($filename,$stickyInfo);
         my $wrev = revparse($filename);
 
         if ($wrev && $meta && ($wrev=~/^-/))
@@ -634,8 +637,10 @@ sub req_add
 
                 # this is an "entries" line
                 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-                $log->debug("/$filepart/$meta->{revision}//$kopts/");
-                print "/$filepart/$meta->{revision}//$kopts/\n";
+                my $entryLine = "/$filepart/$meta->{revision}//$kopts/";
+                $entryLine .= getStickyTagOrDate($stickyInfo);
+                $log->debug($entryLine);
+                print "$entryLine\n";
                 # permissions
                 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
                 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
@@ -666,7 +671,8 @@ sub req_add
         print "$filename\n";
         my $kopts = kopts_from_path($filename,"file",
                         $state->{entries}{$filename}{modified_filename});
-        print "/$filepart/0//$kopts/\n";
+        print "/$filepart/0//$kopts/" .
+              getStickyTagOrDate($stickyInfo) . "\n";
 
         my $requestedKopts = $state->{opt}{k};
         if(defined($requestedKopts))
@@ -734,7 +740,10 @@ sub req_remove
             next;
         }
 
-        my $meta = $updater->getmeta($filename);
+        # only from entries
+        my $stickyInfo = resolveStickyInfo($filename);
+
+        my $meta = $updater->getmeta($filename,$stickyInfo);
         my $wrev = revparse($filename);
 
         unless ( defined ( $wrev ) )
@@ -764,7 +773,7 @@ sub req_remove
         print "Checked-in $dirpart\n";
         print "$filename\n";
         my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-        print "/$filepart/-$wrev//$kopts/\n";
+        print "/$filepart/-$wrev//$kopts/" . getStickyTagOrDate($stickyInfo) . "\n";
 
         $rmcount++;
     }
@@ -944,6 +953,9 @@ sub req_co
         return 1;
     }
 
+    my $stickyInfo = { 'tag' => $state->{opt}{r},
+                       'date' => $state->{opt}{D} };
+
     my $module = $state->{args}[0];
     $state->{module} = $module;
     my $checkout_path = $module;
@@ -961,64 +973,32 @@ sub req_co
     my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
     $updater->update();
 
-    $checkout_path =~ s|/$||; # get rid of trailing slashes
+    my $headHash;
+    if( defined($stickyInfo) && defined($stickyInfo->{tag}) )
+    {
+        $headHash = $updater->lookupCommitRef($stickyInfo->{tag});
+        if( !defined($headHash) )
+        {
+            print "error 1 no such tag `$stickyInfo->{tag}'\n";
+            cleanupWorkTree();
+            exit;
+        }
+    }
 
-    # Eclipse seems to need the Clear-sticky command
-    # to prepare the 'Entries' file for the new directory.
-    print "Clear-sticky $checkout_path/\n";
-    print $state->{CVSROOT} . "/$module/\n";
-    print "Clear-static-directory $checkout_path/\n";
-    print $state->{CVSROOT} . "/$module/\n";
-    print "Clear-sticky $checkout_path/\n"; # yes, twice
-    print $state->{CVSROOT} . "/$module/\n";
-    print "Template $checkout_path/\n";
-    print $state->{CVSROOT} . "/$module/\n";
-    print "0\n";
-
-    # instruct the client that we're checking out to $checkout_path
-    print "E cvs checkout: Updating $checkout_path\n";
+    $checkout_path =~ s|/$||; # get rid of trailing slashes
 
     my %seendirs = ();
     my $lastdir ='';
 
-    # recursive
-    sub prepdir {
-       my ($dir, $repodir, $remotedir, $seendirs) = @_;
-       my $parent = dirname($dir);
-       $dir       =~ s|/+$||;
-       $repodir   =~ s|/+$||;
-       $remotedir =~ s|/+$||;
-       $parent    =~ s|/+$||;
-       $log->debug("announcedir $dir, $repodir, $remotedir" );
-
-       if ($parent eq '.' || $parent eq './') {
-           $parent = '';
-       }
-       # recurse to announce unseen parents first
-       if (length($parent) && !exists($seendirs->{$parent})) {
-           prepdir($parent, $repodir, $remotedir, $seendirs);
-       }
-       # Announce that we are going to modify at the parent level
-       if ($parent) {
-           print "E cvs checkout: Updating $remotedir/$parent\n";
-       } else {
-           print "E cvs checkout: Updating $remotedir\n";
-       }
-       print "Clear-sticky $remotedir/$parent/\n";
-       print "$repodir/$parent/\n";
-
-       print "Clear-static-directory $remotedir/$dir/\n";
-       print "$repodir/$dir/\n";
-       print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
-       print "$repodir/$parent/\n";
-       print "Template $remotedir/$dir/\n";
-       print "$repodir/$dir/\n";
-       print "0\n";
-
-       $seendirs->{$dir} = 1;
-    }
-
-    foreach my $git ( @{$updater->gethead} )
+    prepDirForOutput(
+            ".",
+            $state->{CVSROOT} . "/$module",
+            $checkout_path,
+            \%seendirs,
+            'checkout',
+            $state->{dirArgs} );
+
+    foreach my $git ( @{$updater->getAnyHead($headHash)} )
     {
         # Don't want to check out deleted files
         next if ( $git->{filehash} eq "deleted" );
@@ -1026,16 +1006,13 @@ sub req_co
         my $fullName = $git->{name};
         ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
 
-       if (length($git->{dir}) && $git->{dir} ne './'
-           && $git->{dir} ne $lastdir ) {
-           unless (exists($seendirs{$git->{dir}})) {
-               prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
-                       $checkout_path, \%seendirs);
-               $lastdir = $git->{dir};
-               $seendirs{$git->{dir}} = 1;
-           }
-           print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
-       }
+        unless (exists($seendirs{$git->{dir}})) {
+            prepDirForOutput($git->{dir}, $state->{CVSROOT} . "/$module/",
+                             $checkout_path, \%seendirs, 'checkout',
+                             $state->{dirArgs} );
+            $lastdir = $git->{dir};
+            $seendirs{$git->{dir}} = 1;
+        }
 
         # modification time of this file
         print "Mod-time $git->{modified}\n";
@@ -1055,7 +1032,8 @@ sub req_co
 
         # this is an "entries" line
         my $kopts = kopts_from_path($fullName,"sha1",$git->{filehash});
-        print "/$git->{name}/$git->{revision}//$kopts/\n";
+        print "/$git->{name}/$git->{revision}//$kopts/" .
+                        getStickyTagOrDate($stickyInfo) . "\n";
         # permissions
         print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
 
@@ -1068,6 +1046,119 @@ sub req_co
     statecleanup();
 }
 
+# used by req_co and req_update to set up directories for files
+# recursively handles parents
+sub prepDirForOutput
+{
+    my ($dir, $repodir, $remotedir, $seendirs, $request, $dirArgs) = @_;
+
+    my $parent = dirname($dir);
+    $dir       =~ s|/+$||;
+    $repodir   =~ s|/+$||;
+    $remotedir =~ s|/+$||;
+    $parent    =~ s|/+$||;
+
+    if ($parent eq '.' || $parent eq './')
+    {
+        $parent = '';
+    }
+    # recurse to announce unseen parents first
+    if( length($parent) &&
+        !exists($seendirs->{$parent}) &&
+        ( $request eq "checkout" ||
+          exists($dirArgs->{$parent}) ) )
+    {
+        prepDirForOutput($parent, $repodir, $remotedir,
+                         $seendirs, $request, $dirArgs);
+    }
+    # Announce that we are going to modify at the parent level
+    if ($dir eq '.' || $dir eq './')
+    {
+        $dir = '';
+    }
+    if(exists($seendirs->{$dir}))
+    {
+        return;
+    }
+    $log->debug("announcedir $dir, $repodir, $remotedir" );
+    my($thisRemoteDir,$thisRepoDir);
+    if ($dir ne "")
+    {
+        $thisRepoDir="$repodir/$dir";
+        if($remotedir eq ".")
+        {
+            $thisRemoteDir=$dir;
+        }
+        else
+        {
+            $thisRemoteDir="$remotedir/$dir";
+        }
+    }
+    else
+    {
+        $thisRepoDir=$repodir;
+        $thisRemoteDir=$remotedir;
+    }
+    unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
+    {
+        print "E cvs $request: Updating $thisRemoteDir\n";
+    }
+
+    my ($opt_r)=$state->{opt}{r};
+    my $stickyInfo;
+    if(exists($state->{opt}{A}))
+    {
+        # $stickyInfo=undef;
+    }
+    elsif( defined($opt_r) && $opt_r ne "" )
+           # || ( defined($state->{opt}{D}) && $state->{opt}{D} ne "" ) # TODO
+    {
+        $stickyInfo={ 'tag' => (defined($opt_r)?$opt_r:undef) };
+
+        # TODO: Convert -D value into the form 2011.04.10.04.46.57,
+        #   similar to an entry line's sticky date, without the D prefix.
+        #   It sometimes (always?) arrives as something more like
+        #   '10 Apr 2011 04:46:57 -0000'...
+        # $stickyInfo={ 'date' => (defined($stickyDate)?$stickyDate:undef) };
+    }
+    else
+    {
+        $stickyInfo=getDirStickyInfo($state->{prependdir} . $dir);
+    }
+
+    my $stickyResponse;
+    if(defined($stickyInfo))
+    {
+        $stickyResponse = "Set-sticky $thisRemoteDir/\n" .
+                          "$thisRepoDir/\n" .
+                          getStickyTagOrDate($stickyInfo) . "\n";
+    }
+    else
+    {
+        $stickyResponse = "Clear-sticky $thisRemoteDir/\n" .
+                          "$thisRepoDir/\n";
+    }
+
+    unless ( $state->{globaloptions}{-n} )
+    {
+        print $stickyResponse;
+
+        print "Clear-static-directory $thisRemoteDir/\n";
+        print "$thisRepoDir/\n";
+        print $stickyResponse; # yes, twice
+        print "Template $thisRemoteDir/\n";
+        print "$thisRepoDir/\n";
+        print "0\n";
+    }
+
+    $seendirs->{$dir} = 1;
+
+    # FUTURE: This would more accurately emulate CVS by sending
+    #   another copy of sticky after processing the files in that
+    #   directory.  Or intermediate: perhaps send all sticky's for
+    #   $seendirs after after processing all files.
+}
+
 # update \n
 #     Response expected: yes. Actually do a cvs update command. This uses any
 #     previous Argument, Directory, Entry, or Modified requests, if they have
@@ -1111,29 +1202,19 @@ sub req_update
 
     #$log->debug("update state : " . Dumper($state));
 
-    my $last_dirname = "///";
+    my($repoDir);
+    $repoDir=$state->{CVSROOT} . "/$state->{module}/$state->{prependdir}";
+
+    my %seendirs = ();
 
     # foreach file specified on the command line ...
-    foreach my $filename ( @{$state->{args}} )
+    foreach my $argsFilename ( @{$state->{args}} )
     {
-        $filename = filecleanup($filename);
+        my $filename;
+        $filename = filecleanup($argsFilename);
 
         $log->debug("Processing file $filename");
 
-        unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
-        {
-            my $cur_dirname = dirname($filename);
-            if ( $cur_dirname ne $last_dirname )
-            {
-                $last_dirname = $cur_dirname;
-                if ( $cur_dirname eq "" )
-                {
-                    $cur_dirname = ".";
-                }
-                print "E cvs update: Updating $cur_dirname\n";
-            }
-        }
-
         # if we have a -C we should pretend we never saw modified stuff
         if ( exists ( $state->{opt}{C} ) )
         {
@@ -1142,13 +1223,11 @@ sub req_update
             $state->{entries}{$filename}{unchanged} = 1;
         }
 
-        my $meta;
-        if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^(1\.\d+)$/ )
-        {
-            $meta = $updater->getmeta($filename, $1);
-        } else {
-            $meta = $updater->getmeta($filename);
-        }
+        my $stickyInfo = resolveStickyInfo($filename,
+                                           $state->{opt}{r},
+                                           $state->{opt}{D},
+                                           exists($state->{opt}{A}));
+        my $meta = $updater->getmeta($filename, $stickyInfo);
 
         # If -p was given, "print" the contents of the requested revision.
         if ( exists ( $state->{opt}{p} ) ) {
@@ -1161,6 +1240,17 @@ sub req_update
             next;
         }
 
+        # Directories:
+        prepDirForOutput(
+                dirname($argsFilename),
+                $repoDir,
+                ".",
+                \%seendirs,
+                "update",
+                $state->{dirArgs} );
+
+        my $wrev = revparse($filename);
+
 	if ( ! defined $meta )
 	{
 	    $meta = {
@@ -1168,16 +1258,23 @@ sub req_update
 	        revision => '0',
 	        filehash => 'added'
 	    };
+	    if($wrev ne "0")
+	    {
+	        $meta->{filehash}='deleted';
+	    }
 	}
 
         my $oldmeta = $meta;
 
-        my $wrev = revparse($filename);
-
         # If the working copy is an old revision, lets get that version too for comparison.
-        if ( defined($wrev) and $wrev ne $meta->{revision} )
+        my $oldWrev=$wrev;
+        if(defined($oldWrev))
         {
-            $oldmeta = $updater->getmeta($filename, $wrev);
+            $oldWrev=~s/^-//;
+            if($oldWrev ne $meta->{revision})
+            {
+                $oldmeta = $updater->getmeta($filename, $oldWrev);
+            }
         }
 
         #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
@@ -1195,6 +1292,7 @@ sub req_update
         if ( defined ( $wrev )
              and defined($meta->{revision})
              and $wrev eq $meta->{revision}
+             and $wrev ne "0"
              and defined($state->{entries}{$filename}{modified_hash})
              and not exists ( $state->{opt}{C} ) )
         {
@@ -1205,7 +1303,7 @@ sub req_update
             next;
         }
 
-        if ( $meta->{filehash} eq "deleted" )
+        if ( $meta->{filehash} eq "deleted" && $wrev ne "0" )
         {
             # TODO: If it has been modified in the sandbox, error out
             #   with the appropriate message, rather than deleting a modified
@@ -1267,10 +1365,6 @@ sub req_update
 		    $log->debug("Updating existing file 'Update-existing $dirpart'");
 		} else {
 		    # instruct client we're sending a file to put in this path as a new file
-		    print "Clear-static-directory $dirpart\n";
-		    print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
-		    print "Clear-sticky $dirpart\n";
-		    print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
 
 		    $log->debug("Creating new file 'Created $dirpart'");
 		    print "Created $dirpart\n";
@@ -1279,8 +1373,10 @@ sub req_update
 
 		# this is an "entries" line
 		my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-		$log->debug("/$filepart/$meta->{revision}//$kopts/");
-		print "/$filepart/$meta->{revision}//$kopts/\n";
+                my $entriesLine = "/$filepart/$meta->{revision}//$kopts/";
+                $entriesLine .= getStickyTagOrDate($stickyInfo);
+		$log->debug($entriesLine);
+		print "$entriesLine\n";
 
 		# permissions
 		$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
@@ -1328,7 +1424,9 @@ sub req_update
                     my $kopts = kopts_from_path("$dirpart/$filepart",
                                                 "file",$mergedFile);
                     $log->debug("/$filepart/$meta->{revision}//$kopts/");
-                    print "/$filepart/$meta->{revision}//$kopts/\n";
+                    my $entriesLine="/$filepart/$meta->{revision}//$kopts/";
+                    $entriesLine .= getStickyTagOrDate($stickyInfo);
+                    print "$entriesLine\n";
                 }
             }
             elsif ( $return == 1 )
@@ -1344,7 +1442,9 @@ sub req_update
                     print $state->{CVSROOT} . "/$state->{module}/$filename\n";
                     my $kopts = kopts_from_path("$dirpart/$filepart",
                                                 "file",$mergedFile);
-                    print "/$filepart/$meta->{revision}/+/$kopts/\n";
+                    my $entriesLine = "/$filepart/$meta->{revision}/+/$kopts/";
+                    $entriesLine .= getStickyTagOrDate($stickyInfo);
+                    print "$entriesLine\n";
                 }
             }
             else
@@ -1372,6 +1472,43 @@ sub req_update
 
     }
 
+    # prepDirForOutput() any other existing directories unless they already
+    # have the right sticky tag:
+    unless ( $state->{globaloptions}{n} )
+    {
+        my $dir;
+        foreach $dir (keys(%{$state->{dirMap}}))
+        {
+            if( ! $seendirs{$dir} &&
+                exists($state->{dirArgs}{$dir}) )
+            {
+                my($oldTag);
+                $oldTag=$state->{dirMap}{$dir}{tagspec};
+
+                unless( ( exists($state->{opt}{A}) &&
+                          defined($oldTag) ) ||
+                          ( defined($state->{opt}{r}) &&
+                            ( !defined($oldTag) ||
+                              $state->{opt}{r} ne $oldTag ) ) )
+                        # TODO?: OR sticky dir is different...
+                {
+                    next;
+                }
+
+                prepDirForOutput(
+                        $dir,
+                        $repoDir,
+                        ".",
+                        \%seendirs,
+                        'update',
+                        $state->{dirArgs} );
+            }
+
+            # TODO?: Consider sending a final duplicate Sticky response
+            #   to more closely mimic real CVS.
+        }
+    }
+
     print "ok\n";
 }
 
@@ -1404,23 +1541,11 @@ sub req_ci
     my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
     $updater->update();
 
-    # Remember where the head was at the beginning.
-    my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
-    chomp $parenthash;
-    if ($parenthash !~ /^[0-9a-f]{40}$/) {
-	    print "error 1 pserver cannot find the current HEAD of module";
-	    cleanupWorkTree();
-	    exit;
-    }
-
-    setupWorkTree($parenthash);
-
-    $log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
-
-    $log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
-
     my @committedfiles = ();
     my %oldmeta;
+    my $stickyInfo;
+    my $branchRef;
+    my $parenthash;
 
     # foreach file specified on the command line ...
     foreach my $filename ( @{$state->{args}} )
@@ -1430,7 +1555,67 @@ sub req_ci
 
         next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
 
-        my $meta = $updater->getmeta($filename);
+        #####
+        # Figure out which branch and parenthash we are committing
+        # to, and setup worktree:
+
+        # should always come from entries:
+        my $fileStickyInfo = resolveStickyInfo($filename);
+        if( !defined($branchRef) )
+        {
+            $stickyInfo = $fileStickyInfo;
+            if( defined($stickyInfo) &&
+                ( defined($stickyInfo->{date}) ||
+                  !defined($stickyInfo->{tag}) ) )
+            {
+                print "error 1 cannot commit with sticky date for file `$filename'\n";
+                cleanupWorkTree();
+                exit;
+            }
+
+            $branchRef = "refs/heads/$state->{module}";
+            if ( defined($stickyInfo) && defined($stickyInfo->{tag}) )
+            {
+                $branchRef = "refs/heads/$stickyInfo->{tag}";
+            }
+
+            $parenthash = `git show-ref -s $branchRef`;
+            chomp $parenthash;
+            if ($parenthash !~ /^[0-9a-f]{40}$/)
+            {
+                if ( defined($stickyInfo) && defined($stickyInfo->{tag}) )
+                {
+                    print "error 1 sticky tag `$stickyInfo->{tag}' for file `$filename' is not a branch\n";
+                }
+                else
+                {
+                    print "error 1 pserver cannot find the current HEAD of module";
+                }
+                cleanupWorkTree();
+                exit;
+            }
+
+            setupWorkTree($parenthash);
+
+            $log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
+
+            $log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
+        }
+        elsif( !refHashEqual($stickyInfo,$fileStickyInfo) )
+        {
+            #TODO: We could split the cvs commit into multiple
+            #  git commits by distinct stickyTag values, but that
+            #  is lowish priority.
+            print "error 1 Committing different files to different"
+                  . " branches is not currently supported\n";
+            cleanupWorkTree();
+            exit;
+        }
+
+        #####
+        # Process this file:
+
+        my $meta = $updater->getmeta($filename,$stickyInfo);
 	$oldmeta{$filename} = $meta;
 
         my $wrev = revparse($filename);
@@ -1532,7 +1717,7 @@ sub req_ci
     }
 
 	### Emulate git-receive-pack by running hooks/update
-	my @hook = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
+	my @hook = ( $ENV{GIT_DIR}.'hooks/update', $branchRef,
 			$parenthash, $commithash );
 	if( -x $hook[0] ) {
 		unless( system( @hook ) == 0 )
@@ -1546,7 +1731,7 @@ sub req_ci
 
 	### Update the ref
 	if (system(qw(git update-ref -m), "cvsserver ci",
-			"refs/heads/$state->{module}", $commithash, $parenthash)) {
+			$branchRef, $commithash, $parenthash)) {
 		$log->warn("update-ref for $state->{module} failed.");
 		print "error 1 Cannot commit -- update first\n";
 		cleanupWorkTree();
@@ -1560,7 +1745,7 @@ sub req_ci
 
 		local $SIG{PIPE} = sub { die 'pipe broke' };
 
-		print $pipe "$parenthash $commithash refs/heads/$state->{module}\n";
+		print $pipe "$parenthash $commithash $branchRef\n";
 
 		close $pipe || die "bad pipe: $! $?";
 	}
@@ -1570,7 +1755,7 @@ sub req_ci
 	### Then hooks/post-update
 	$hook = $ENV{GIT_DIR}.'hooks/post-update';
 	if (-x $hook) {
-		system($hook, "refs/heads/$state->{module}");
+		system($hook, $branchRef);
 	}
 
     # foreach file specified on the command line ...
@@ -1578,7 +1763,7 @@ sub req_ci
     {
         $filename = filecleanup($filename);
 
-        my $meta = $updater->getmeta($filename);
+        my $meta = $updater->getmeta($filename,$stickyInfo);
 	unless (defined $meta->{revision}) {
 	  $meta->{revision} = "1.1";
 	}
@@ -1602,7 +1787,8 @@ sub req_ci
             print "Checked-in $dirpart\n";
             print "$filename\n";
             my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-            print "/$filepart/$meta->{revision}//$kopts/\n";
+            print "/$filepart/$meta->{revision}//$kopts/" .
+                  getStickyTagOrDate($stickyInfo) . "\n";
         }
     }
 
@@ -1639,16 +1825,19 @@ sub req_status
            next;
         }
 
-        my $meta = $updater->getmeta($filename);
-        my $oldmeta = $meta;
-
         my $wrev = revparse($filename);
 
+        my $stickyInfo = resolveStickyInfo($filename);
+        my $meta = $updater->getmeta($filename,$stickyInfo);
+        my $oldmeta = $meta;
+
         # If the working copy is an old revision, lets get that
         # version too for comparison.
         if ( defined($wrev) and $wrev ne $meta->{revision} )
         {
-            $oldmeta = $updater->getmeta($filename, $wrev);
+            my($rmRev)=$wrev;
+            $rmRev=~s/^-//;
+            $oldmeta = $updater->getmeta($filename, $rmRev);
         }
 
         # TODO : All possible statuses aren't yet implemented
@@ -1691,6 +1880,7 @@ sub req_status
         # same revision but there are local changes
         if ( defined ( $wrev ) and defined($meta->{revision}) and
              $wrev eq $meta->{revision} and
+             $wrev ne "0" and
              $state->{entries}{$filename}{modified_filename} )
         {
             $status ||= "Locally Modified";
@@ -1706,7 +1896,8 @@ sub req_status
         }
 
         if ( defined ( $state->{entries}{$filename}{revision} ) and
-             not defined ( $meta->{revision} ) )
+             ( !defined($meta->{revision}) ||
+               $meta->{revision} eq "0" ) )
         {
             $status ||= "Locally Added";
         }
@@ -1802,98 +1993,133 @@ sub req_diff
     # be providing status on ...
     argsfromdir($updater);
 
+    my($foundDiff);
+
     # foreach file specified on the command line ...
-    foreach my $filename ( @{$state->{args}} )
+    foreach my $argFilename ( @{$state->{args}} )
     {
-        $filename = filecleanup($filename);
+        my($filename) = filecleanup($argFilename);
 
         my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
 
         my $wrev = revparse($filename);
 
-        # We need _something_ to diff against
-        next unless ( defined ( $wrev ) );
+        # Priority for revision1:
+        #  1. First -r (missing file: check -N)
+        #  2. wrev from client's Entry line
+        #      - missing line/file: check -N
+        #      - "0": added file not committed (empty contents for rev1)
+        #      - Prefixed with dash (to be removed): check -N
 
-        # if we have a -r switch, use it
         if ( defined ( $revision1 ) )
         {
-            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
             $meta1 = $updater->getmeta($filename, $revision1);
-            unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
+        }
+        elsif( defined($wrev) && $wrev ne "0" )
+        {
+            my($rmRev)=$wrev;
+            $rmRev=~s/^-//;
+            $meta1 = $updater->getmeta($filename, $rmRev);
+        }
+        if ( !defined($meta1) ||
+             $meta1->{filehash} eq "deleted" )
+        {
+            if( !exists($state->{opt}{N}) )
             {
-                print "E File $filename at revision $revision1 doesn't exist\n";
+                if(!defined($revision1))
+                {
+                    print "E File $filename at revision $revision1 doesn't exist\n";
+                }
                 next;
             }
-            transmitfile($meta1->{filehash}, { targetfile => $file1 });
-        }
-        # otherwise we just use the working copy revision
-        else
-        {
-            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
-            $meta1 = $updater->getmeta($filename, $wrev);
-            transmitfile($meta1->{filehash}, { targetfile => $file1 });
+            elsif( !defined($meta1) )
+            {
+                $meta1 = {
+                    name => $filename,
+                    revision => '0',
+                    filehash => 'deleted'
+                };
+            }
         }
 
+        # Priority for revision2:
+        #  1. Second -r (missing file: check -N)
+        #  2. Modified file contents from client
+        #  3. wrev from client's Entry line
+        #      - missing line/file: check -N
+        #      - Prefixed with dash (to be removed): check -N
+
         # if we have a second -r switch, use it too
         if ( defined ( $revision2 ) )
         {
-            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
             $meta2 = $updater->getmeta($filename, $revision2);
-
-            unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
-            {
-                print "E File $filename at revision $revision2 doesn't exist\n";
-                next;
-            }
-
-            transmitfile($meta2->{filehash}, { targetfile => $file2 });
         }
-        # otherwise we just use the working copy
-        else
+        elsif(defined($state->{entries}{$filename}{modified_filename}))
         {
             $file2 = $state->{entries}{$filename}{modified_filename};
+	    $meta2 = {
+                name => $filename,
+	        revision => '0',
+	        filehash => 'modified'
+            };
         }
-
-        # if we have been given -r, and we don't have a $file2 yet, lets
-        # get one
-        if ( defined ( $revision1 ) and not defined ( $file2 ) )
+        elsif( defined($wrev) && ($wrev!~/^-/) )
         {
-            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+            if(!defined($revision1))  # no revision and no modifications:
+            {
+                next;
+            }
             $meta2 = $updater->getmeta($filename, $wrev);
-            transmitfile($meta2->{filehash}, { targetfile => $file2 });
+        }
+        if(!defined($file2))
+        {
+            if ( !defined($meta2) ||
+                 $meta2->{filehash} eq "deleted" )
+            {
+                if( !exists($state->{opt}{N}) )
+                {
+                    if(!defined($revision2))
+                    {
+                        print "E File $filename at revision $revision2 doesn't exist\n";
+                    }
+                    next;
+                }
+                elsif( !defined($meta2) )
+                {
+	            $meta2 = {
+                        name => $filename,
+	                revision => '0',
+	                filehash => 'deleted'
+                    };
+                }
+            }
         }
 
-        # We need to have retrieved something useful
-        next unless ( defined ( $meta1 ) );
-
-        # Files to date if the working copy and repo copy have the same
-        # revision, and the working copy is unmodified
-        if ( not defined ( $meta2 ) and $wrev eq $meta1->{revision} and
-             ( ( $state->{entries}{$filename}{unchanged} and
-                 ( not defined ( $state->{entries}{$filename}{conflict} ) or
-                   $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
-               ( defined($state->{entries}{$filename}{modified_hash}) and
-                 $state->{entries}{$filename}{modified_hash} eq
-                        $meta1->{filehash} ) ) )
+        if( $meta1->{filehash} eq $meta2->{filehash} )
         {
+            $log->info("unchanged $filename");
             next;
         }
 
-        # Apparently we only show diffs for locally modified files
-        unless ( defined($meta2) or
-                 defined ( $state->{entries}{$filename}{modified_filename} ) )
+        # Retrieve revision contents:
+        ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+        transmitfile($meta1->{filehash}, { targetfile => $file1 });
+
+        if(!defined($file2))
         {
-            next;
+            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+            transmitfile($meta2->{filehash}, { targetfile => $file2 });
         }
 
-        print "M Index: $filename\n";
+        # Generate the actual diff:
+        print "M Index: $argFilename\n";
         print "M =======" . ( "=" x 60 ) . "\n";
         print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
-        if ( defined ( $meta1 ) )
+        if ( defined ( $meta1 ) && $meta1->{revision} ne "0" )
         {
             print "M retrieving revision $meta1->{revision}\n"
         }
-        if ( defined ( $meta2 ) )
+        if ( defined ( $meta2 ) && $meta2->{revision} ne "0" )
         {
             print "M retrieving revision $meta2->{revision}\n"
         }
@@ -1914,33 +2140,73 @@ sub req_diff
                 }
             }
         }
-        print "$filename\n";
+        print "$argFilename\n";
 
         $log->info("Diffing $filename -r $meta1->{revision} -r " .
                    ( $meta2->{revision} or "workingcopy" ));
 
-        ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
-
-        if ( exists $state->{opt}{u} )
+        # TODO: Use --label instead of -L because -L is no longer
+        #  documented and may go away someday.  Not sure if there there are
+        #  versions that only support -L, which would make this change risky?
+        #  http://osdir.com/ml/bug-gnu-utils-gnu/2010-12/msg00060.html
+        #    ("man diff" should actually document the best migration strategy,
+        #  [current behavior, future changes, old compatibility issues
+        #  or lack thereof, etc], not just stop mentioning the option...)
+        # TODO: Real CVS seems to include a date in the label, before
+        #  the revision part, without the keyword "revision".  The following
+        #  has minimal changes compared to original versions of
+        #  git-cvsserver.perl.  (Mostly tab vs space after filename.)
+
+        my (@diffCmd) = ( 'diff' );
+        if ( exists($state->{opt}{N}) )
         {
-            system("diff -u -L '$filename revision $meta1->{revision}'" .
-                        " -L '$filename " .
-                        ( defined($meta2->{revision}) ?
-                                "revision $meta2->{revision}" :
-                                "working copy" ) .
-                        "' $file1 $file2 > $filediff" );
-        } else {
-            system("diff $file1 $file2 > $filediff");
+            push @diffCmd,"-N";
         }
+        if ( exists $state->{opt}{u} )
+        {
+            push @diffCmd,("-u","-L");
+            if( $meta1->{filehash} eq "deleted" )
+            {
+                push @diffCmd,"/dev/null";
+            } else {
+                push @diffCmd,("$argFilename\trevision $meta1->{revision}");
+            }
 
-        while ( <$fh> )
+            if( defined($meta2->{filehash}) )
+            {
+                if( $meta2->{filehash} eq "deleted" )
+                {
+                    push @diffCmd,("-L","/dev/null");
+                } else {
+                    push @diffCmd,("-L",
+                                   "$argFilename\trevision $meta2->{revision}");
+                }
+            } else {
+                push @diffCmd,("-L","$argFilename\tworking copy");
+            }
+        }
+        push @diffCmd,($file1,$file2);
+        if(!open(DIFF,"-|",@diffCmd))
         {
-            print "M $_";
+            $log->warn("Unable to run diff: $!");
         }
-        close $fh;
+        my($diffLine);
+        while(defined($diffLine=<DIFF>))
+        {
+            print "M $diffLine";
+            $foundDiff=1;
+        }
+        close(DIFF);
     }
 
-    print "ok\n";
+    if($foundDiff)
+    {
+        print "error  \n";
+    }
+    else
+    {
+        print "ok\n";
+    }
 }
 
 sub req_log
@@ -2160,7 +2426,7 @@ sub argsplit
         $opt = { A => 0, N => 0, P => 0, R => 0, c => 0, f => 0, l => 0, n => 0, p => 0, s => 0, r => 1, D => 1, d => 1, k => 1, j => 1, } if ( $type eq "co" );
         $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
         $opt = { A => 0, P => 0, C => 0, d => 0, f => 0, l => 0, R => 0, p => 0, k => 1, r => 1, D => 1, j => 1, I => 1, W => 1 } if ( $type eq "update" );
-        $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
+        $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2, N => 0 } if ( $type eq "diff" );
         $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
         $opt = { k => 1, m => 1 } if ( $type eq "add" );
         $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
@@ -3101,6 +3367,45 @@ sub descramble
     return $ret;
 }
 
+# Test if the (deep) values of two references to a hash are the same.
+sub refHashEqual
+{
+    my($v1,$v2) = @_;
+
+    my $out;
+    if(!defined($v1))
+    {
+        if(!defined($v2))
+        {
+            $out=1;
+        }
+    }
+    elsif( !defined($v2) ||
+           scalar(keys(%{$v1})) != scalar(keys(%{$v2})) )
+    {
+        # $out=undef;
+    }
+    else
+    {
+        $out=1;
+
+        my $key;
+        foreach $key (keys(%{$v1}))
+        {
+            if( !exists($v2->{$key}) ||
+                defined($v1->{$key}) ne defined($v2->{$key}) ||
+                ( defined($v1->{$key}) &&
+                  $v1->{$key} ne $v2->{$key} ) )
+            {
+               $out=undef;
+               last;
+            }
+        }
+    }
+
+    return $out;
+}
+
 
 package GITCVS::log;
 
@@ -3506,6 +3811,8 @@ sub update
     my $lastcommit = $self->_get_prop("last_commit");
 
     if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
+         # invalidate the gethead cache
+         $self->clearCommitRefCaches();
          return 1;
     }
 
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 19/20] cvsserver: add t9402 to test branch and tag refs
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (17 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 18/20] cvsserver: support -r and sticky tags for most operations Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-14  5:42 ` [PATCH 20/20] cvsserver Documentation: new cvs ... -r support Matthew Ogilvie
  2012-10-16 19:25 ` [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Junio C Hamano
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 t/t9402-git-cvsserver-refs.sh | 558 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 558 insertions(+)
 create mode 100755 t/t9402-git-cvsserver-refs.sh

diff --git a/t/t9402-git-cvsserver-refs.sh b/t/t9402-git-cvsserver-refs.sh
new file mode 100755
index 0000000..a256faa
--- /dev/null
+++ b/t/t9402-git-cvsserver-refs.sh
@@ -0,0 +1,558 @@
+#!/bin/sh
+
+test_description='git-cvsserver and git refspecs
+
+tests ability for git-cvsserver to switch between and compare
+tags, branches and other git refspecs'
+
+. ./test-lib.sh
+
+#########
+
+check_start_tree() {
+    rm -f "$WORKDIR/check.list"
+    echo "start $1" >> "${WORKDIR}/check.log"
+}
+
+check_file() {
+    sandbox="$1"
+    file="$2"
+    ver="$3"
+    GIT_DIR=$SERVERDIR git show "${ver}:${file}" \
+        > "$WORKDIR/check.got" 2> "$WORKDIR/check.stderr"
+    test_cmp "$WORKDIR/check.got" "$sandbox/$file"
+    stat=$?
+    echo "check_file $sandbox $file $ver : $stat" >> "$WORKDIR/check.log"
+    echo "$file" >> "$WORKDIR/check.list"
+    return $stat
+}
+
+check_end_tree() {
+    sandbox="$1"
+    expectCount=$(wc -l < "$WORKDIR/check.list")
+    cvsCount=$(find "$sandbox" -name CVS -prune -o -type f -print | wc -l)
+    test x"$cvsCount" = x"$expectCount"
+    stat=$?
+    echo "check_end $sandbox : $stat cvs=$cvsCount expect=$expectCount" \
+        >> "$WORKDIR/check.log"
+    return $stat
+}
+
+check_end_full_tree() {
+    sandbox="$1"
+    ver="$2"
+    expectCount=$(wc -l < "$WORKDIR/check.list")
+    cvsCount=$(find "$sandbox" -name CVS -prune -o -type f -print | wc -l)
+    gitCount=$(git ls-tree -r "$2" | wc -l)
+    test x"$cvsCount" = x"$expectCount" -a x"$gitCount" = x"$expectCount"
+    stat=$?
+    echo "check_end $sandbox : $stat cvs=$cvsCount git=$gitCount expect=$expectCount" \
+        >> "$WORKDIR/check.log"
+    return $stat
+}
+
+#########
+
+check_diff() {
+    diffFile="$1"
+    vOld="$2"
+    vNew="$3"
+    rm -rf diffSandbox
+    git clone -q -n . diffSandbox &&
+    ( cd diffSandbox &&
+      git checkout "$vOld" &&
+      git apply -p0 --index <"../$diffFile" &&
+      git diff --exit-code "$vNew" ) > check_diff_apply.out 2>&1
+}
+
+#########
+
+cvs >/dev/null 2>&1
+if test $? -ne 1
+then
+    skip_all='skipping git-cvsserver tests, cvs not found'
+    test_done
+fi
+if ! test_have_prereq PERL
+then
+    skip_all='skipping git-cvsserver tests, perl not available'
+    test_done
+fi
+"$PERL_PATH" -e 'use DBI; use DBD::SQLite' >/dev/null 2>&1 || {
+    skip_all='skipping git-cvsserver tests, Perl SQLite interface unavailable'
+    test_done
+}
+
+unset GIT_DIR GIT_CONFIG
+WORKDIR=$(pwd)
+SERVERDIR=$(pwd)/gitcvs.git
+git_config="$SERVERDIR/config"
+CVSROOT=":fork:$SERVERDIR"
+CVSWORK="$(pwd)/cvswork"
+CVS_SERVER=git-cvsserver
+export CVSROOT CVS_SERVER
+
+rm -rf "$CVSWORK" "$SERVERDIR"
+test_expect_success 'setup v1, b1' '
+    echo "Simple text file" > textfile.c &&
+    echo "t2" > t2 &&
+    mkdir adir &&
+    echo "adir/afile line1" > adir/afile &&
+    echo "adir/afile line2" >> adir/afile &&
+    echo "adir/afile line3" >> adir/afile &&
+    echo "adir/afile line4" >> adir/afile &&
+    echo "adir/a2file" >> adir/a2file &&
+    mkdir adir/bdir &&
+    echo "adir/bdir/bfile line 1" > adir/bdir/bfile &&
+    echo "adir/bdir/bfile line 2" >> adir/bdir/bfile &&
+    echo "adir/bdir/b2file" > adir/bdir/b2file &&
+    git add textfile.c t2 adir &&
+    git commit -q -m "First Commit (v1)" &&
+    git tag v1 &&
+    git branch b1 &&
+    git clone -q --bare "$WORKDIR/.git" "$SERVERDIR" >/dev/null 2>&1 &&
+    GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled true &&
+    GIT_DIR="$SERVERDIR" git config gitcvs.logfile "$SERVERDIR/gitcvs.log"
+'
+
+rm -rf cvswork
+test_expect_success 'cvs co v1' '
+    cvs -f -Q co -r v1 -d cvswork master >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1 &&
+    check_file cvswork t2 v1 &&
+    check_file cvswork adir/afile v1 &&
+    check_file cvswork adir/a2file v1 &&
+    check_file cvswork adir/bdir/bfile v1 &&
+    check_file cvswork adir/bdir/b2file v1 &&
+    check_end_tree cvswork
+'
+
+rm -rf cvswork
+test_expect_success 'cvs co b1' '
+    cvs -f co -r b1 -d cvswork master >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1 &&
+    check_file cvswork t2 v1 &&
+    check_file cvswork adir/afile v1 &&
+    check_file cvswork adir/a2file v1 &&
+    check_file cvswork adir/bdir/bfile v1 &&
+    check_file cvswork adir/bdir/b2file v1 &&
+    check_end_tree cvswork
+'
+
+test_expect_success 'cvs co b1 [cvswork3]' '
+    cvs -f co -r b1 -d cvswork3 master >cvs.log 2>&1 &&
+    check_start_tree cvswork3 &&
+    check_file cvswork3 textfile.c v1 &&
+    check_file cvswork3 t2 v1 &&
+    check_file cvswork3 adir/afile v1 &&
+    check_file cvswork3 adir/a2file v1 &&
+    check_file cvswork3 adir/bdir/bfile v1 &&
+    check_file cvswork3 adir/bdir/b2file v1 &&
+    check_end_full_tree cvswork3 v1
+'
+
+test_expect_success 'edit cvswork3 and save diff' '
+    ( cd cvswork3 &&
+      sed -i -e "s/line1/line1 - data/" adir/afile &&
+      echo "afile5" > adir/afile5 &&
+      rm t2 &&
+      cvs -f add adir/afile5 &&
+      cvs -f rm t2 &&
+      test_must_fail cvs -f diff -N -u >"$WORKDIR/cvswork3edit.diff"
+    )
+'
+
+test_expect_success 'setup v1.2 on b1' '
+    git checkout b1 &&
+    echo "new v1.2" > t3 &&
+    rm t2 &&
+    sed -i -e "s/line3/line3 - more data/" adir/afile &&
+    rm adir/a2file &&
+    echo "a3file" >> adir/a3file &&
+    echo "bfile line 3" >> adir/bdir/bfile &&
+    rm adir/bdir/b2file &&
+    echo "b3file" > adir/bdir/b3file &&
+    mkdir cdir &&
+    echo "cdir/cfile" > cdir/cfile &&
+    git add -A cdir adir t3 t2 &&
+    git commit -q -m 'v1.2' &&
+    git tag v1.2 &&
+    git push --tags gitcvs.git b1:b1
+'
+
+test_expect_success 'cvs -f up (on b1 adir)' '
+    ( cd cvswork/adir &&
+      cvs -f up -d ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1 &&
+    check_file cvswork t2 v1 &&
+    check_file cvswork adir/afile v1.2 &&
+    check_file cvswork adir/a3file v1.2 &&
+    check_file cvswork adir/bdir/bfile v1.2 &&
+    check_file cvswork adir/bdir/b3file v1.2 &&
+    check_end_tree cvswork
+'
+
+test_expect_success 'cvs up (on b1 /)' '
+    ( cd cvswork &&
+      cvs -f up -d ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1.2 &&
+    check_file cvswork t3 v1.2 &&
+    check_file cvswork adir/afile v1.2 &&
+    check_file cvswork adir/a3file v1.2 &&
+    check_file cvswork adir/bdir/bfile v1.2 &&
+    check_file cvswork adir/bdir/b3file v1.2 &&
+    check_file cvswork cdir/cfile v1.2 &&
+    check_end_tree cvswork
+'
+
+# Make sure "CVS/Tag" files didn't get messed up:
+test_expect_success 'cvs up (on b1 /) (again; check CVS/Tag files)' '
+    ( cd cvswork &&
+      cvs -f up -d ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1.2 &&
+    check_file cvswork t3 v1.2 &&
+    check_file cvswork adir/afile v1.2 &&
+    check_file cvswork adir/a3file v1.2 &&
+    check_file cvswork adir/bdir/bfile v1.2 &&
+    check_file cvswork adir/bdir/b3file v1.2 &&
+    check_file cvswork cdir/cfile v1.2 &&
+    check_end_tree cvswork
+'
+
+# update to another version:
+test_expect_success 'cvs up -r v1' '
+    ( cd cvswork &&
+      cvs -f up -r v1 ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1 &&
+    check_file cvswork t2 v1 &&
+    check_file cvswork adir/afile v1 &&
+    check_file cvswork adir/a2file v1 &&
+    check_file cvswork adir/bdir/bfile v1 &&
+    check_file cvswork adir/bdir/b2file v1 &&
+    check_end_tree cvswork
+'
+
+test_expect_success 'cvs up' '
+    ( cd cvswork &&
+      cvs -f up ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1 &&
+    check_file cvswork t2 v1 &&
+    check_file cvswork adir/afile v1 &&
+    check_file cvswork adir/a2file v1 &&
+    check_file cvswork adir/bdir/bfile v1 &&
+    check_file cvswork adir/bdir/b2file v1 &&
+    check_end_tree cvswork
+'
+
+test_expect_success 'cvs up (again; check CVS/Tag files)' '
+    ( cd cvswork &&
+      cvs -f up -d ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1 &&
+    check_file cvswork t2 v1 &&
+    check_file cvswork adir/afile v1 &&
+    check_file cvswork adir/a2file v1 &&
+    check_file cvswork adir/bdir/bfile v1 &&
+    check_file cvswork adir/bdir/b2file v1 &&
+    check_end_tree cvswork
+'
+
+test_expect_success 'setup simple b2' '
+    git branch b2 v1 &&
+    git push --tags gitcvs.git b2:b2
+'
+
+test_expect_success 'cvs co b2 [into cvswork2]' '
+    cvs -f co -r b2 -d cvswork2 master >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1 &&
+    check_file cvswork t2 v1 &&
+    check_file cvswork adir/afile v1 &&
+    check_file cvswork adir/a2file v1 &&
+    check_file cvswork adir/bdir/bfile v1 &&
+    check_file cvswork adir/bdir/b2file v1 &&
+    check_end_tree cvswork
+'
+
+test_expect_success 'root dir edit [cvswork2]' '
+    ( cd cvswork2 &&
+      echo "Line 2" >> textfile.c &&
+      test_must_fail cvs -f diff -u >"$WORKDIR/cvsEdit1.diff" &&
+      cvs -f commit -m "edit textfile.c" textfile.c
+    ) >cvsEdit1.log 2>&1
+'
+
+test_expect_success 'root dir rm file [cvswork2]' '
+    ( cd cvswork2 &&
+      cvs -f rm -f t2 &&
+      cvs -f diff -u > ../cvsEdit2-empty.diff &&
+      test_must_fail cvs -f diff -N -u >"$WORKDIR/cvsEdit2-N.diff" &&
+      cvs -f commit -m "rm t2"
+    ) > cvsEdit2.log 2>&1
+'
+
+test_expect_success 'subdir edit/add/rm files [cvswork2' '
+    ( cd cvswork2 &&
+      sed -i -e "s/line 1/line 1 (v2)/" adir/bdir/bfile &&
+      rm adir/bdir/b2file &&
+      cd adir &&
+      cvs -f rm bdir/b2file &&
+      echo "4th file" > bdir/b4file &&
+      cvs -f add bdir/b4file &&
+      test_must_fail cvs -f diff -N -u >"$WORKDIR/cvsEdit3.diff" &&
+      git fetch gitcvs.git b2:b2 &&
+      ( cd .. &&
+        test_must_fail cvs -f diff -u -N -r v1.2 >"$WORKDIR/cvsEdit3-v1.2.diff" &&
+        test_must_fail cvs -f diff -u -N -r v1.2 -r v1 >"$WORKDIR/cvsEdit3-v1.2-v1.diff"
+      ) &&
+      cvs -f commit -m "various add/rm/edit"
+    ) >cvs.log 2>&1
+'
+
+test_expect_success 'validate result of edits [cvswork2]' '
+    git fetch gitcvs.git b2:b2 &&
+    git tag v2 b2 &&
+    git push --tags gitcvs.git b2:b2 &&
+    check_start_tree cvswork2 &&
+    check_file cvswork2 textfile.c v2 &&
+    check_file cvswork2 adir/afile v2 &&
+    check_file cvswork2 adir/a2file v2 &&
+    check_file cvswork2 adir/bdir/bfile v2 &&
+    check_file cvswork2 adir/bdir/b4file v2 &&
+    check_end_full_tree cvswork2 v2
+'
+
+test_expect_success 'validate basic diffs saved during above cvswork2 edits' '
+    test $(grep Index: cvsEdit1.diff | wc -l) = 1 &&
+    test ! -s cvsEdit2-empty.diff &&
+    test $(grep Index: cvsEdit2-N.diff | wc -l) = 1 &&
+    test $(grep Index: cvsEdit3.diff | wc -l) = 3 &&
+    rm -rf diffSandbox &&
+    git clone -q -n . diffSandbox &&
+    ( cd diffSandbox &&
+      git checkout v1 &&
+      git apply -p0 --index <"$WORKDIR/cvsEdit1.diff" &&
+      git apply -p0 --index <"$WORKDIR/cvsEdit2-N.diff" &&
+      git apply -p0 --directory=adir --index <"$WORKDIR/cvsEdit3.diff" &&
+      git diff --exit-code v2 ) >"check_diff_apply.out" 2>&1
+'
+
+test_expect_success 'validate v1.2 diff saved during last cvswork2 edit' '
+    test $(grep Index: cvsEdit3-v1.2.diff | wc -l) = 9 &&
+    check_diff cvsEdit3-v1.2.diff v1.2 v2
+'
+
+test_expect_success 'validate v1.2 v1 diff saved during last cvswork2 edit' '
+    test $(grep Index: cvsEdit3-v1.2-v1.diff | wc -l) = 9 &&
+    check_diff cvsEdit3-v1.2-v1.diff v1.2 v1
+'
+
+test_expect_success 'cvs up [cvswork2]' '
+    ( cd cvswork2 &&
+      cvs -f up ) >cvs.log 2>&1 &&
+    check_start_tree cvswork2 &&
+    check_file cvswork2 textfile.c v2 &&
+    check_file cvswork2 adir/afile v2 &&
+    check_file cvswork2 adir/a2file v2 &&
+    check_file cvswork2 adir/bdir/bfile v2 &&
+    check_file cvswork2 adir/bdir/b4file v2 &&
+    check_end_full_tree cvswork2 v2
+'
+
+test_expect_success 'cvs up -r b2 [back to cvswork]' '
+    ( cd cvswork &&
+      cvs -f up -r b2 ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v2 &&
+    check_file cvswork adir/afile v2 &&
+    check_file cvswork adir/a2file v2 &&
+    check_file cvswork adir/bdir/bfile v2 &&
+    check_file cvswork adir/bdir/b4file v2 &&
+    check_end_full_tree cvswork v2
+'
+
+test_expect_success 'cvs up -r b1' '
+    ( cd cvswork &&
+      cvs -f up -r b1 ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1.2 &&
+    check_file cvswork t3 v1.2 &&
+    check_file cvswork adir/afile v1.2 &&
+    check_file cvswork adir/a3file v1.2 &&
+    check_file cvswork adir/bdir/bfile v1.2 &&
+    check_file cvswork adir/bdir/b3file v1.2 &&
+    check_file cvswork cdir/cfile v1.2 &&
+    check_end_full_tree cvswork v1.2
+'
+
+test_expect_success 'cvs up -A' '
+    ( cd cvswork &&
+      cvs -f up -A ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1 &&
+    check_file cvswork t2 v1 &&
+    check_file cvswork adir/afile v1 &&
+    check_file cvswork adir/a2file v1 &&
+    check_file cvswork adir/bdir/bfile v1 &&
+    check_file cvswork adir/bdir/b2file v1 &&
+    check_end_full_tree cvswork v1
+'
+
+test_expect_success 'cvs up (check CVS/Tag files)' '
+    ( cd cvswork &&
+      cvs -f up ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1 &&
+    check_file cvswork t2 v1 &&
+    check_file cvswork adir/afile v1 &&
+    check_file cvswork adir/a2file v1 &&
+    check_file cvswork adir/bdir/bfile v1 &&
+    check_file cvswork adir/bdir/b2file v1 &&
+    check_end_full_tree cvswork v1
+'
+
+# This is not really legal CVS, but it seems to work anyway:
+test_expect_success 'cvs up -r heads/b1' '
+    ( cd cvswork &&
+      cvs -f up -r heads/b1 ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1.2 &&
+    check_file cvswork t3 v1.2 &&
+    check_file cvswork adir/afile v1.2 &&
+    check_file cvswork adir/a3file v1.2 &&
+    check_file cvswork adir/bdir/bfile v1.2 &&
+    check_file cvswork adir/bdir/b3file v1.2 &&
+    check_file cvswork cdir/cfile v1.2 &&
+    check_end_full_tree cvswork v1.2
+'
+
+# But this should work even if CVS client checks -r more carefully:
+test_expect_success 'cvs up -r heads_-s-b2 (cvsserver escape mechanism)' '
+    ( cd cvswork &&
+      cvs -f up -r heads_-s-b2 ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v2 &&
+    check_file cvswork adir/afile v2 &&
+    check_file cvswork adir/a2file v2 &&
+    check_file cvswork adir/bdir/bfile v2 &&
+    check_file cvswork adir/bdir/b4file v2 &&
+    check_end_full_tree cvswork v2
+'
+
+v1hash=$(git rev-parse v1)
+test_expect_success 'cvs up -r $(git rev-parse v1)' '
+    test -n "$v1hash" &&
+    ( cd cvswork &&
+      cvs -f up -r "$v1hash" ) >cvs.log 2>&1 &&
+    check_start_tree cvswork &&
+    check_file cvswork textfile.c v1 &&
+    check_file cvswork t2 v1 &&
+    check_file cvswork adir/afile v1 &&
+    check_file cvswork adir/a2file v1 &&
+    check_file cvswork adir/bdir/bfile v1 &&
+    check_file cvswork adir/bdir/b2file v1 &&
+    check_end_full_tree cvswork v1
+'
+
+test_expect_success 'cvs diff -r v1 -u' '
+    ( cd cvswork &&
+      cvs -f diff -r v1 -u ) >cvsDiff.out 2>cvs.log &&
+    test ! -s cvsDiff.out &&
+    test ! -s cvs.log
+'
+
+test_expect_success 'cvs diff -N -r v2 -u' '
+    ( cd cvswork &&
+      test_must_fail cvs -f diff -N -r v2 -u ) >cvsDiff.out 2>cvs.log &&
+    test ! -s cvs.log &&
+    test -s cvsDiff.out &&
+    check_diff cvsDiff.out v2 v1 > check_diff.out 2>&1
+'
+
+test_expect_success 'cvs diff -N -r v2 -r v1.2' '
+    ( cd cvswork &&
+      test_must_fail cvs -f diff -N -r v2 -r v1.2 -u ) >cvsDiff.out 2>cvs.log &&
+    test ! -s cvs.log &&
+    test -s cvsDiff.out &&
+    check_diff cvsDiff.out v2 v1.2 > check_diff.out 2>&1
+'
+
+test_expect_success 'apply early [cvswork3] diff to b3' '
+    git clone -q . gitwork3 &&
+    ( cd gitwork3 &&
+      git checkout -b b3 v1 &&
+      git apply -p0 --index <"$WORKDIR/cvswork3edit.diff" &&
+      git commit -m "cvswork3 edits applied" ) &&
+    git fetch gitwork3 b3:b3 &&
+    git tag v3 b3
+'
+
+test_expect_success 'check [cvswork3] diff' '
+    ( cd cvswork3 &&
+      test_must_fail cvs -f diff -N -u ) >"$WORKDIR/cvsDiff.out" 2>cvs.log &&
+    test ! -s cvs.log &&
+    test -s cvsDiff.out &&
+    test $(grep Index: cvsDiff.out | wc -l) = 3 &&
+    test_cmp cvsDiff.out cvswork3edit.diff &&
+    check_diff cvsDiff.out v1 v3 > check_diff.out 2>&1
+'
+
+test_expect_success 'merge early [cvswork3] b3 with b1' '
+    ( cd gitwork3 &&
+      git merge "message" HEAD b1 )
+    git fetch gitwork3 b3:b3 &&
+    git tag v3merged b3 &&
+    git push --tags gitcvs.git b3:b3
+'
+
+# This test would fail if cvsserver properly created a ".#afile"* file
+# for the merge.
+# TODO: Validate that the .# file was saved properly, and then
+#   delete/ignore it when checking the tree.
+test_expect_success 'cvs up dirty [cvswork3]' '
+    ( cd cvswork3 &&
+      cvs -f up &&
+      test_must_fail cvs -f diff -N -u >"$WORKDIR/cvsDiff.out" ) >cvs.log 2>&1 &&
+    test -s cvsDiff.out &&
+    test $(grep Index: cvsDiff.out | wc -l) = 2
+    check_start_tree cvswork3 &&
+    check_file cvswork3 textfile.c v3merged &&
+    check_file cvswork3 t3 v3merged &&
+    check_file cvswork3 adir/afile v3merged &&
+    check_file cvswork3 adir/a3file v3merged &&
+    check_file cvswork3 adir/afile5 v3merged &&
+    check_file cvswork3 adir/bdir/bfile v3merged &&
+    check_file cvswork3 adir/bdir/b3file v3merged &&
+    check_file cvswork3 cdir/cfile v3merged &&
+    check_end_full_tree cvswork3 v3merged
+'
+
+# TODO: test cvs status
+
+test_expect_success 'cvs commit [cvswork3' '
+    ( cd cvswork3 &&
+      cvs -f commit -m "dirty sandbox after auto-merge"
+    ) > cvs.log 2>&1 &&
+    check_start_tree cvswork3 &&
+    check_file cvswork3 textfile.c v3merged &&
+    check_file cvswork3 t3 v3merged &&
+    check_file cvswork3 adir/afile v3merged &&
+    check_file cvswork3 adir/a3file v3merged &&
+    check_file cvswork3 adir/afile5 v3merged &&
+    check_file cvswork3 adir/bdir/bfile v3merged &&
+    check_file cvswork3 adir/bdir/b3file v3merged &&
+    check_file cvswork3 cdir/cfile v3merged &&
+    check_end_full_tree cvswork3 v3merged &&
+    git fetch gitcvs.git b3:b4 &&
+    git tag v4.1 b4 &&
+    git diff --exit-code v4.1 v3merged > check_diff_apply.out 2>&1
+'
+
+test_done
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH 20/20] cvsserver Documentation: new cvs ... -r support
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (18 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 19/20] cvsserver: add t9402 to test branch and tag refs Matthew Ogilvie
@ 2012-10-14  5:42 ` Matthew Ogilvie
  2012-10-16 19:25 ` [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Junio C Hamano
  20 siblings, 0 replies; 22+ messages in thread
From: Matthew Ogilvie @ 2012-10-14  5:42 UTC (permalink / raw)
  To: git; +Cc: Matthew Ogilvie

Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net>
---
 Documentation/git-cvsserver.txt | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/Documentation/git-cvsserver.txt b/Documentation/git-cvsserver.txt
index 88d814a..940c2ba 100644
--- a/Documentation/git-cvsserver.txt
+++ b/Documentation/git-cvsserver.txt
@@ -359,6 +359,43 @@ Operations supported
 
 All the operations required for normal use are supported, including
 checkout, diff, status, update, log, add, remove, commit.
+
+Most CVS command arguments that read CVS tags or revision numbers
+(typically -r) work, and also support any git refspec
+(tag, branch, commit ID, etc).
+However, CVS revision numbers for non-default branches are not well
+emulated, and cvs log does not show tags or branches at
+all.  (Non-main-branch CVS revision numbers superficially resemble CVS
+revision numbers, but they actually encode a git commit ID directly,
+rather than represent the number of revisions since the branch point.)
+
+Note that there are two ways to checkout a particular branch.
+As described elsewhere on this page, the "module" parameter
+of cvs checkout is interpreted as a branch name, and it becomes
+the main branch.  It remains the main branch for a given sandbox
+even if you temporarily make another branch sticky with
+cvs update -r.  Alternatively, the -r argument can indicate
+some other branch to actually checkout, even though the module
+is still the "main" branch.  Tradeoffs (as currently
+implemented): Each new "module" creates a new database on disk with
+a history for the given module, and after the database is created,
+operations against that main branch are fast.  Or alternatively,
+-r doesn't take any extra disk space, but may be significantly slower for
+many operations, like cvs update.
+
+If you want to refer to a git refspec that has characters that are
+not allowed by CVS, you have two options.  First, it may just work
+to supply the git refspec directly to the appropriate CVS -r argument;
+some CVS clients don't seem to do much sanity checking of the argument.
+Second, if that fails, you can use a special character escape mechanism
+that only uses characters that are valid in CVS tags.  A sequence
+of 4 or 5 characters of the form (underscore (`"_"`), dash (`"-"`),
+one or two characters, and dash (`"-"`)) can encode various characters based
+on the one or two letters: `"s"` for slash (`"/"`), `"p"` for
+period (`"."`), `"u"` for underscore (`"_"`), or two hexadecimal digits
+for any byte value at all (typically an ASCII number, or perhaps a part
+of a UTF-8 encoded character).
+
 Legacy monitoring operations are not supported (edit, watch and related).
 Exports and tagging (tags and branches) are not supported at this stage.
 
-- 
1.7.10.2.484.gcd07cc5

^ permalink raw reply related	[flat|nested] 22+ messages in thread

* Re: [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs
  2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
                   ` (19 preceding siblings ...)
  2012-10-14  5:42 ` [PATCH 20/20] cvsserver Documentation: new cvs ... -r support Matthew Ogilvie
@ 2012-10-16 19:25 ` Junio C Hamano
  20 siblings, 0 replies; 22+ messages in thread
From: Junio C Hamano @ 2012-10-16 19:25 UTC (permalink / raw)
  To: Matthew Ogilvie; +Cc: git

Matthew Ogilvie <mmogilvi_git@miniinfo.net> writes:

> I don't actually intend to use any of this myself.
>
> I started it a few years ago under the theory that a more accurate
> emulation of CVS would make it easier to convince the team at $DAYJOB
> to switch to git, but we eventually switched without using
> git-cvsserver at all.

Nice ;-)

> ... That said, perhaps some of the trivial cleanup
> patches could go in now?

Not "now" now during the pre-release freeze, but the earlier patches
(up to #11, arguably together with #12 and #13) do look like nice
clean-ups to have.

Thanks.

^ permalink raw reply	[flat|nested] 22+ messages in thread

end of thread, other threads:[~2012-10-16 19:25 UTC | newest]

Thread overview: 22+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2012-10-14  5:42 [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 01/20] cvsserver t9400: add basic 'cvs log' test Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 02/20] cvsserver: removed unused sha1Or-k mode from kopts_from_path Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 03/20] cvsserver: add comments about database schema/usage Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 04/20] cvsserver update: comment about how we shouldn't remove a user-modified file Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 05/20] cvsserver: remove unused functions _headrev and gethistory Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 06/20] cvsserver: clean up client request handler map comments Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 07/20] cvsserver: split up long lines in req_{status,diff,log} Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 08/20] cvsserver: use whole CVS rev number in-process; don't strip "1." prefix Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 09/20] cvsserver: cvs add: do not expand directory arguments Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 10/20] cvsserver status: provide real sticky info Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 11/20] cvsserver: factor out git-log parsing logic Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 12/20] cvsserver: cleanup extra slashes in filename arguments Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 13/20] cvsserver: define a tag name character escape mechanism Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 14/20] cvsserver: add misc commit lookup, file meta data, and file listing functions Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 15/20] cvsserver: implement req_Sticky and related utilities Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 16/20] cvsserver: generalize getmeta() to recognize commit refs Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 17/20] cvsserver: Add version awareness to argsfromdir Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 18/20] cvsserver: support -r and sticky tags for most operations Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 19/20] cvsserver: add t9402 to test branch and tag refs Matthew Ogilvie
2012-10-14  5:42 ` [PATCH 20/20] cvsserver Documentation: new cvs ... -r support Matthew Ogilvie
2012-10-16 19:25 ` [PATCH 00/20] git-cvsserver: add support for cvs "-r" refs Junio C Hamano

Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).